Migrate PIN storage from plain SharedPreferences to encrypted DataStore

The PIN was stored as plaintext in SharedPreferences despite the
function names suggesting encryption. This migrates it to the existing
DataStore infrastructure with AES-GCM encryption via SecureCryptoHelper,
using an app-level DataStore file ("app_datastore").

A one-time migration reads the legacy plaintext PIN from SharedPreferences,
writes it encrypted to the DataStore, and removes the old entry.

https://claude.ai/code/session_01TeTHfGJL42J7eHCDdhsDnP
This commit is contained in:
Claude
2026-03-13 10:51:12 +00:00
parent 3f2ba7b467
commit d039cacde9
2 changed files with 37 additions and 10 deletions
@@ -26,8 +26,21 @@ object DataStoreAccess {
)
}
@Volatile
private var appDataStore: DataStore<Preferences>? = null
private fun getAppDataStore(context: Context): DataStore<Preferences> = appDataStore ?: synchronized(this) {
appDataStore ?: PreferenceDataStoreFactory.create(
scope = Amber.instance.applicationIOScope,
produceFile = {
context.applicationContext.preferencesDataStoreFile("app_datastore")
},
).also { appDataStore = it }
}
val NOSTR_PRIVKEY = stringPreferencesKey("nostr_privkey")
val SEED_WORDS = stringPreferencesKey("seed_words")
val PIN = stringPreferencesKey("pin")
suspend fun saveEncryptedKey(context: Context, npub: String, key: Preferences.Key<String>, value: String) {
val encrypted = SecureCryptoHelper.encrypt(value)
@@ -52,4 +65,20 @@ object DataStoreAccess {
prefs.clear()
}
}
suspend fun savePin(context: Context, pin: String?) {
getAppDataStore(context).edit { prefs ->
if (pin == null) {
prefs.remove(PIN)
} else {
prefs[PIN] = SecureCryptoHelper.encrypt(pin)
}
}
}
suspend fun loadPin(context: Context): String? {
val prefs = getAppDataStore(context).data.first()
val encrypted = prefs[PIN] ?: return null
return SecureCryptoHelper.decrypt(encrypted)
}
}
@@ -159,20 +159,18 @@ object LocalPreferences {
fun loadPinFromEncryptedStorage(): String? {
val context = Amber.instance
return sharedPrefs(context).getString(SettingsKeys.PIN.key, null)
// Migration: move PIN from plain SharedPreferences to encrypted DataStore
val legacyPin = sharedPrefs(context).getString(SettingsKeys.PIN.key, null)
if (legacyPin != null) {
runBlocking { DataStoreAccess.savePin(context, legacyPin) }
sharedPrefs(context).edit { remove(SettingsKeys.PIN.key) }
}
return runBlocking { DataStoreAccess.loadPin(context) }
}
fun savePinToEncryptedStorage(pin: String?) {
val context = Amber.instance
sharedPrefs(context).edit {
apply {
if (pin == null) {
remove(SettingsKeys.PIN.key)
} else {
putString(SettingsKeys.PIN.key, pin)
}
}
}
runBlocking { DataStoreAccess.savePin(context, pin) }
}
suspend fun reloadApp() {