diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/MainActivity.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/MainActivity.kt index a282af1de..b6bd7f9fa 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/MainActivity.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/MainActivity.kt @@ -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 -> { diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/UnlockCoordinator.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/UnlockCoordinator.kt index 45954abeb..d4fb15f5f 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/UnlockCoordinator.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/UnlockCoordinator.kt @@ -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)