fix: cipher call #2105

This commit is contained in:
J-Jamet
2025-08-13 11:03:50 +02:00
parent c8c232639f
commit df3bd7e0a1
3 changed files with 79 additions and 99 deletions

View File

@@ -235,7 +235,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
mDeviceUnlockViewModel.uiState.collect { uiState ->
// New value received
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
uiState.cipherCredentialRequired?.let { cipher ->
uiState.credentialRequiredCipher?.let { cipher ->
mDeviceUnlockViewModel.encryptCredential(
credential = getCredentialForEncryption(),
cipher = cipher

View File

@@ -32,6 +32,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
@@ -52,7 +53,6 @@ import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.concurrent.Executors
import javax.crypto.Cipher
@RequiresApi(Build.VERSION_CODES.M)
class DeviceUnlockFragment: Fragment() {
@@ -66,43 +66,19 @@ class DeviceUnlockFragment: Fragment() {
// Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false
private var mDeviceCredentialEncryptionResultLauncher = registerForActivityResult(
private var mDeviceCredentialResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
// TODO onEncryptionPromptSucceeded()
mDeviceUnlockViewModel.onAuthenticationSucceeded(result)
} else {
setAuthenticationFailed()
}
}
private var mDeviceCredentialDecryptionResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
// TODO onDecryptionPromptSucceeded()
} else {
setAuthenticationFailed()
}
}
private var encryptionAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
private var biometricAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
onEncryptionPromptSucceeded(result.cryptoObject?.cipher)
}
override fun onAuthenticationFailed() {
setAuthenticationFailed()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
setAuthenticationError(errorCode, errString)
}
}
private var decryptionAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
onDecryptionPromptSucceeded(result.cryptoObject?.cipher)
mDeviceUnlockViewModel.onAuthenticationSucceeded(result)
}
override fun onAuthenticationFailed() {
@@ -143,6 +119,13 @@ class DeviceUnlockFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Init device unlock prompt
mBiometricPrompt = BiometricPrompt(
this@DeviceUnlockFragment,
Executors.newSingleThreadExecutor(),
biometricAuthenticationCallback
)
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
viewLifecycleOwner.lifecycleScope.launch {
@@ -168,7 +151,7 @@ class DeviceUnlockFragment: Fragment() {
mDeviceUnlockViewModel.checkUnlockAvailability()
}
fun closeBiometricPrompt() {
fun destroyBiometricPrompt() {
mBiometricPrompt?.cancelAuthentication()
mBiometricPrompt = null
}
@@ -199,18 +182,6 @@ class DeviceUnlockFragment: Fragment() {
state: DeviceUnlockPromptMode
) {
cryptoPrompt?.let {
// Init advanced unlock prompt
mBiometricPrompt = BiometricPrompt(
this@DeviceUnlockFragment,
Executors.newSingleThreadExecutor(),
when (cryptoPrompt.type) {
DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->
encryptionAuthenticationCallback
DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION ->
decryptionAuthenticationCallback
}
)
when (state) {
DeviceUnlockPromptMode.IDLE -> {}
DeviceUnlockPromptMode.SHOW -> {
@@ -218,7 +189,7 @@ class DeviceUnlockFragment: Fragment() {
mDeviceUnlockViewModel.promptShown()
}
DeviceUnlockPromptMode.CLOSE -> {
closeBiometricPrompt()
destroyBiometricPrompt()
mDeviceUnlockViewModel.biometricPromptClosed()
}
}
@@ -234,35 +205,27 @@ class DeviceUnlockFragment: Fragment() {
} ?: ""
if (cryptoPrompt.isBiometricOperation) {
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
setTitle(promptTitle)
if (promptDescription.isNotEmpty())
setDescription(promptDescription)
setConfirmationRequired(false)
if (isDeviceCredentialBiometricOperation(context)) {
setAllowedAuthenticators(DEVICE_CREDENTIAL)
} else {
setNegativeButtonText(getString(android.R.string.cancel))
}
}.build()
mBiometricPrompt?.authenticate(
promptInfoExtractCredential,
BiometricPrompt.PromptInfo.Builder().apply {
setTitle(promptTitle)
if (promptDescription.isNotEmpty())
setDescription(promptDescription)
setConfirmationRequired(false)
if (isDeviceCredentialBiometricOperation(context)) {
setAllowedAuthenticators(DEVICE_CREDENTIAL)
} else {
setNegativeButtonText(getString(android.R.string.cancel))
}
}.build(),
BiometricPrompt.CryptoObject(cryptoPrompt.cipher))
}
else if (cryptoPrompt.isDeviceCredentialOperation) {
} else if (cryptoPrompt.isDeviceCredentialOperation) {
context?.let { context ->
val keyGuardManager = ContextCompat.getSystemService(
context,
KeyguardManager::class.java
)
@Suppress("DEPRECATION")
when (cryptoPrompt.type) {
DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->
mDeviceCredentialEncryptionResultLauncher
DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION ->
mDeviceCredentialDecryptionResultLauncher
}.launch(
keyGuardManager?.createConfirmDeviceCredentialIntent(
mDeviceCredentialResultLauncher?.launch(
ContextCompat.getSystemService(
context,
KeyguardManager::class.java
)?.createConfirmDeviceCredentialIntent(
promptTitle,
promptDescription
)
@@ -271,7 +234,7 @@ class DeviceUnlockFragment: Fragment() {
}
} catch (e: Exception) {
Log.e(TAG, "Unable to open prompt", e)
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
mDeviceUnlockViewModel.setException(e)
}
}
}
@@ -390,19 +353,8 @@ class DeviceUnlockFragment: Fragment() {
}
}
private fun onDecryptionPromptSucceeded(cipher: Cipher?) {
mDeviceUnlockViewModel.decryptCredential(cipher)
mBiometricPrompt = null
}
private fun onEncryptionPromptSucceeded(cipher: Cipher?) {
mDeviceUnlockViewModel.retrieveCredentialForEncryption(cipher)
mBiometricPrompt = null
}
private fun setAuthenticationError(errorCode: Int, errString: CharSequence) {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
mBiometricPrompt = null
when (errorCode) {
BiometricPrompt.ERROR_CANCELED,
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
@@ -410,17 +362,14 @@ class DeviceUnlockFragment: Fragment() {
// Ignore negative button
}
else ->
mDeviceUnlockViewModel.setException(
SecurityException(errString.toString())
)
mDeviceUnlockViewModel.setException(SecurityException(errString.toString()))
}
}
private fun setAuthenticationFailed() {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
mBiometricPrompt = null
mDeviceUnlockViewModel.setException(SecurityException(
getString(R.string.advanced_unlock_not_recognized))
mDeviceUnlockViewModel.setException(
SecurityException(getString(R.string.advanced_unlock_not_recognized))
)
}

View File

@@ -3,11 +3,14 @@ package com.kunzisoft.keepass.viewmodels
import android.app.Application
import android.net.Uri
import android.os.Build
import androidx.activity.result.ActivityResult
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.lifecycle.AndroidViewModel
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.biometric.DeviceUnlockCryptoPrompt
import com.kunzisoft.keepass.biometric.DeviceUnlockCryptoPromptType
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
import com.kunzisoft.keepass.biometric.DeviceUnlockMode
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
@@ -140,7 +143,6 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
fun disconnect() {
this.databaseUri = null
closeBiometricPrompt()
cipherDatabaseListener?.let {
cipherDatabaseAction.unregisterDatabaseListener(it)
}
@@ -159,10 +161,38 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
}
}
fun retrieveCredentialForEncryption(cipher: Cipher?) {
@RequiresApi(Build.VERSION_CODES.M)
fun onAuthenticationSucceeded(
activityResult: ActivityResult
) {
uiState.value.cryptoPrompt?.let { prompt ->
when (prompt.type) {
DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->
retrieveCredentialForEncryption( prompt.cipher)
DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION ->
decryptCredential( prompt.cipher)
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
uiState.value.cryptoPrompt?.type?.let { type ->
when (type) {
DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->
retrieveCredentialForEncryption(result.cryptoObject?.cipher)
DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION ->
decryptCredential(result.cryptoObject?.cipher)
}
}
}
private fun retrieveCredentialForEncryption(cipher: Cipher?) {
_uiState.update { currentState ->
currentState.copy(
cipherCredentialRequired = cipher
credentialRequiredCipher = cipher
)
}
}
@@ -191,11 +221,13 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
)
} catch (e: Exception) {
setException(e)
}
_uiState.update { currentState ->
currentState.copy(
cipherCredentialRequired = null
)
} finally {
// Reinit credential storage request
_uiState.update { currentState ->
currentState.copy(
credentialRequiredCipher = null
)
}
}
}
@@ -276,8 +308,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
if (allowAutoOpenBiometricPrompt
&& PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(getApplication())
) {
if (uiState.value.cryptoPrompt != null)
showPrompt()
showPrompt()
}
}
@@ -382,7 +413,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat
_uiState.update { currentState ->
currentState.copy(
cryptoPrompt = null,
cryptoPromptState = DeviceUnlockPromptMode.SHOW
cryptoPromptState = DeviceUnlockPromptMode.IDLE
)
}
}
@@ -409,7 +440,7 @@ enum class DeviceUnlockPromptMode {
data class DeviceUnlockState(
val deviceUnlockMode: DeviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE,
val allowAdvancedUnlockMenu: Boolean = false,
val cipherCredentialRequired: Cipher? = null,
val credentialRequiredCipher: Cipher? = null,
val cipherEncryptDatabase: CipherEncryptDatabase? = null,
val cipherDecryptDatabase: CipherDecryptDatabase? = null,
val cryptoPrompt: DeviceUnlockCryptoPrompt? = null,