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)
This commit is contained in:
Kayos 2026-03-28 12:39:12 -07:00
parent c21a3b7c48
commit 02ecbfda83

View file

@ -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()
}