From 9e9192dd3beacf42429f971badde5623d92100a2 Mon Sep 17 00:00:00 2001 From: Kayos Date: Sat, 28 Mar 2026 12:49:39 -0700 Subject: [PATCH] Fix wallet keystore auth: remove biometric requirement from mnemonic key The mnemonic encryption key should be device-protected (unlocked when device is unlocked), not require biometric/PIN at time of use. This was breaking: - Wallet creation on devices without biometrics - Emulator testing entirely Changes: - Remove setUserAuthenticationRequired(true) from keystore key spec - Remove setUserAuthenticationValidityDurationSeconds() - Remove setInvalidatedByBiometricEnrollment() - Remove emulator detection hacks (isEmulator, canUseBiometricAuth) - Remove unused Build and BiometricManager imports - Add documentation explaining security model Security model: - Mnemonic encrypted with AES-256-GCM using Android Keystore key - Key is device-bound (cannot be extracted) - Key is accessible when device is unlocked - Transaction signing should use BiometricPrompt separately (future enhancement) --- .../impl/storage/CardanoKeyStorageImpl.kt | 80 ++++++------------- 1 file changed, 26 insertions(+), 54 deletions(-) diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/storage/CardanoKeyStorageImpl.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/storage/CardanoKeyStorageImpl.kt index b43318de1c..8537374a1f 100644 --- a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/storage/CardanoKeyStorageImpl.kt +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/storage/CardanoKeyStorageImpl.kt @@ -7,8 +7,6 @@ package io.element.android.features.wallet.impl.storage import android.content.Context -import android.os.Build -import androidx.biometric.BiometricManager import android.content.SharedPreferences import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyPermanentlyInvalidatedException @@ -34,6 +32,15 @@ import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec +/** + * Secure storage for Cardano wallet mnemonics using Android Keystore. + * + * Security model: + * - Mnemonic is encrypted with AES-256-GCM using a key stored in Android Keystore + * - The keystore key is device-bound (cannot be extracted) + * - The key is accessible when the device is unlocked (no additional biometric required for storage) + * - Transaction signing should use BiometricPrompt separately for user confirmation + */ @ContributesBinding(AppScope::class) class CardanoKeyStorageImpl @Inject constructor( @ApplicationContext private val context: Context, @@ -176,37 +183,16 @@ class CardanoKeyStorageImpl @Inject constructor( } /** - * Check if the device is an emulator. + * Get or create the AES secret key for encrypting the mnemonic. + * + * The key is: + * - Stored in Android Keystore (hardware-backed when available) + * - Device-bound (cannot be extracted) + * - Accessible when device is unlocked (no additional auth required) + * + * Note: Biometric/PIN confirmation for transactions should be handled separately + * at the transaction signing layer, not at the storage layer. */ - private fun isEmulator(): Boolean { - return (Build.FINGERPRINT.startsWith("generic") - || Build.FINGERPRINT.startsWith("unknown") - || Build.FINGERPRINT.contains("sdk_gphone") - || Build.MODEL.contains("google_sdk") - || Build.MODEL.contains("Emulator") - || Build.MODEL.contains("Android SDK built for x86") - || Build.MODEL.contains("sdk_gphone") - || Build.MANUFACTURER.contains("Genymotion") - || Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic") - || Build.DEVICE.startsWith("emu") - || Build.PRODUCT.contains("sdk_gphone") - || Build.HARDWARE.contains("goldfish") - || Build.HARDWARE.contains("ranchu") - || "google_sdk" == Build.PRODUCT) - } - - /** - * Check if biometric/credential authentication is available. - */ - private fun canUseBiometricAuth(): Boolean { - val biometricManager = BiometricManager.from(context) - // Check for both biometric and device credential (PIN/pattern/password) - val biometricResult = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) - val credentialResult = biometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) - return biometricResult == BiometricManager.BIOMETRIC_SUCCESS || - credentialResult == BiometricManager.BIOMETRIC_SUCCESS - } - private fun getOrCreateSecretKey(sessionId: SessionId): SecretKey { val alias = KEYSTORE_ALIAS_PREFIX + sanitizeSessionId(sessionId) @@ -217,34 +203,20 @@ class CardanoKeyStorageImpl @Inject constructor( val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE) - // On emulators or devices without biometric auth, skip user authentication requirement - // This is acceptable for debug/test builds; production builds should enforce it - val isEmulatorDevice = isEmulator() - val hasBiometricAuth = canUseBiometricAuth() - val requireUserAuth = !isEmulatorDevice && hasBiometricAuth - - Timber.d("Keystore auth check: isEmulator=$isEmulatorDevice, hasBiometricAuth=$hasBiometricAuth, requireUserAuth=$requireUserAuth") - - val keySpecBuilder = KeyGenParameterSpec.Builder( + // Key spec: device-protected, no additional user authentication required + // This allows wallet operations when device is unlocked + // Transaction signing should use BiometricPrompt separately for confirmation + val keySpec = KeyGenParameterSpec.Builder( alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setKeySize(AES_KEY_SIZE) - - if (requireUserAuth) { - keySpecBuilder - .setUserAuthenticationRequired(true) - .setUserAuthenticationValidityDurationSeconds(30) - .setInvalidatedByBiometricEnrollment(true) - Timber.i("Creating keystore key with user authentication required") - } else { - keySpecBuilder.setUserAuthenticationRequired(false) - Timber.i("Creating keystore key WITHOUT user authentication (emulator or no biometrics)") - } + .build() - keyGenerator.init(keySpecBuilder.build()) + keyGenerator.init(keySpec) + Timber.d("Created new keystore key for wallet: $alias") return keyGenerator.generateKey() } @@ -281,7 +253,7 @@ class CardanoKeyStorageImpl @Inject constructor( val secretKey = try { getOrCreateSecretKey(sessionId) } catch (e: KeyPermanentlyInvalidatedException) { - Timber.e(e, "Key invalidated due to biometric change for session: ${sessionId.value}") + Timber.e(e, "Key invalidated for session: ${sessionId.value}") throw e }