Merge pull request #322 from greenart7c3/claude/migrate-pin-to-datastore-2u6eF

Migrate PIN storage from SharedPreferences to encrypted DataStore
This commit is contained in:
greenart7c3
2026-03-13 08:54:47 -03:00
committed by GitHub
5 changed files with 90 additions and 47 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)
}
}
@@ -157,22 +157,20 @@ object LocalPreferences {
}
}
fun loadPinFromEncryptedStorage(): String? {
suspend 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) {
DataStoreAccess.savePin(context, legacyPin)
sharedPrefs(context).edit { remove(SettingsKeys.PIN.key) }
}
return DataStoreAccess.loadPin(context)
}
fun savePinToEncryptedStorage(pin: String?) {
suspend 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)
}
}
}
DataStoreAccess.savePin(context, pin)
}
suspend fun reloadApp() {
@@ -90,8 +90,12 @@ fun SecurityScreen(
.padding(horizontal = 8.dp)
.clickable {
if (setupPin) {
val pin = LocalPreferences.loadPinFromEncryptedStorage()
navController.navigate("${Route.ConfirmPin.route.split("/")[0]}/$pin")
scope.launch(Dispatchers.IO) {
val pin = LocalPreferences.loadPinFromEncryptedStorage()
scope.launch(Dispatchers.Main) {
navController.navigate("${Route.ConfirmPin.route.split("/")[0]}/$pin")
}
}
} else {
navController.navigate(Route.SetupPin.route)
}
@@ -105,8 +109,12 @@ fun SecurityScreen(
checked = setupPin,
onCheckedChange = {
if (setupPin) {
val pin = LocalPreferences.loadPinFromEncryptedStorage()
navController.navigate("${Route.ConfirmPin.route.split("/")[0]}/$pin")
scope.launch(Dispatchers.IO) {
val pin = LocalPreferences.loadPinFromEncryptedStorage()
scope.launch(Dispatchers.Main) {
navController.navigate("${Route.ConfirmPin.route.split("/")[0]}/$pin")
}
}
} else {
navController.navigate(Route.SetupPin.route)
}
@@ -2,6 +2,7 @@ package com.greenart7c3.nostrsigner.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController
@@ -10,6 +11,8 @@ import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.ui.components.RandomPinInput
import com.greenart7c3.nostrsigner.ui.navigation.Route
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun SetupPinScreen(
@@ -32,6 +35,7 @@ fun ConfirmPinScreen(
navController: NavController,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
Column(
modifier,
) {
@@ -41,22 +45,21 @@ fun ConfirmPinScreen(
ToastManager.toast(context.getString(R.string.pin), context.getString(R.string.pin_does_not_match))
} else {
val usePin = Amber.instance.settings.usePin
if (usePin) {
Amber.instance.settings = Amber.instance.settings.copy(usePin = false)
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
LocalPreferences.savePinToEncryptedStorage(null)
navController.navigate(Route.Security.route) {
popUpTo(Route.Security.route) {
inclusive = true
}
scope.launch(Dispatchers.IO) {
if (usePin) {
Amber.instance.settings = Amber.instance.settings.copy(usePin = false)
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
LocalPreferences.savePinToEncryptedStorage(null)
} else {
Amber.instance.settings = Amber.instance.settings.copy(usePin = true)
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
LocalPreferences.savePinToEncryptedStorage(pin)
}
} else {
Amber.instance.settings = Amber.instance.settings.copy(usePin = true)
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
LocalPreferences.savePinToEncryptedStorage(pin)
navController.navigate(Route.Security.route) {
popUpTo(Route.Security.route) {
inclusive = true
scope.launch(Dispatchers.Main) {
navController.navigate(Route.Security.route) {
popUpTo(Route.Security.route) {
inclusive = true
}
}
}
}
@@ -15,6 +15,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -36,6 +37,7 @@ fun BiometricAuthScreen(
onAuth: (Boolean) -> Unit,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var showPinDialog by remember { mutableStateOf(false) }
val keyguardLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
@@ -117,22 +119,25 @@ fun BiometricAuthScreen(
) {
RandomPinInput(
text = stringResource(R.string.enter_pin),
onPinEntered = {
val pin = LocalPreferences.loadPinFromEncryptedStorage()
if (it == pin) {
Amber.instance.settings = Amber.instance.settings.copy(
lastBiometricsTime = System.currentTimeMillis(),
)
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
onAuth(true)
showPinDialog = false
} else {
Toast.makeText(
context,
context.getString(R.string.pin_does_not_match),
Toast.LENGTH_SHORT,
).show()
onPinEntered = { enteredPin ->
scope.launch(Dispatchers.IO) {
val pin = LocalPreferences.loadPinFromEncryptedStorage()
scope.launch(Dispatchers.Main) {
if (enteredPin == pin) {
Amber.instance.settings = Amber.instance.settings.copy(
lastBiometricsTime = System.currentTimeMillis(),
)
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
onAuth(true)
showPinDialog = false
} else {
Toast.makeText(
context,
context.getString(R.string.pin_does_not_match),
Toast.LENGTH_SHORT,
).show()
}
}
}
},
)