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