fix: Better biometric exception implementation

This commit is contained in:
J-Jamet
2025-09-09 13:37:50 +02:00
parent e8ec27dc38
commit a46251be7b
2 changed files with 48 additions and 44 deletions

View File

@@ -380,7 +380,7 @@ class DeviceUnlockManager(private var appContext: Context) {
} }
} }
fun deviceUnlockError(error: Exception, context: Context): String { fun deviceUnlockError(error: Throwable, context: Context): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& (error is UnrecoverableKeyException && (error is UnrecoverableKeyException
|| error is KeyPermanentlyInvalidatedException)) { || error is KeyPermanentlyInvalidatedException)) {

View File

@@ -20,11 +20,14 @@ import com.kunzisoft.keepass.model.CipherDecryptDatabase
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.CredentialStorage import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.crypto.Cipher import javax.crypto.Cipher
@@ -37,6 +40,8 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
private var deviceUnlockManager: DeviceUnlockManager? = null private var deviceUnlockManager: DeviceUnlockManager? = null
private var databaseUri: Uri? = null private var databaseUri: Uri? = null
private var mCipherJob: Job? = null
private var deviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE private var deviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE
var cryptoPrompt: DeviceUnlockCryptoPrompt? = null var cryptoPrompt: DeviceUnlockCryptoPrompt? = null
private set private set
@@ -95,6 +100,18 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
cipherDatabaseAction.registerDatabaseListener(cipherDatabaseListener) cipherDatabaseAction.registerDatabaseListener(cipherDatabaseListener)
} }
private fun cancelAndLaunchCipherJob(
coroutineExceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, e ->
setException(e)
},
block: suspend () -> Unit
) {
mCipherJob?.cancel()
mCipherJob = viewModelScope.launch(coroutineExceptionHandler) {
block()
}
}
fun checkConditionToStoreCredential(condition: Boolean) { fun checkConditionToStoreCredential(condition: Boolean) {
isConditionToStoreCredentialVerified = condition isConditionToStoreCredentialVerified = condition
checkUnlockAvailability() checkUnlockAvailability()
@@ -153,12 +170,8 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
private fun changeMode(deviceUnlockMode: DeviceUnlockMode) { private fun changeMode(deviceUnlockMode: DeviceUnlockMode) {
this.deviceUnlockMode = deviceUnlockMode this.deviceUnlockMode = deviceUnlockMode
when (deviceUnlockMode) { when (deviceUnlockMode) {
DeviceUnlockMode.STORE_CREDENTIAL -> { DeviceUnlockMode.STORE_CREDENTIAL -> initEncryptData()
initEncryptData() DeviceUnlockMode.EXTRACT_CREDENTIAL -> initDecryptData()
}
DeviceUnlockMode.EXTRACT_CREDENTIAL -> {
initDecryptData()
}
else -> {} else -> {}
} }
_uiState.update { currentState -> _uiState.update { currentState ->
@@ -246,7 +259,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
credential: ByteArray, credential: ByteArray,
cipher: Cipher? cipher: Cipher?
) { ) {
try { cancelAndLaunchCipherJob {
deviceUnlockManager?.encryptData( deviceUnlockManager?.encryptData(
value = credential, value = credential,
cipher = cipher, cipher = cipher,
@@ -260,26 +273,23 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
this.specParameters = ivSpec this.specParameters = ivSpec
} }
) )
} ?: setException(UnknownDatabaseLocationException()) } ?: throw UnknownDatabaseLocationException()
} }
) )
} catch (e: Exception) { }
setException(e) // Reinit credential storage request
} finally { _uiState.update { currentState ->
// Reinit credential storage request currentState.copy(
_uiState.update { currentState -> credentialRequiredCipher = null
currentState.copy( )
credentialRequiredCipher = null
)
}
} }
} }
fun decryptCredential(cipher: Cipher?) { fun decryptCredential(cipher: Cipher?) {
// retrieve the encrypted value from preferences // retrieve the encrypted value from preferences
databaseUri?.let { databaseUri -> cancelAndLaunchCipherJob {
cipherDatabase?.encryptedValue?.let { encryptedCredential -> databaseUri?.let { databaseUri ->
try { cipherDatabase?.encryptedValue?.let { encryptedCredential ->
deviceUnlockManager?.decryptData( deviceUnlockManager?.decryptData(
encryptedValue = encryptedCredential, encryptedValue = encryptedCredential,
cipher = cipher, cipher = cipher,
@@ -295,12 +305,10 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
cipherDatabaseAction.resetCipherParameters(databaseUri) cipherDatabaseAction.resetCipherParameters(databaseUri)
} }
) )
} catch (e: Exception) { } ?: deleteEncryptedDatabaseKey()
setException(e) } ?: run {
} throw UnknownDatabaseLocationException()
} ?: deleteEncryptedDatabaseKey() }
} ?: run {
setException(UnknownDatabaseLocationException())
} }
} }
@@ -368,7 +376,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
} }
} }
fun setException(value: Exception?) { fun setException(value: Throwable?) {
_uiState.update { currentState -> _uiState.update { currentState ->
currentState.copy( currentState.copy(
exception = value exception = value
@@ -385,29 +393,25 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
} }
private fun initEncryptData() { private fun initEncryptData() {
try { cancelAndLaunchCipherJob {
deviceUnlockManager = DeviceUnlockManager(getApplication()) deviceUnlockManager = DeviceUnlockManager(getApplication())
deviceUnlockManager?.initEncryptData { cryptoPrompt -> deviceUnlockManager?.initEncryptData { cryptoPrompt ->
onPromptRequested(cryptoPrompt) onPromptRequested(cryptoPrompt)
} ?: setException(Exception("Device unlock manager not initialized")) } ?: throw Exception("Device unlock manager not initialized")
} catch (e: Exception) {
setException(e)
} }
} }
private fun initDecryptData() { private fun initDecryptData() {
try { cancelAndLaunchCipherJob {
cipherDatabase?.let { cipherDb -> cipherDatabase?.let { cipherDb ->
deviceUnlockManager = DeviceUnlockManager(getApplication()) deviceUnlockManager = DeviceUnlockManager(getApplication())
deviceUnlockManager?.initDecryptData(cipherDb.specParameters) { cryptoPrompt -> deviceUnlockManager?.initDecryptData(cipherDb.specParameters) { cryptoPrompt ->
onPromptRequested( onPromptRequested(
cryptoPrompt, cryptoPrompt,
autoOpen = isAutoOpenBiometricPromptAllowed autoOpen = isAutoOpenBiometricPromptAllowed
) )
} ?: setException(Exception("Device unlock manager not initialized")) } ?: throw Exception("Device unlock manager not initialized")
} ?: setException(Exception("Cipher database not initialized")) } ?: throw Exception("Cipher database not initialized")
} catch (e: Exception) {
setException(e)
} }
} }
@@ -475,5 +479,5 @@ data class DeviceUnlockState(
val cipherDecryptDatabase: CipherDecryptDatabase? = null, val cipherDecryptDatabase: CipherDecryptDatabase? = null,
val cryptoPromptState: DeviceUnlockPromptMode = DeviceUnlockPromptMode.IDLE_CLOSE, val cryptoPromptState: DeviceUnlockPromptMode = DeviceUnlockPromptMode.IDLE_CLOSE,
val autoOpenPrompt: Boolean = false, val autoOpenPrompt: Boolean = false,
val exception: Exception? = null val exception: Throwable? = null
) )