From 02ecbfda833a7c8722dbe294bd9b624a1c215bf9 Mon Sep 17 00:00:00 2001 From: Kayos Date: Sat, 28 Mar 2026 12:39:12 -0700 Subject: [PATCH] Fix emulator detection for keystore authentication - Add additional emulator detection patterns for modern Android emulators (sdk_gphone, emu device prefix, goldfish/ranchu hardware) - On emulators or devices without biometric auth, skip user authentication requirement for keystore keys (allows wallet creation without BiometricPrompt) - Add debug logging for authentication requirement decisions - Fixes UserNotAuthenticatedException on emulators Tested on: sdk_gphone64_x86_64 (Android 14 emulator) --- .../impl/storage/CardanoKeyStorageImpl.kt | 62 +++++++++++++++++-- 1 file changed, 56 insertions(+), 6 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 4cb8643ea6..b43318de1c 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,6 +7,8 @@ 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 @@ -173,6 +175,38 @@ class CardanoKeyStorageImpl @Inject constructor( } } + /** + * Check if the device is an emulator. + */ + 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) @@ -182,19 +216,35 @@ class CardanoKeyStorageImpl @Inject constructor( } val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE) - val keySpec = KeyGenParameterSpec.Builder( + + // 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( alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setKeySize(AES_KEY_SIZE) - .setUserAuthenticationRequired(true) - .setUserAuthenticationValidityDurationSeconds(30) - .setInvalidatedByBiometricEnrollment(true) - .build() + + 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)") + } - keyGenerator.init(keySpec) + keyGenerator.init(keySpecBuilder.build()) return keyGenerator.generateKey() }