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)
This commit is contained in:
parent
02ecbfda83
commit
9e9192dd3b
1 changed files with 26 additions and 54 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue