Update Android unlock flow to support both biometrics and pin fallback (#1562)

This commit is contained in:
Leendert de Borst
2026-01-31 20:25:41 +00:00
committed by Leendert de Borst
parent 510b3c95d7
commit 749980f58f
2 changed files with 40 additions and 25 deletions
@@ -10,6 +10,9 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnable
import com.facebook.react.defaults.DefaultReactActivityDelegate
import expo.modules.ReactActivityDelegateWrapper
import expo.modules.splashscreen.SplashScreenManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* The main activity of the app.
@@ -148,12 +151,20 @@ class MainActivity : ReactActivity() {
return
}
try {
vaultStore.storeEncryptionKey(encryptionKeyBase64)
vaultStore.unlockVault()
promise.resolve(true)
} catch (e: Exception) {
promise.reject("UNLOCK_ERROR", "Failed to unlock vault: ${e.message}", e)
// Run vault unlock on IO thread to avoid blocking the main thread
// (unlockVault involves file I/O and database operations)
CoroutineScope(Dispatchers.IO).launch {
try {
// Use initEncryptionKey instead of storeEncryptionKey
// storeEncryptionKey would trigger biometric prompt if biometrics is enabled
// since it tries to store the key in the biometric-protected keystore.
// For PIN unlock, we just want to set the key in memory.
vaultStore.initEncryptionKey(encryptionKeyBase64)
vaultStore.unlockVault()
promise.resolve(true)
} catch (e: Exception) {
promise.reject("UNLOCK_ERROR", "Failed to unlock vault: ${e.message}", e)
}
}
}
net.aliasvault.app.pinunlock.PinUnlockActivity.RESULT_CANCELLED -> {
@@ -39,23 +39,24 @@ class UnlockCoordinator(
/**
* Start the unlock flow by checking which auth method is enabled.
* Priority: PIN -> Biometric -> Error
* Priority: Biometric -> PIN -> Error
* Biometrics takes priority, PIN serves as fallback if biometrics fails or is unavailable.
*/
fun startUnlockFlow() {
val pinEnabled = vaultStore.isPinEnabled()
val biometricEnabled = vaultStore.isBiometricAuthEnabled()
when {
pinEnabled -> {
// PIN is enabled - launch PIN unlock activity
Log.d(TAG, "PIN unlock is enabled, launching PIN unlock activity")
launchPinUnlock()
}
biometricEnabled -> {
// Only biometric is enabled - attempt biometric unlock
// Biometric is enabled - attempt biometric unlock first
Log.d(TAG, "Biometric unlock is enabled, attempting biometric unlock")
attemptBiometricUnlock()
}
pinEnabled -> {
// Only PIN is enabled - launch PIN unlock activity
Log.d(TAG, "PIN unlock is enabled, launching PIN unlock activity")
launchPinUnlock()
}
else -> {
// Neither PIN nor biometric is enabled
Log.e(TAG, "No unlock method is enabled or available")
@@ -66,16 +67,18 @@ class UnlockCoordinator(
/**
* Launch PIN unlock activity.
* Can be called directly to retry PIN unlock.
*/
private fun launchPinUnlock() {
fun launchPinUnlock() {
val intent = Intent(activity, PinUnlockActivity::class.java)
activity.startActivityForResult(intent, REQUEST_CODE_PIN_UNLOCK)
}
/**
* Attempt biometric unlock using the keystore provider.
* Can be called directly to retry biometric unlock.
*/
private fun attemptBiometricUnlock() {
fun attemptBiometricUnlock() {
val keystoreProvider = AndroidKeystoreProvider(activity.applicationContext) { activity }
keystoreProvider.retrieveKeyExternal(
activity,
@@ -164,20 +167,21 @@ class UnlockCoordinator(
/**
* Handle errors during biometric keystore retrieval.
* Falls back to PIN if enabled, otherwise reports the error.
*/
private fun handleBiometricKeystoreError(e: Exception) {
// For any biometric error, try PIN fallback if enabled
if (vaultStore.isPinEnabled()) {
Log.d(TAG, "Biometric failed (${e.message}), falling back to PIN")
launchPinUnlock()
return // Don't call onError, we're falling back to PIN
}
// No PIN fallback available - report the error
val errorMessage = when {
e.message?.contains("user canceled", ignoreCase = true) == true ||
e.message?.contains("authentication failed", ignoreCase = true) == true -> {
// User cancelled or biometric failed - check if PIN is available as fallback
if (vaultStore.isPinEnabled()) {
Log.d(TAG, "Biometric cancelled/failed, falling back to PIN")
launchPinUnlock()
return // Don't call onError, we're falling back to PIN
} else {
"Authentication cancelled"
}
}
e.message?.contains("canceled", ignoreCase = true) == true ->
"Authentication cancelled"
else -> "Failed to retrieve encryption key"
}
onError(errorMessage)