From 6c37f7b12c445fa0e720b02ffb9ca06299e16040 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 3 Aug 2025 18:17:56 +0200 Subject: [PATCH 01/26] fix: Update to 4.1.4 --- CHANGELOG | 2 ++ app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/136.txt | 1 + fastlane/metadata/android/fr-FR/changelogs/136.txt | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/136.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/136.txt diff --git a/CHANGELOG b/CHANGELOG index 933856781..c5292644b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +KeePassDX(4.1.4) + KeePassDX(4.1.3) * Fix Autofill Registration #2089 * Fix Biometric errors #2081 diff --git a/app/build.gradle b/app/build.gradle index 9e1c0c0d6..d78671178 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 15 targetSdkVersion 34 - versionCode = 135 - versionName = "4.1.3" + versionCode = 136 + versionName = "4.1.4" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" diff --git a/fastlane/metadata/android/en-US/changelogs/136.txt b/fastlane/metadata/android/en-US/changelogs/136.txt new file mode 100644 index 000000000..42780ecb1 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/136.txt @@ -0,0 +1 @@ + * \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/136.txt b/fastlane/metadata/android/fr-FR/changelogs/136.txt new file mode 100644 index 000000000..42780ecb1 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/136.txt @@ -0,0 +1 @@ + * \ No newline at end of file From 7be554a3786832e8396937d6053894cf814f05c1 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 10 Aug 2025 12:24:23 +0200 Subject: [PATCH 02/26] fix: unlock manager #2098 #2101 --- CHANGELOG | 1 + .../activities/MainCredentialActivity.kt | 48 +- .../app/database/CipherDatabaseAction.kt | 12 +- .../biometric/AdvancedUnlockCryptoPrompt.kt | 10 - .../biometric/AdvancedUnlockFragment.kt | 667 ------------------ .../biometric/DeviceUnlockCryptoPrompt.kt | 17 + .../keepass/biometric/DeviceUnlockFragment.kt | 510 +++++++++++++ ...nlockManager.kt => DeviceUnlockManager.kt} | 386 ++++------ .../keepass/biometric/DeviceUnlockMode.kt | 11 + .../settings/NestedAppSettingsFragment.kt | 8 +- .../keepass/settings/PreferencesUtil.kt | 4 +- ...dUnlockInfoView.kt => DeviceUnlockView.kt} | 12 +- .../viewmodels/AdvancedUnlockViewModel.kt | 152 ---- .../viewmodels/DeviceUnlockViewModel.kt | 388 ++++++++++ .../fragment_advanced_unlock.xml | 2 +- app/src/main/res/values-en-rGB/strings.xml | 4 +- .../metadata/android/en-US/changelogs/136.txt | 2 +- .../metadata/android/fr-FR/changelogs/136.txt | 2 +- 18 files changed, 1128 insertions(+), 1108 deletions(-) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockCryptoPrompt.kt delete mode 100644 app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockCryptoPrompt.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt rename app/src/main/java/com/kunzisoft/keepass/biometric/{AdvancedUnlockManager.kt => DeviceUnlockManager.kt} (51%) create mode 100644 app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockMode.kt rename app/src/main/java/com/kunzisoft/keepass/view/{AdvancedUnlockInfoView.kt => DeviceUnlockView.kt} (84%) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/viewmodels/AdvancedUnlockViewModel.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt rename app/src/main/res/{layout => layout-v23}/fragment_advanced_unlock.xml (76%) diff --git a/CHANGELOG b/CHANGELOG index c5292644b..b6016826f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ KeePassDX(4.1.4) + * Fix UnlockManager #2098 #2101 KeePassDX(4.1.3) * Fix Autofill Registration #2089 diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index 83cdf54f6..5afb94256 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -55,8 +55,8 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper -import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment -import com.kunzisoft.keepass.biometric.AdvancedUnlockManager +import com.kunzisoft.keepass.biometric.DeviceUnlockFragment +import com.kunzisoft.keepass.biometric.DeviceUnlockManager import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException @@ -81,7 +81,7 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.view.MainCredentialView import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.showActionErrorIfNeeded -import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel +import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import kotlinx.coroutines.launch import java.io.FileNotFoundException @@ -98,10 +98,10 @@ class MainCredentialActivity : DatabaseModeActivity() { private var confirmButtonView: Button? = null private var infoContainerView: ViewGroup? = null private lateinit var coordinatorLayout: CoordinatorLayout - private var advancedUnlockFragment: AdvancedUnlockFragment? = null + private var deviceUnlockFragment: DeviceUnlockFragment? = null private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels() - private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels() + private val mDeviceUnlockViewModel: DeviceUnlockViewModel by viewModels() private val mPasswordActivityEducation = PasswordActivityEducation(this) @@ -170,8 +170,9 @@ class MainCredentialActivity : DatabaseModeActivity() { // Listen password checkbox to init advanced unlock and confirmation button mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified -> - mAdvancedUnlockViewModel.checkUnlockAvailability( - conditionToStoreCredentialVerified = verified + mDeviceUnlockViewModel.checkConditionToStoreCredential( + condition = verified, + databaseFileUri = mDatabaseFileUri ) // TODO Async by ViewModel enableConfirmationButton() @@ -226,20 +227,22 @@ class MainCredentialActivity : DatabaseModeActivity() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - mAdvancedUnlockViewModel.uiState.collect { uiState -> + mDeviceUnlockViewModel.uiState.collect { uiState -> // New value received - if (uiState.isCredentialRequired) { - mAdvancedUnlockViewModel.provideCredentialForEncryption( - getCredentialForEncryption() - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (uiState.isCredentialRequired) { + mDeviceUnlockViewModel.encryptCredential( + getCredentialForEncryption() + ) + } } uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase -> onCredentialEncrypted(cipherEncryptDatabase) - mAdvancedUnlockViewModel.consumeCredentialEncrypted() + mDeviceUnlockViewModel.consumeCredentialEncrypted() } uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase -> onCredentialDecrypted(cipherDecryptDatabase) - mAdvancedUnlockViewModel.consumeCredentialDecrypted() + mDeviceUnlockViewModel.consumeCredentialDecrypted() } } } @@ -250,11 +253,12 @@ class MainCredentialActivity : DatabaseModeActivity() { super.onResume() // Init Biometric elements only if allowed - if (PreferencesUtil.isAdvancedUnlockEnable(this)) { - advancedUnlockFragment = supportFragmentManager - .findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment? - if (advancedUnlockFragment == null) { - advancedUnlockFragment = AdvancedUnlockFragment().also { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && PreferencesUtil.isAdvancedUnlockEnable(this)) { + deviceUnlockFragment = supportFragmentManager + .findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? DeviceUnlockFragment? + if (deviceUnlockFragment == null) { + deviceUnlockFragment = DeviceUnlockFragment().also { supportFragmentManager.commit { replace( R.id.fragment_advanced_unlock_container_view, @@ -276,7 +280,7 @@ class MainCredentialActivity : DatabaseModeActivity() { // Don't allow auto open prompt if lock become when UI visible if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) { - mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false + mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false } mDatabaseFileUri?.let { databaseFileUri -> @@ -494,7 +498,7 @@ class MainCredentialActivity : DatabaseModeActivity() { loadDatabase() } else { // Init Biometric elements - mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri) + mDeviceUnlockViewModel.databaseFileLoaded(databaseFileUri) } enableConfirmationButton() @@ -654,7 +658,7 @@ class MainCredentialActivity : DatabaseModeActivity() { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !readOnlyEducationPerformed) { - val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(this) + val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(this) if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) && advancedUnlockButton != null) { diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt index b1158fe3e..fee325499 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt @@ -177,14 +177,18 @@ class CipherDatabaseAction(context: Context) { } } - fun containsCipherDatabase(databaseUri: Uri, + fun containsCipherDatabase(databaseUri: Uri?, contains: (Boolean) -> Unit) { - getCipherDatabase(databaseUri) { - contains.invoke(it != null) + if (databaseUri == null) { + contains.invoke(false) + } else { + getCipherDatabase(databaseUri) { + contains.invoke(it != null) + } } } - fun resetCipherParameters(databaseUri: Uri) { + fun resetCipherParameters(databaseUri: Uri?) { containsCipherDatabase(databaseUri) { contains -> if (contains) { mBinder?.resetTimer() diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockCryptoPrompt.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockCryptoPrompt.kt deleted file mode 100644 index 26e9baff4..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockCryptoPrompt.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.kunzisoft.keepass.biometric - -import androidx.annotation.StringRes -import javax.crypto.Cipher - -data class AdvancedUnlockCryptoPrompt(var cipher: Cipher, - @StringRes var promptTitleId: Int, - @StringRes var promptDescriptionId: Int? = null, - var isDeviceCredentialOperation: Boolean, - var isBiometricOperation: Boolean) \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt deleted file mode 100644 index 7b35c67f7..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt +++ /dev/null @@ -1,667 +0,0 @@ -/* - * Copyright 2020 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePassDX. - * - * KeePassDX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePassDX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePassDX. If not, see . - * - */ -package com.kunzisoft.keepass.biometric - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.Settings -import android.util.Log -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.RequiresApi -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricPrompt -import androidx.core.view.MenuProvider -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.app.database.CipherDatabaseAction -import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException -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 com.kunzisoft.keepass.view.AdvancedUnlockInfoView -import com.kunzisoft.keepass.view.hideByFading -import com.kunzisoft.keepass.view.showByFading -import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCallback { - - private var mAdvancedUnlockEnabled = false - private var mAutoOpenPromptEnabled = false - - private var advancedUnlockManager: AdvancedUnlockManager? = null - private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE - private var mAdvancedUnlockInfoView: AdvancedUnlockInfoView? = null - - var databaseFileUri: Uri? = null - private set - - // TODO Retrieve credential storage from app database - var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT - - // Variable to check if the prompt can be open (if the right activity is currently shown) - // checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization - private var allowOpenBiometricPrompt = false - - private lateinit var cipherDatabaseAction : CipherDatabaseAction - - private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null - - private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels() - - // Only to fix multiple fingerprint menu #332 - private var mAllowAdvancedUnlockMenu = false - private var mAddBiometricMenuInProgress = false - - // Only keep connection when we request a device credential activity - private var keepConnection = false - - private var isConditionToStoreCredentialVerified = false - - private var mDeviceCredentialResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false - // To wait resume - if (keepConnection) { - mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = - result.resultCode == Activity.RESULT_OK - } - keepConnection = false - } - - private val menuProvider: MenuProvider = object: MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // biometric menu - if (mAllowAdvancedUnlockMenu) - menuInflater.inflate(R.menu.advanced_unlock, menu) - } - } - - override fun onMenuItemSelected(menuItem: MenuItem): Boolean { - when (menuItem.itemId) { - R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - deleteEncryptedDatabaseKey() - } - } - return false - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - super.onCreateView(inflater, container, savedInstanceState) - - val rootView = inflater.inflate(R.layout.fragment_advanced_unlock, container, false) - - mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view) - - return rootView - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - activity?.addMenuProvider(menuProvider, viewLifecycleOwner) - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - mAdvancedUnlockViewModel.uiState.collect { uiState -> - // Database loaded - uiState.databaseFileLoaded?.let { databaseLoaded -> - onDatabaseLoaded(databaseLoaded) - mAdvancedUnlockViewModel.consumeDatabaseFileLoaded() - } - // New credential value received - uiState.credential?.let { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - advancedUnlockManager?.encryptData(uiState.credential) - } - mAdvancedUnlockViewModel.consumeCredentialForEncryption() - } - // Condition to store credential verified - isConditionToStoreCredentialVerified = uiState.isConditionToStoreCredentialVerified - // Check unlock availability - if (uiState.onUnlockAvailabilityCheckRequested) { - checkUnlockAvailability() - mAdvancedUnlockViewModel.consumeCheckUnlockAvailability() - } - } - } - } - } - - override fun onResume() { - super.onResume() - context?.let { - mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it) - mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it) - } - keepConnection = false - } - - private fun onDatabaseLoaded(databaseUri: Uri?) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // To get device credential unlock result, only if same database uri - if (databaseUri != null - && mAdvancedUnlockEnabled) { - mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded?.let { authSucceeded -> - if (databaseUri == databaseFileUri) { - if (authSucceeded) { - advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded() - } else { - advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed() - } - } else { - disconnect() - } - } ?: run { - if (databaseUri != databaseFileUri) { - connect(databaseUri) - } - } - } else { - disconnect() - } - mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null - } - } - - /** - * Check unlock availability and change the current mode depending of device's state - */ - private fun checkUnlockAvailability() { - context?.let { context -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - allowOpenBiometricPrompt = true - if (PreferencesUtil.isBiometricUnlockEnable(context)) { - // biometric not supported (by API level or hardware) so keep option hidden - // or manually disable - val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context) - if (!PreferencesUtil.isAdvancedUnlockEnable(context) - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { - toggleMode(Mode.BIOMETRIC_UNAVAILABLE) - } else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) { - toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED) - } else { - // biometric is available but not configured, show icon but in disabled state with some information - if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { - toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED) - } else { - selectMode() - } - } - } else if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) { - if (AdvancedUnlockManager.isDeviceSecure(context)) { - selectMode() - } else { - toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED) - } - } - } - } - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun selectMode() { - // Check if fingerprint well init (be called the first time the fingerprint is configured - // and the activity still active) - if (advancedUnlockManager?.isKeyManagerInitialized != true) { - advancedUnlockManager = AdvancedUnlockManager { requireActivity() } - // callback for fingerprint findings - advancedUnlockManager?.advancedUnlockCallback = this - } - // Recheck to change the mode - if (advancedUnlockManager?.isKeyManagerInitialized != true) { - toggleMode(Mode.KEY_MANAGER_UNAVAILABLE) - } else { - if (isConditionToStoreCredentialVerified) { - // listen for encryption - toggleMode(Mode.STORE_CREDENTIAL) - } else { - databaseFileUri?.let { databaseUri -> - cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher -> - // biometric available but no stored password found yet for this DB so show info don't listen - toggleMode(if (containsCipher) { - // listen for decryption - Mode.EXTRACT_CREDENTIAL - } else { - if (isConditionToStoreCredentialVerified) { - // if condition OK, key manager in error - Mode.KEY_MANAGER_UNAVAILABLE - } else { - // wait for typing - Mode.WAIT_CREDENTIAL - } - }) - } - } - } - } - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun toggleMode(newBiometricMode: Mode) { - if (newBiometricMode != biometricMode) { - biometricMode = newBiometricMode - initAdvancedUnlockMode() - } - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun initNotAvailable() { - showViews(false) - - mAdvancedUnlockInfoView?.setIconViewClickListener(null) - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun openBiometricSetting() { - mAdvancedUnlockInfoView?.setIconViewClickListener { - try { - when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { - context?.startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL)) - } - Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> { - @Suppress("DEPRECATION") context - ?.startActivity(Intent(Settings.ACTION_FINGERPRINT_ENROLL)) - } - else -> { - context?.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS)) - } - } - } catch (e: Exception) { - // ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices... - context?.startActivity(Intent(Settings.ACTION_SETTINGS)) - } - } - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun initSecurityUpdateRequired() { - showViews(true) - setAdvancedUnlockedTitleView(R.string.biometric_security_update_required) - - openBiometricSetting() - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun initNotConfigured() { - showViews(true) - setAdvancedUnlockedTitleView(R.string.configure_biometric) - setAdvancedUnlockedMessageView("") - - openBiometricSetting() - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun initKeyManagerNotAvailable() { - showViews(true) - setAdvancedUnlockedTitleView(R.string.keystore_not_accessible) - - openBiometricSetting() - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun initWaitData() { - showViews(true) - setAdvancedUnlockedTitleView(R.string.unavailable) - setAdvancedUnlockedMessageView("") - - context?.let { context -> - mAdvancedUnlockInfoView?.setIconViewClickListener { - onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS, - context.getString(R.string.credential_before_click_advanced_unlock_button)) - } - } - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) { - lifecycleScope.launch(Dispatchers.Main) { - if (allowOpenBiometricPrompt) { - if (cryptoPrompt.isDeviceCredentialOperation) - keepConnection = true - try { - advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt, - mDeviceCredentialResultLauncher) - } catch (e: Exception) { - Log.e(TAG, "Unable to open advanced unlock prompt", e) - setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) - } - } - } - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun initEncryptData() { - showViews(true) - setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric) - setAdvancedUnlockedMessageView("") - - advancedUnlockManager?.initEncryptData { cryptoPrompt -> - // Set listener to open the biometric dialog and save credential - mAdvancedUnlockInfoView?.setIconViewClickListener { _ -> - openAdvancedUnlockPrompt(cryptoPrompt) - } - } ?: throw Exception("AdvancedUnlockManager not initialized") - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun initDecryptData() { - showViews(true) - setAdvancedUnlockedTitleView(R.string.unlock) - setAdvancedUnlockedMessageView("") - - advancedUnlockManager?.let { unlockHelper -> - databaseFileUri?.let { databaseUri -> - cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase -> - cipherDatabase?.let { - unlockHelper.initDecryptData(it.specParameters) { cryptoPrompt -> - - // Set listener to open the biometric dialog and check credential - mAdvancedUnlockInfoView?.setIconViewClickListener { _ -> - openAdvancedUnlockPrompt(cryptoPrompt) - } - - // Auto open the biometric prompt - if (mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt - && mAutoOpenPromptEnabled) { - mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false - openAdvancedUnlockPrompt(cryptoPrompt) - } - } - } ?: deleteEncryptedDatabaseKey() - } - } ?: throw UnknownDatabaseLocationException() - } ?: throw Exception("AdvancedUnlockManager not initialized") - } - - private fun initAdvancedUnlockMode() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - mAllowAdvancedUnlockMenu = false - try { - when (biometricMode) { - Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable() - Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired() - Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> initNotConfigured() - Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable() - Mode.WAIT_CREDENTIAL -> initWaitData() - Mode.STORE_CREDENTIAL -> initEncryptData() - Mode.EXTRACT_CREDENTIAL -> initDecryptData() - } - } catch (e: Exception) { - onGenericException(e) - } - invalidateBiometricMenu() - } - } - - private fun invalidateBiometricMenu() { - // Show fingerprint key deletion - if (!mAddBiometricMenuInProgress) { - mAddBiometricMenuInProgress = true - databaseFileUri?.let { databaseUri -> - cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher -> - mAllowAdvancedUnlockMenu = containsCipher - && (biometricMode != Mode.BIOMETRIC_UNAVAILABLE - && biometricMode != Mode.KEY_MANAGER_UNAVAILABLE) - mAddBiometricMenuInProgress = false - activity?.invalidateOptionsMenu() - } - } - } - } - - @RequiresApi(Build.VERSION_CODES.M) - fun connect(databaseUri: Uri) { - showViews(true) - this.databaseFileUri = databaseUri - cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener { - override fun onCipherDatabaseCleared() { - advancedUnlockManager?.closeBiometricPrompt() - checkUnlockAvailability() - } - } - cipherDatabaseAction.apply { - reloadPreferences() - cipherDatabaseListener?.let { - registerDatabaseListener(it) - } - } - checkUnlockAvailability() - } - - @RequiresApi(Build.VERSION_CODES.M) - fun disconnect(hideViews: Boolean = true, - closePrompt: Boolean = true) { - this.databaseFileUri = null - // Close the biometric prompt - allowOpenBiometricPrompt = false - if (closePrompt) - advancedUnlockManager?.closeBiometricPrompt() - cipherDatabaseListener?.let { - cipherDatabaseAction.unregisterDatabaseListener(it) - } - biometricMode = Mode.BIOMETRIC_UNAVAILABLE - if (hideViews) { - showViews(false) - } - } - - @RequiresApi(Build.VERSION_CODES.M) - fun deleteEncryptedDatabaseKey() { - mAllowAdvancedUnlockMenu = false - advancedUnlockManager?.closeBiometricPrompt() - databaseFileUri?.let { databaseUri -> - cipherDatabaseAction.deleteByDatabaseUri(databaseUri) { - checkUnlockAvailability() - } - } ?: checkUnlockAvailability() - } - - @RequiresApi(Build.VERSION_CODES.M) - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - lifecycleScope.launch(Dispatchers.Main) { - Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") - setAdvancedUnlockedMessageView(errString.toString()) - } - } - - @RequiresApi(Build.VERSION_CODES.M) - override fun onAuthenticationFailed() { - lifecycleScope.launch(Dispatchers.Main) { - Log.e(TAG, "Biometric authentication failed, biometric not recognized") - setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized) - } - } - - @RequiresApi(Build.VERSION_CODES.M) - override fun onAuthenticationSucceeded() { - lifecycleScope.launch(Dispatchers.Main) { - when (biometricMode) { - Mode.BIOMETRIC_UNAVAILABLE -> { - } - Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> { - } - Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> { - } - Mode.KEY_MANAGER_UNAVAILABLE -> { - } - Mode.WAIT_CREDENTIAL -> { - } - Mode.STORE_CREDENTIAL -> { - // newly store the entered password in encrypted way - mAdvancedUnlockViewModel.retrieveCredentialForEncryption() - } - Mode.EXTRACT_CREDENTIAL -> { - // retrieve the encrypted value from preferences - databaseFileUri?.let { databaseUri -> - cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase -> - cipherDatabase?.encryptedValue?.let { value -> - advancedUnlockManager?.decryptData(value) - } ?: deleteEncryptedDatabaseKey() - } - } ?: run { - onAuthenticationError(-1, getString(R.string.error_database_uri_null)) - } - } - } - } - } - - override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) { - databaseFileUri?.let { databaseUri -> - mAdvancedUnlockViewModel.onCredentialEncrypted( - CipherEncryptDatabase().apply { - this.databaseUri = databaseUri - this.credentialStorage = credentialDatabaseStorage - this.encryptedValue = encryptedValue - this.specParameters = ivSpec - } - ) - } - } - - override fun handleDecryptedResult(decryptedValue: ByteArray) { - // Load database directly with password retrieve - databaseFileUri?.let { databaseUri -> - mAdvancedUnlockViewModel.onCredentialDecrypted( - CipherDecryptDatabase().apply { - this.databaseUri = databaseUri - this.credentialStorage = credentialDatabaseStorage - this.decryptedValue = decryptedValue - } - ) - cipherDatabaseAction.resetCipherParameters(databaseUri) - } - } - - @RequiresApi(Build.VERSION_CODES.M) - override fun onUnrecoverableKeyException(e: Exception) { - setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) - } - - @RequiresApi(Build.VERSION_CODES.M) - override fun onInvalidKeyException(e: Exception) { - setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) - } - - @RequiresApi(Build.VERSION_CODES.M) - override fun onGenericException(e: Exception) { - val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: "" - setAdvancedUnlockedMessageView(errorMessage) - } - - private fun showViews(show: Boolean) { - lifecycleScope.launch(Dispatchers.Main) { - if (show) { - if (mAdvancedUnlockInfoView?.visibility != View.VISIBLE) - mAdvancedUnlockInfoView?.showByFading() - } - else { - if (mAdvancedUnlockInfoView?.visibility == View.VISIBLE) - mAdvancedUnlockInfoView?.hideByFading() - } - } - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun setAdvancedUnlockedTitleView(textId: Int) { - lifecycleScope.launch(Dispatchers.Main) { - mAdvancedUnlockInfoView?.setTitle(textId) - } - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun setAdvancedUnlockedMessageView(textId: Int) { - lifecycleScope.launch(Dispatchers.Main) { - mAdvancedUnlockInfoView?.setMessage(textId) - } - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun setAdvancedUnlockedMessageView(text: CharSequence) { - lifecycleScope.launch(Dispatchers.Main) { - mAdvancedUnlockInfoView?.setMessage(text) - } - } - - enum class Mode { - BIOMETRIC_UNAVAILABLE, - BIOMETRIC_SECURITY_UPDATE_REQUIRED, - DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED, - KEY_MANAGER_UNAVAILABLE, - WAIT_CREDENTIAL, - STORE_CREDENTIAL, - EXTRACT_CREDENTIAL - } - - override fun onPause() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (!keepConnection) { - // If close prompt, bug "user not authenticated in Android R" - disconnect(false) - advancedUnlockManager = null - } - } - super.onPause() - } - - override fun onDestroyView() { - mAdvancedUnlockInfoView = null - super.onDestroyView() - } - - override fun onDestroy() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - disconnect() - advancedUnlockManager = null - } - super.onDestroy() - } - - companion object { - private val TAG = AdvancedUnlockFragment::class.java.name - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockCryptoPrompt.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockCryptoPrompt.kt new file mode 100644 index 000000000..ca5be5bc7 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockCryptoPrompt.kt @@ -0,0 +1,17 @@ +package com.kunzisoft.keepass.biometric + +import androidx.annotation.StringRes +import javax.crypto.Cipher + +data class DeviceUnlockCryptoPrompt( + var type: DeviceUnlockCryptoPromptType, + var cipher: Cipher, + @StringRes var titleId: Int, + @StringRes var descriptionId: Int? = null, + var isDeviceCredentialOperation: Boolean, + var isBiometricOperation: Boolean +) + +enum class DeviceUnlockCryptoPromptType { + CREDENTIAL_ENCRYPTION, CREDENTIAL_DECRYPTION +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt new file mode 100644 index 000000000..98391e983 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -0,0 +1,510 @@ +/* + * Copyright 2020 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.biometric + +import android.app.Activity +import android.app.KeyguardManager +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.security.keystore.KeyPermanentlyInvalidatedException +import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.view.DeviceUnlockView +import com.kunzisoft.keepass.view.hideByFading +import com.kunzisoft.keepass.view.showByFading +import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.security.UnrecoverableKeyException +import java.util.concurrent.Executors + +@RequiresApi(Build.VERSION_CODES.M) +class DeviceUnlockFragment: Fragment() { + + private var mDeviceUnlockView: DeviceUnlockView? = null + + private val mDeviceUnlockViewModel: DeviceUnlockViewModel by activityViewModels() + + private var mBiometricPrompt: BiometricPrompt? = null + + // Only to fix multiple fingerprint menu #332 + private var mAllowAdvancedUnlockMenu = false + + // Only keep connection when we request a device credential activity + private var keepConnection = false + + private var storeCredentialButtonClickListener: View.OnClickListener? = null + private var extractCredentialButtonClickListener: View.OnClickListener? = null + + private var mDeviceCredentialResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false + // To wait resume + if (keepConnection) { + mDeviceUnlockViewModel.deviceCredentialAuthSucceeded = + result.resultCode == Activity.RESULT_OK + } + keepConnection = false + } + + private var storeAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + // newly store the entered password in encrypted way + mDeviceUnlockViewModel.retrieveCredentialForEncryption() + } + + override fun onAuthenticationFailed() { + setAuthenticationFailed() + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + setAuthenticationError(errorCode, errString) + } + } + + private var extractAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + mDeviceUnlockViewModel.decryptCredential() + } + + override fun onAuthenticationFailed() { + setAuthenticationFailed() + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + setAuthenticationError(errorCode, errString) + } + } + + private val menuProvider: MenuProvider = object: MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + // biometric menu + if (mAllowAdvancedUnlockMenu) + menuInflater.inflate(R.menu.advanced_unlock, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + R.id.menu_keystore_remove_key -> + deleteEncryptedDatabaseKey() + } + return false + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + + val rootView = inflater.inflate(R.layout.fragment_advanced_unlock, container, false) + + mDeviceUnlockView = rootView.findViewById(R.id.advanced_unlock_view) + + return rootView + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + activity?.addMenuProvider(menuProvider, viewLifecycleOwner) + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + mDeviceUnlockViewModel.uiState.collect { uiState -> + // Change mode + toggleDeviceCredentialMode(uiState.deviceUnlockMode) + // Prompt + uiState.cryptoPrompt?.let { prompt -> + mDeviceUnlockViewModel.promptShown() + when (prompt.type) { + DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION -> + manageEncryptionPrompt(prompt) + DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION -> + manageDecryptionPrompt(prompt) + } + } + if (uiState.closePromptRequested) { + closeBiometricPrompt() + mDeviceUnlockViewModel.biometricPromptClosed() + } + // Errors + setAdvancedUnlockedError(uiState.error) + // Advanced menu + mAllowAdvancedUnlockMenu = uiState.allowAdvancedUnlockMenu + activity?.invalidateOptionsMenu() + } + } + } + } + + override fun onResume() { + super.onResume() + keepConnection = false + } + + fun openAdvancedUnlockPrompt( + cryptoPrompt: DeviceUnlockCryptoPrompt, + authenticationCallback: BiometricPrompt.AuthenticationCallback + ) { + // Init advanced unlock prompt + mBiometricPrompt = BiometricPrompt( + this@DeviceUnlockFragment, + Executors.newSingleThreadExecutor(), + authenticationCallback + ) + + val promptTitle = getString(cryptoPrompt.titleId) + val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId -> + getString(descriptionId) + } ?: "" + + 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.CryptoObject(cryptoPrompt.cipher)) + } + else if (cryptoPrompt.isDeviceCredentialOperation) { + context?.let { context -> + val keyGuardManager = ContextCompat.getSystemService( + context, + KeyguardManager::class.java + ) + @Suppress("DEPRECATION") + mDeviceCredentialResultLauncher.launch( + keyGuardManager?.createConfirmDeviceCredentialIntent( + promptTitle, + promptDescription + ) + ) + } + } + } + + fun closeBiometricPrompt() { + mBiometricPrompt?.cancelAuthentication() + } + + private var currentCredentialMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE + private fun toggleDeviceCredentialMode(deviceUnlockMode: DeviceUnlockMode) { + if (currentCredentialMode == deviceUnlockMode) { + return + } + currentCredentialMode = deviceUnlockMode + try { + when (deviceUnlockMode) { + DeviceUnlockMode.BIOMETRIC_UNAVAILABLE -> setNotAvailableMode() + DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> setSecurityUpdateRequiredMode() + DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> setNotConfiguredMode() + DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE -> setKeyManagerNotAvailableMode() + DeviceUnlockMode.WAIT_CREDENTIAL -> setWaitCredentialMode() + DeviceUnlockMode.STORE_CREDENTIAL -> setStoreCredentialMode() + DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode() + } + } catch (e: Exception) { + showGenericException(e) + } + } + + private fun manageEncryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { + if (cryptoPrompt.isDeviceCredentialOperation) { + keepConnection = true + } + storeCredentialButtonClickListener = View.OnClickListener { _ -> + try { + openAdvancedUnlockPrompt( + cryptoPrompt, + storeAuthenticationCallback + ) + } catch (e: Exception) { + Log.e(TAG, "Unable to open encryption prompt", e) + storeCredentialButtonClickListener = null + setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) + } + } + } + + private fun openExtractPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { + try { + openAdvancedUnlockPrompt( + cryptoPrompt, + extractAuthenticationCallback + ) + } catch (e: Exception) { + Log.e(TAG, "Unable to open decryption prompt", e) + extractCredentialButtonClickListener = null + setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) + } + } + + private fun manageDecryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { + // Set listener to open the biometric dialog and check credential + extractCredentialButtonClickListener = View.OnClickListener { _ -> + openExtractPrompt(cryptoPrompt) + } + // Auto open the biometric prompt + if (mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt + && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())) { + mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false + openExtractPrompt(cryptoPrompt) + } + } + + + fun showGenericException(e: Exception) { + lifecycleScope.launch(Dispatchers.Main) { + val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: "" + setAdvancedUnlockedMessageView(errorMessage) + } + } + + private fun setNotAvailableMode() { + lifecycleScope.launch(Dispatchers.Main) { + showViews(false) + mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener(null) + } + } + + private fun openBiometricSetting() { + mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { + try { + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { + context?.startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL)) + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> { + @Suppress("DEPRECATION") context + ?.startActivity(Intent(Settings.ACTION_FINGERPRINT_ENROLL)) + } + else -> { + context?.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS)) + } + } + } catch (e: Exception) { + // ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices... + context?.startActivity(Intent(Settings.ACTION_SETTINGS)) + } + } + } + + private fun setSecurityUpdateRequiredMode() { + lifecycleScope.launch(Dispatchers.Main) { + showViews(true) + setAdvancedUnlockedTitleView(R.string.biometric_security_update_required) + openBiometricSetting() + } + } + + private fun setNotConfiguredMode() { + lifecycleScope.launch(Dispatchers.Main) { + showViews(true) + setAdvancedUnlockedTitleView(R.string.configure_biometric) + setAdvancedUnlockedMessageView("") + openBiometricSetting() + } + } + + private fun setKeyManagerNotAvailableMode() { + lifecycleScope.launch(Dispatchers.Main) { + showViews(true) + setAdvancedUnlockedTitleView(R.string.keystore_not_accessible) + openBiometricSetting() + } + } + + private fun setWaitCredentialMode() { + lifecycleScope.launch(Dispatchers.Main) { + showViews(true) + setAdvancedUnlockedTitleView(R.string.unavailable) + setAdvancedUnlockedMessageView("") + context?.let { context -> + mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { + showError( + BiometricPrompt.ERROR_UNABLE_TO_PROCESS, + context.getString(R.string.credential_before_click_advanced_unlock_button) + ) + } + } + } + } + + private fun showError(errorCode: Int, errString: CharSequence) { + lifecycleScope.launch(Dispatchers.Main) { + Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") + setAdvancedUnlockedMessageView(errString.toString()) + } + } + + private fun setStoreCredentialMode() { + lifecycleScope.launch(Dispatchers.Main) { + showViews(true) + setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric) + setAdvancedUnlockedMessageView("") + context?.let { context -> + mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { view -> + storeCredentialButtonClickListener?.onClick(view) ?: run { + showError( + BiometricPrompt.ERROR_HW_UNAVAILABLE, + context.getString(R.string.keystore_not_accessible) + ) + } + storeCredentialButtonClickListener = null + } + } + } + } + + private fun setExtractCredentialMode() { + lifecycleScope.launch(Dispatchers.Main) { + showViews(true) + setAdvancedUnlockedTitleView(R.string.unlock) + setAdvancedUnlockedMessageView("") + context?.let { context -> + mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { view -> + extractCredentialButtonClickListener?.onClick(view) ?: run { + showError( + BiometricPrompt.ERROR_HW_UNAVAILABLE, + context.getString(R.string.keystore_not_accessible) + ) + } + extractCredentialButtonClickListener = null + } + } + } + } + + fun deleteEncryptedDatabaseKey() { + mDeviceUnlockViewModel.deleteEncryptedDatabaseKey() + } + + private fun showViews(show: Boolean) { + lifecycleScope.launch(Dispatchers.Main) { + if (show) { + if (mDeviceUnlockView?.visibility != View.VISIBLE) + mDeviceUnlockView?.showByFading() + } + else { + if (mDeviceUnlockView?.visibility == View.VISIBLE) + mDeviceUnlockView?.hideByFading() + } + } + } + + private fun setAdvancedUnlockedTitleView(textId: Int) { + lifecycleScope.launch(Dispatchers.Main) { + mDeviceUnlockView?.setTitle(textId) + } + } + + private fun setAdvancedUnlockedMessageView(textId: Int) { + lifecycleScope.launch(Dispatchers.Main) { + mDeviceUnlockView?.setMessage(textId) + } + } + + private fun setAdvancedUnlockedMessageView(text: CharSequence?) { + lifecycleScope.launch(Dispatchers.Main) { + mDeviceUnlockView?.setMessage(text) + } + } + + private fun setAdvancedUnlockedError(exception: Exception?) { + when (exception) { + is UnrecoverableKeyException -> { + setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) + } + is KeyPermanentlyInvalidatedException -> { + setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) + } + else -> { + setAdvancedUnlockedMessageView( + exception?.cause?.localizedMessage + ?: exception?.localizedMessage + ?: "") + } + } + } + + private fun setAuthenticationError(errorCode: Int, errString: CharSequence) { + lifecycleScope.launch(Dispatchers.Main) { + Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") + setAdvancedUnlockedMessageView(errString.toString()) + } + } + + private fun setAuthenticationFailed() { + lifecycleScope.launch(Dispatchers.Main) { + Log.e(TAG, "Biometric authentication failed, biometric not recognized") + setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized) + } + } + + override fun onPause() { + if (!keepConnection) { + // If close prompt, bug "user not authenticated in Android R" + mDeviceUnlockViewModel.disconnect() + } + super.onPause() + } + + override fun onDestroyView() { + mDeviceUnlockView = null + super.onDestroyView() + } + + override fun onDestroy() { + mDeviceUnlockViewModel.disconnect() + super.onDestroy() + } + + companion object { + private val TAG = DeviceUnlockFragment::class.java.name + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt similarity index 51% rename from app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt rename to app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt index 764396dde..20049bc2c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt @@ -21,7 +21,6 @@ package com.kunzisoft.keepass.biometric import android.app.KeyguardManager import android.content.Context -import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.security.keystore.KeyGenParameterSpec @@ -29,110 +28,70 @@ import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyProperties import android.util.Log import android.widget.Toast -import androidx.activity.result.ActivityResultLauncher import androidx.annotation.RequiresApi import androidx.biometric.BiometricManager -import androidx.biometric.BiometricManager.Authenticators.* -import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentActivity import com.kunzisoft.keepass.R import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.settings.PreferencesUtil import java.security.KeyStore import java.security.UnrecoverableKeyException -import java.util.concurrent.Executors -import javax.crypto.BadPaddingException import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.spec.IvParameterSpec @RequiresApi(api = Build.VERSION_CODES.M) -class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) { +class DeviceUnlockManager(private var appContext: Context) { private var keyStore: KeyStore? = null private var keyGenerator: KeyGenerator? = null private var cipher: Cipher? = null - private var biometricPrompt: BiometricPrompt? = null - private var authenticationCallback = object: BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - advancedUnlockCallback?.onAuthenticationSucceeded() - } - - override fun onAuthenticationFailed() { - advancedUnlockCallback?.onAuthenticationFailed() - } - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - advancedUnlockCallback?.onAuthenticationError(errorCode, errString) - } - } - - var advancedUnlockCallback: AdvancedUnlockCallback? = null - - private var isKeyManagerInit = false - - private val biometricUnlockEnable = PreferencesUtil.isBiometricUnlockEnable(retrieveContext()) - private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(retrieveContext()) - - val isKeyManagerInitialized: Boolean - get() { - if (!isKeyManagerInit) { - advancedUnlockCallback?.onGenericException(Exception("Biometric not initialized")) - } - return isKeyManagerInit - } - - private fun isBiometricOperation(): Boolean { - return biometricUnlockEnable || isDeviceCredentialBiometricOperation() - } - - // Since Android 30, device credential is also a biometric operation - private fun isDeviceCredentialOperation(): Boolean { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.R - && deviceCredentialUnlockEnable - } - - private fun isDeviceCredentialBiometricOperation(): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - && deviceCredentialUnlockEnable - } + private var biometricUnlockEnable = isBiometricUnlockEnable(appContext) + private var deviceCredentialUnlockEnable = isDeviceCredentialUnlockEnable(appContext) init { - if (isDeviceSecure(retrieveContext()) - && (biometricUnlockEnable || deviceCredentialUnlockEnable)) { - try { - this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE) - this.keyGenerator = KeyGenerator.getInstance(ADVANCED_UNLOCK_KEY_ALGORITHM, ADVANCED_UNLOCK_KEYSTORE) - this.cipher = Cipher.getInstance( + if (biometricUnlockEnable || deviceCredentialUnlockEnable) { + if (isDeviceSecure(appContext)) { + try { + this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE) + this.keyGenerator = KeyGenerator.getInstance( + ADVANCED_UNLOCK_KEY_ALGORITHM, + ADVANCED_UNLOCK_KEYSTORE + ) + this.cipher = Cipher.getInstance( ADVANCED_UNLOCK_KEY_ALGORITHM + "/" + ADVANCED_UNLOCK_BLOCKS_MODES + "/" - + ADVANCED_UNLOCK_ENCRYPTION_PADDING) - isKeyManagerInit = (keyStore != null - && keyGenerator != null - && cipher != null) - } catch (e: Exception) { - Log.e(TAG, "Unable to initialize the keystore", e) - isKeyManagerInit = false - advancedUnlockCallback?.onGenericException(e) + + ADVANCED_UNLOCK_ENCRYPTION_PADDING + ) + if (keyStore == null) { + throw SecurityException("Unable to initialize the keystore") + } + if (keyGenerator == null) { + throw SecurityException("Unable to initialize the key generator") + } + if (cipher == null) { + throw SecurityException("Unable to initialize the cipher") + } + } catch (e: Exception) { + Log.e(TAG, "Unable to initialize the device unlock manager", e) + throw e + } + } else { + throw SecurityException("Device not secure enough") } - } else { - // really not much to do when no fingerprint support found - isKeyManagerInit = false } } @Synchronized private fun getSecretKey(): SecretKey? { - if (!isKeyManagerInitialized) { - return null - } try { // Create new key if needed keyStore?.let { keyStore -> keyStore.load(null) - try { if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) { // Set the alias of the entry in Android KeyStore where the key will appear @@ -151,7 +110,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) } // To store in the security chip if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P - && retrieveContext().packageManager.hasSystemFeature( + && appContext.packageManager.hasSystemFeature( PackageManager.FEATURE_STRONGBOX_KEYSTORE)) { setIsStrongBoxBacked(true) } @@ -161,98 +120,111 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) } } catch (e: Exception) { Log.e(TAG, "Unable to create a key in keystore", e) - advancedUnlockCallback?.onGenericException(e) + throw e } - return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey? } } catch (e: Exception) { Log.e(TAG, "Unable to retrieve the key in keystore", e) - advancedUnlockCallback?.onGenericException(e) + throw e } return null } - @Synchronized fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) { + @Synchronized fun initEncryptData( + actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit + ) { initEncryptData(actionIfCypherInit, true) } - @Synchronized private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit, - firstLaunch: Boolean) { - if (!isKeyManagerInitialized) { - return - } + @Synchronized private fun initEncryptData( + actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit, + firstLaunch: Boolean + ) { try { getSecretKey()?.let { secretKey -> cipher?.let { cipher -> cipher.init(Cipher.ENCRYPT_MODE, secretKey) - actionIfCypherInit.invoke( - AdvancedUnlockCryptoPrompt( - cipher, - R.string.advanced_unlock_prompt_store_credential_title, - R.string.advanced_unlock_prompt_store_credential_message, - isDeviceCredentialOperation(), isBiometricOperation()) + DeviceUnlockCryptoPrompt( + type = DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION, + cipher = cipher, + titleId = R.string.advanced_unlock_prompt_store_credential_title, + descriptionId = R.string.advanced_unlock_prompt_store_credential_message, + isDeviceCredentialOperation = isDeviceCredentialOperation( + deviceCredentialUnlockEnable + ), + isBiometricOperation = isBiometricOperation( + biometricUnlockEnable, deviceCredentialUnlockEnable + ) + ) ) } } } catch (unrecoverableKeyException: UnrecoverableKeyException) { Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException) - advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException) + throw unrecoverableKeyException } catch (invalidKeyException: KeyPermanentlyInvalidatedException) { Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException) if (firstLaunch) { - deleteAllEntryKeysInKeystoreForBiometric(retrieveContext()) + deleteAllEntryKeysInKeystoreForBiometric(appContext) initEncryptData(actionIfCypherInit, false) } else { - advancedUnlockCallback?.onInvalidKeyException(invalidKeyException) + throw invalidKeyException } } catch (e: Exception) { Log.e(TAG, "Unable to initialize encrypt data", e) - advancedUnlockCallback?.onGenericException(e) + throw e } } - @Synchronized fun encryptData(value: ByteArray) { - if (!isKeyManagerInitialized) { - return - } + @Synchronized fun encryptData( + value: ByteArray, + handleEncryptedResult: (encryptedValue: ByteArray, ivSpec: ByteArray) -> Unit + ) { try { val encrypted = cipher?.doFinal(value) ?: byteArrayOf() // passes updated iv spec on to callback so this can be stored for decryption cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec -> - advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv) + handleEncryptedResult.invoke(encrypted, spec.iv) } } catch (e: Exception) { Log.e(TAG, "Unable to encrypt data", e) - advancedUnlockCallback?.onGenericException(e) + throw e } } - @Synchronized fun initDecryptData(ivSpecValue: ByteArray, - actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { + @Synchronized fun initDecryptData( + ivSpecValue: ByteArray, + actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit + ) { initDecryptData(ivSpecValue, actionIfCypherInit, true) } - @Synchronized private fun initDecryptData(ivSpecValue: ByteArray, - actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit, - firstLaunch: Boolean = true) { - if (!isKeyManagerInitialized) { - return - } + @Synchronized private fun initDecryptData( + ivSpecValue: ByteArray, + actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit, + firstLaunch: Boolean + ) { try { // important to restore spec here that was used for decryption val spec = IvParameterSpec(ivSpecValue) getSecretKey()?.let { secretKey -> cipher?.let { cipher -> cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) - actionIfCypherInit.invoke( - AdvancedUnlockCryptoPrompt( - cipher, - R.string.advanced_unlock_prompt_extract_credential_title, - null, - isDeviceCredentialOperation(), isBiometricOperation()) + DeviceUnlockCryptoPrompt( + type = DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION, + cipher = cipher, + titleId = R.string.advanced_unlock_prompt_extract_credential_title, + descriptionId = null, + isDeviceCredentialOperation = isDeviceCredentialOperation( + deviceCredentialUnlockEnable + ), + isBiometricOperation = isBiometricOperation( + biometricUnlockEnable, deviceCredentialUnlockEnable + ) + ) ) } } @@ -262,37 +234,34 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) deleteKeystoreKey() initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch) } else { - advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException) + throw unrecoverableKeyException } } catch (invalidKeyException: KeyPermanentlyInvalidatedException) { Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException) if (firstLaunch) { - deleteAllEntryKeysInKeystoreForBiometric(retrieveContext()) + deleteAllEntryKeysInKeystoreForBiometric(appContext) initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch) } else { - advancedUnlockCallback?.onInvalidKeyException(invalidKeyException) + throw invalidKeyException } } catch (e: Exception) { Log.e(TAG, "Unable to initialize decrypt data", e) - advancedUnlockCallback?.onGenericException(e) + throw e } } - @Synchronized fun decryptData(encryptedValue: ByteArray) { - if (!isKeyManagerInitialized) { - return - } + @Synchronized fun decryptData( + encryptedValue: ByteArray, + handleDecryptedResult: (decryptedValue: ByteArray) -> Unit + ) { try { // actual decryption here cipher?.doFinal(encryptedValue)?.let { decrypted -> - advancedUnlockCallback?.handleDecryptedResult(decrypted) + handleDecryptedResult.invoke(decrypted) } - } catch (badPaddingException: BadPaddingException) { - Log.e(TAG, "Unable to decrypt data", badPaddingException) - advancedUnlockCallback?.onInvalidKeyException(badPaddingException) } catch (e: Exception) { Log.e(TAG, "Unable to decrypt data", e) - advancedUnlockCallback?.onGenericException(e) + throw e } } @@ -302,71 +271,13 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY) } catch (e: Exception) { Log.e(TAG, "Unable to delete entry key in keystore", e) - advancedUnlockCallback?.onGenericException(e) + throw e } } - @Synchronized fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt, - deviceCredentialResultLauncher: ActivityResultLauncher - ) { - // Init advanced unlock prompt - if (biometricPrompt == null) { - biometricPrompt = BiometricPrompt(retrieveContext(), - Executors.newSingleThreadExecutor(), - authenticationCallback) - } - - val promptTitle = retrieveContext().getString(cryptoPrompt.promptTitleId) - val promptDescription = cryptoPrompt.promptDescriptionId?.let { descriptionId -> - retrieveContext().getString(descriptionId) - } ?: "" - - if (cryptoPrompt.isBiometricOperation) { - val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply { - setTitle(promptTitle) - if (promptDescription.isNotEmpty()) - setDescription(promptDescription) - setConfirmationRequired(false) - if (isDeviceCredentialBiometricOperation()) { - setAllowedAuthenticators(DEVICE_CREDENTIAL) - } else { - setNegativeButtonText(retrieveContext().getString(android.R.string.cancel)) - } - }.build() - biometricPrompt?.authenticate( - promptInfoExtractCredential, - BiometricPrompt.CryptoObject(cryptoPrompt.cipher)) - } - else if (cryptoPrompt.isDeviceCredentialOperation) { - val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java) - @Suppress("DEPRECATION") - deviceCredentialResultLauncher.launch( - keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription) - ) - } - } - - @Synchronized fun closeBiometricPrompt() { - biometricPrompt?.cancelAuthentication() - } - - interface AdvancedUnlockErrorCallback { - fun onUnrecoverableKeyException(e: Exception) - fun onInvalidKeyException(e: Exception) - fun onGenericException(e: Exception) - } - - interface AdvancedUnlockCallback : AdvancedUnlockErrorCallback { - fun onAuthenticationSucceeded() - fun onAuthenticationFailed() - fun onAuthenticationError(errorCode: Int, errString: CharSequence) - fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) - fun handleDecryptedResult(decryptedValue: ByteArray) - } - companion object { - private val TAG = AdvancedUnlockManager::class.java.name + private val TAG = DeviceUnlockManager::class.java.name private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore" private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key" @@ -445,62 +356,65 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) /** * Remove entry key in keystore */ - fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity, - advancedCallback: AdvancedUnlockErrorCallback) { - AdvancedUnlockManager{ fragmentActivity }.apply { - advancedUnlockCallback = object : AdvancedUnlockCallback { - override fun onAuthenticationSucceeded() {} - - override fun onAuthenticationFailed() {} - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {} - - override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {} - - override fun handleDecryptedResult(decryptedValue: ByteArray) {} - - override fun onUnrecoverableKeyException(e: Exception) { - advancedCallback.onUnrecoverableKeyException(e) - } - - override fun onInvalidKeyException(e: Exception) { - advancedCallback.onInvalidKeyException(e) - } - - override fun onGenericException(e: Exception) { - advancedCallback.onGenericException(e) - } - } + fun deleteEntryKeyInKeystoreForBiometric( + appContext: Context + ) { + DeviceUnlockManager(appContext).apply { deleteKeystoreKey() } } - fun deleteAllEntryKeysInKeystoreForBiometric(activity: FragmentActivity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - deleteEntryKeyInKeystoreForBiometric( - activity, - object : AdvancedUnlockErrorCallback { - fun showException(e: Exception) { - Toast.makeText(activity, - activity.getString(R.string.advanced_unlock_scanning_error, e.localizedMessage), - Toast.LENGTH_SHORT).show() - } - - override fun onUnrecoverableKeyException(e: Exception) { - showException(e) - } - - override fun onInvalidKeyException(e: Exception) { - showException(e) - } - - override fun onGenericException(e: Exception) { - showException(e) - } - }) + fun deleteAllEntryKeysInKeystoreForBiometric(appContext: Context) { + try { + deleteEntryKeyInKeystoreForBiometric(appContext) + } catch (e: Exception) { + Toast.makeText(appContext, + appContext.getString( + R.string.advanced_unlock_scanning_error, + e.localizedMessage + ), + Toast.LENGTH_SHORT).show() + } finally { + CipherDatabaseAction.getInstance(appContext).deleteAll() } - CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll() } } +} +fun isBiometricUnlockEnable(appContext: Context) = + PreferencesUtil.isBiometricUnlockEnable(appContext) + +fun isDeviceCredentialUnlockEnable(appContext: Context) = + PreferencesUtil.isDeviceCredentialUnlockEnable(appContext) + +private fun isBiometricOperation( + biometricUnlockEnable: Boolean, + deviceCredentialUnlockEnable: Boolean +): Boolean { + return biometricUnlockEnable + || isDeviceCredentialBiometricOperation(deviceCredentialUnlockEnable) +} + +// Since Android 30, device credential is also a biometric operation +private fun isDeviceCredentialOperation( + deviceCredentialUnlockEnable: Boolean +): Boolean { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.R + && deviceCredentialUnlockEnable +} + +private fun isDeviceCredentialBiometricOperation( + deviceCredentialUnlockEnable: Boolean +): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && deviceCredentialUnlockEnable +} + +fun isDeviceCredentialBiometricOperation(context: Context?): Boolean { + if (context == null) { + return false + } + return isDeviceCredentialBiometricOperation( + isDeviceCredentialUnlockEnable(context) + ) } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockMode.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockMode.kt new file mode 100644 index 000000000..46d5df4a1 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockMode.kt @@ -0,0 +1,11 @@ +package com.kunzisoft.keepass.biometric + +enum class DeviceUnlockMode { + BIOMETRIC_UNAVAILABLE, + BIOMETRIC_SECURITY_UPDATE_REQUIRED, + DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED, + KEY_MANAGER_UNAVAILABLE, + WAIT_CREDENTIAL, + STORE_CREDENTIAL, + EXTRACT_CREDENTIAL +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt index 88f24cffd..c1042d990 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt @@ -41,7 +41,7 @@ import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction -import com.kunzisoft.keepass.biometric.AdvancedUnlockManager +import com.kunzisoft.keepass.biometric.DeviceUnlockManager import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.icons.IconPackChooser import com.kunzisoft.keepass.services.ClipboardEntryNotificationService @@ -251,7 +251,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { val tempAdvancedUnlockPreference: TwoStatePreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key)) val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - AdvancedUnlockManager.biometricUnlockSupported(activity) + DeviceUnlockManager.biometricUnlockSupported(activity) } else false biometricUnlockEnablePreference?.apply { // False if under Marshmallow @@ -296,7 +296,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { } val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - AdvancedUnlockManager.deviceCredentialUnlockSupported(activity) + DeviceUnlockManager.deviceCredentialUnlockSupported(activity) } else false deviceCredentialUnlockEnablePreference?.apply { // Biometric unlock already checked @@ -395,7 +395,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { validate?.invoke() warningAlertDialog?.setOnDismissListener(null) if (deleteKeys && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity) + DeviceUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity) } } .setNegativeButton(resources.getString(android.R.string.cancel) diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt index 765278227..8a48448ba 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt @@ -29,7 +29,7 @@ import androidx.preference.PreferenceManager import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.stylish.Stylish -import com.kunzisoft.keepass.biometric.AdvancedUnlockManager +import com.kunzisoft.keepass.biometric.DeviceUnlockManager import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.search.SearchParameters import com.kunzisoft.keepass.education.Education @@ -512,7 +512,7 @@ object PreferencesUtil { return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key), context.resources.getBoolean(R.bool.biometric_unlock_enable_default)) && (if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - AdvancedUnlockManager.biometricUnlockSupported(context) + DeviceUnlockManager.biometricUnlockSupported(context) } else { false }) diff --git a/app/src/main/java/com/kunzisoft/keepass/view/AdvancedUnlockInfoView.kt b/app/src/main/java/com/kunzisoft/keepass/view/DeviceUnlockView.kt similarity index 84% rename from app/src/main/java/com/kunzisoft/keepass/view/AdvancedUnlockInfoView.kt rename to app/src/main/java/com/kunzisoft/keepass/view/DeviceUnlockView.kt index 083b2be73..7c7f111b7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/AdvancedUnlockInfoView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/DeviceUnlockView.kt @@ -31,9 +31,9 @@ import androidx.annotation.StringRes import com.kunzisoft.keepass.R @RequiresApi(api = Build.VERSION_CODES.M) -class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0) +class DeviceUnlockView @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0) : LinearLayout(context, attrs, defStyle) { private var biometricButtonView: Button? = null @@ -45,7 +45,7 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context, biometricButtonView = findViewById(R.id.biometric_button) } - fun setIconViewClickListener(listener: OnClickListener?) { + fun setDeviceUnlockButtonViewClickListener(listener: OnClickListener?) { biometricButtonView?.setOnClickListener(listener) } @@ -61,8 +61,8 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context, title = context.getString(textId) } - fun setMessage(text: CharSequence) { - if (text.isNotEmpty()) + fun setMessage(text: CharSequence?) { + if (!text.isNullOrEmpty()) Toast.makeText(context, text, Toast.LENGTH_LONG).show() } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/AdvancedUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/AdvancedUnlockViewModel.kt deleted file mode 100644 index e3e174e50..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/AdvancedUnlockViewModel.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.kunzisoft.keepass.viewmodels - -import android.net.Uri -import androidx.lifecycle.ViewModel -import com.kunzisoft.keepass.model.CipherDecryptDatabase -import com.kunzisoft.keepass.model.CipherEncryptDatabase -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update - -class AdvancedUnlockViewModel : ViewModel() { - - var allowAutoOpenBiometricPrompt : Boolean = true - var deviceCredentialAuthSucceeded: Boolean? = null - - private val _uiState = MutableStateFlow(DeviceUnlockState()) - val uiState: StateFlow = _uiState - - fun checkUnlockAvailability(conditionToStoreCredentialVerified: Boolean? = null) { - _uiState.update { currentState -> - currentState.copy( - onUnlockAvailabilityCheckRequested = true, - isConditionToStoreCredentialVerified = conditionToStoreCredentialVerified - ?: _uiState.value.isConditionToStoreCredentialVerified - ) - } - } - - fun consumeCheckUnlockAvailability() { - _uiState.update { currentState -> - currentState.copy( - onUnlockAvailabilityCheckRequested = false - ) - } - } - - fun databaseFileLoaded(databaseUri: Uri?) { - _uiState.update { currentState -> - currentState.copy( - databaseFileLoaded = databaseUri - ) - } - } - - fun consumeDatabaseFileLoaded() { - _uiState.update { currentState -> - currentState.copy( - databaseFileLoaded = null - ) - } - } - - fun retrieveCredentialForEncryption() { - _uiState.update { currentState -> - currentState.copy( - isCredentialRequired = true, - credential = null - ) - } - } - - fun provideCredentialForEncryption(credential: ByteArray) { - _uiState.update { currentState -> - currentState.copy( - isCredentialRequired = false, - credential = credential - ) - } - } - - fun consumeCredentialForEncryption() { - _uiState.update { currentState -> - currentState.copy( - isCredentialRequired = false, - credential = null - ) - } - } - - fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) { - _uiState.update { currentState -> - currentState.copy( - cipherEncryptDatabase = cipherEncryptDatabase - ) - } - } - - fun consumeCredentialEncrypted() { - _uiState.update { currentState -> - currentState.copy( - cipherEncryptDatabase = null - ) - } - } - - fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) { - _uiState.update { currentState -> - currentState.copy( - cipherDecryptDatabase = cipherDecryptDatabase - ) - } - } - - fun consumeCredentialDecrypted() { - _uiState.update { currentState -> - currentState.copy( - cipherDecryptDatabase = null - ) - } - } -} - -data class DeviceUnlockState( - val initAdvancedUnlockMode: Boolean = false, - val databaseFileLoaded: Uri? = null, - val isCredentialRequired: Boolean = false, - val credential: ByteArray? = null, - val isConditionToStoreCredentialVerified: Boolean = false, - val onUnlockAvailabilityCheckRequested: Boolean = false, - val cipherEncryptDatabase: CipherEncryptDatabase? = null, - val cipherDecryptDatabase: CipherDecryptDatabase? = null -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as DeviceUnlockState - - if (initAdvancedUnlockMode != other.initAdvancedUnlockMode) return false - if (isCredentialRequired != other.isCredentialRequired) return false - if (isConditionToStoreCredentialVerified != other.isConditionToStoreCredentialVerified) return false - if (onUnlockAvailabilityCheckRequested != other.onUnlockAvailabilityCheckRequested) return false - if (databaseFileLoaded != other.databaseFileLoaded) return false - if (!credential.contentEquals(other.credential)) return false - if (cipherEncryptDatabase != other.cipherEncryptDatabase) return false - if (cipherDecryptDatabase != other.cipherDecryptDatabase) return false - - return true - } - - override fun hashCode(): Int { - var result = initAdvancedUnlockMode.hashCode() - result = 31 * result + isCredentialRequired.hashCode() - result = 31 * result + isConditionToStoreCredentialVerified.hashCode() - result = 31 * result + onUnlockAvailabilityCheckRequested.hashCode() - result = 31 * result + (databaseFileLoaded?.hashCode() ?: 0) - result = 31 * result + (credential?.contentHashCode() ?: 0) - result = 31 * result + (cipherEncryptDatabase?.hashCode() ?: 0) - result = 31 * result + (cipherDecryptDatabase?.hashCode() ?: 0) - return result - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt new file mode 100644 index 000000000..1ce5e62ec --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -0,0 +1,388 @@ +package com.kunzisoft.keepass.viewmodels + +import android.app.Application +import android.net.Uri +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.biometric.BiometricManager +import androidx.lifecycle.AndroidViewModel +import com.kunzisoft.keepass.app.database.CipherDatabaseAction +import com.kunzisoft.keepass.biometric.DeviceUnlockCryptoPrompt +import com.kunzisoft.keepass.biometric.DeviceUnlockManager +import com.kunzisoft.keepass.biometric.DeviceUnlockMode +import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException +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.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class DeviceUnlockViewModel(application: Application): AndroidViewModel(application) { + + var allowAutoOpenBiometricPrompt : Boolean = true + var deviceCredentialAuthSucceeded: Boolean? = null + + private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null + + private var isConditionToStoreCredentialVerified: Boolean = false + + private var deviceUnlockManager: DeviceUnlockManager? = null + private var databaseUri: Uri? = null + + // TODO Retrieve credential storage from app database + var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT + + val cipherDatabaseAction = CipherDatabaseAction.getInstance(getApplication()) + + private val _uiState = MutableStateFlow(DeviceUnlockState()) + val uiState: StateFlow = _uiState + + fun checkConditionToStoreCredential(condition: Boolean, databaseFileUri: Uri?) { + isConditionToStoreCredentialVerified = condition + checkUnlockAvailability(databaseFileUri) + } + + /** + * Check unlock availability and change the current mode depending of device's state + */ + fun checkUnlockAvailability(databaseFileUri: Uri?) { + databaseUri = databaseFileUri + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipherDatabase -> + if (PreferencesUtil.isBiometricUnlockEnable(getApplication())) { + // biometric not supported (by API level or hardware) so keep option hidden + // or manually disable + val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(getApplication()) + if (!PreferencesUtil.isAdvancedUnlockEnable(getApplication()) + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { + changeMode(DeviceUnlockMode.BIOMETRIC_UNAVAILABLE) + } else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) { + changeMode(DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED) + } else { + // biometric is available but not configured, show icon but in disabled state with some information + if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { + changeMode(DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED) + } else { + selectMode(containsCipherDatabase) + } + } + } else if (PreferencesUtil.isDeviceCredentialUnlockEnable(getApplication())) { + if (DeviceUnlockManager.isDeviceSecure(getApplication())) { + selectMode(containsCipherDatabase) + } else { + changeMode(DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED) + } + } + } + } + } + + private fun isModeChanging(newMode: DeviceUnlockMode): Boolean { + return _uiState.value.deviceUnlockMode != newMode + } + + @RequiresApi(Build.VERSION_CODES.M) + fun selectMode(containsCipherDatabase: Boolean) { + try { + if (isConditionToStoreCredentialVerified) { + if (deviceUnlockManager == null + || isModeChanging(DeviceUnlockMode.STORE_CREDENTIAL)) { + deviceUnlockManager = DeviceUnlockManager(getApplication()) + } + // listen for encryption + changeMode(DeviceUnlockMode.STORE_CREDENTIAL) + initEncryptData() + } else if (containsCipherDatabase) { + if (deviceUnlockManager == null + || isModeChanging(DeviceUnlockMode.EXTRACT_CREDENTIAL)) { + deviceUnlockManager = DeviceUnlockManager(getApplication()) + } + // biometric available but no stored password found yet for this DB + // listen for decryption + changeMode(DeviceUnlockMode.EXTRACT_CREDENTIAL) + initDecryptData() + } else { + // wait for typing + changeMode(DeviceUnlockMode.WAIT_CREDENTIAL) + } + } catch (e: Exception) { + changeMode(DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE) + setException(e) + } + } + + fun connect(databaseUri: Uri) { + this.databaseUri = databaseUri + cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener { + override fun onCipherDatabaseCleared() { + closeBiometricPrompt() + checkUnlockAvailability(databaseUri) + } + } + cipherDatabaseAction.apply { + reloadPreferences() + cipherDatabaseListener?.let { + registerDatabaseListener(it) + } + } + checkUnlockAvailability(databaseUri) + } + + fun disconnect() { + this.databaseUri = null + closeBiometricPrompt() + cipherDatabaseListener?.let { + cipherDatabaseAction.unregisterDatabaseListener(it) + } + reset() + } + + fun databaseFileLoaded(databaseUri: Uri?) { + // To get device credential unlock result, only if same database uri + if (databaseUri != null + && PreferencesUtil.isAdvancedUnlockEnable(getApplication())) { + deviceCredentialAuthSucceeded?.let { authSucceeded -> + if (databaseUri == this.databaseUri) { + if (authSucceeded) { + retrieveCredentialForEncryption() + } + } else { + disconnect() + } + } ?: run { + if (databaseUri != this.databaseUri) { + connect(databaseUri) + } + } + } else { + disconnect() + } + deviceCredentialAuthSucceeded = null + } + + fun retrieveCredentialForEncryption() { + _uiState.update { currentState -> + currentState.copy( + isCredentialRequired = true + ) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + fun encryptCredential( + credential: ByteArray + ) { + try { + deviceUnlockManager?.encryptData( + value = credential, + handleEncryptedResult = { encryptedValue, ivSpec -> + databaseUri?.let { databaseUri -> + onCredentialEncrypted( + CipherEncryptDatabase().apply { + this.databaseUri = databaseUri + this.credentialStorage = credentialDatabaseStorage + this.encryptedValue = encryptedValue + this.specParameters = ivSpec + } + ) + } + } + ) + } catch (e: Exception) { + setException(e) + } + _uiState.update { currentState -> + currentState.copy( + isCredentialRequired = false + ) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + fun decryptCredential() { + // retrieve the encrypted value from preferences + databaseUri?.let { databaseUri -> + cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase -> + cipherDatabase?.encryptedValue?.let { encryptedCredential -> + try { + deviceUnlockManager?.decryptData( + encryptedValue = encryptedCredential, + handleDecryptedResult = { decryptedValue -> + // Load database directly with password retrieve + onCredentialDecrypted( + CipherDecryptDatabase().apply { + this.databaseUri = databaseUri + this.credentialStorage = credentialDatabaseStorage + this.decryptedValue = decryptedValue + } + ) + cipherDatabaseAction.resetCipherParameters(databaseUri) + } + ) + } catch (e: Exception) { + setException(e) + } + } ?: deleteEncryptedDatabaseKey() + } + } ?: run { + setException(UnknownDatabaseLocationException()) + } + } + + fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) { + _uiState.update { currentState -> + currentState.copy( + cipherEncryptDatabase = cipherEncryptDatabase + ) + } + } + + fun consumeCredentialEncrypted() { + _uiState.update { currentState -> + currentState.copy( + cipherEncryptDatabase = null + ) + } + } + + fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) { + _uiState.update { currentState -> + currentState.copy( + cipherDecryptDatabase = cipherDecryptDatabase + ) + } + } + + fun consumeCredentialDecrypted() { + _uiState.update { currentState -> + currentState.copy( + cipherDecryptDatabase = null + ) + } + } + + fun onPromptRequested(cryptoPrompt: DeviceUnlockCryptoPrompt) { + _uiState.update { currentState -> + currentState.copy( + cryptoPrompt = cryptoPrompt + ) + } + } + + fun promptShown() { + _uiState.update { currentState -> + currentState.copy( + cryptoPrompt = null + ) + } + } + + fun setException(value: Exception?) { + _uiState.update { currentState -> + currentState.copy( + error = value + ) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun initEncryptData() { + try { + deviceUnlockManager?.initEncryptData { cryptoPrompt -> + onPromptRequested(cryptoPrompt) + } ?: setException(Exception("AdvancedUnlockManager not initialized")) + } catch (e: Exception) { + setException(e) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun initDecryptData() { + databaseUri?.let { databaseUri -> + cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase -> + cipherDatabase?.let { + try { + deviceUnlockManager?.initDecryptData(cipherDatabase.specParameters) { cryptoPrompt -> + onPromptRequested(cryptoPrompt) + } ?: setException(Exception("AdvancedUnlockManager not initialized")) + } catch (e: Exception) { + setException(e) + } + } ?: deleteEncryptedDatabaseKey() + } + } ?: setException(UnknownDatabaseLocationException()) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun changeMode(deviceUnlockMode: DeviceUnlockMode) { + cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher -> + _uiState.update { currentState -> + currentState.copy( + deviceUnlockMode = deviceUnlockMode, + allowAdvancedUnlockMenu = containsCipher + && deviceUnlockMode != DeviceUnlockMode.BIOMETRIC_UNAVAILABLE + && deviceUnlockMode != DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE + ) + } + } + } + + fun deleteEncryptedDatabaseKey() { + closeBiometricPrompt() + databaseUri?.let { databaseUri -> + cipherDatabaseAction.deleteByDatabaseUri(databaseUri) { + checkUnlockAvailability(databaseUri) + } + } ?: checkUnlockAvailability(null) + _uiState.update { currentState -> + currentState.copy( + allowAdvancedUnlockMenu = false + ) + } + } + + fun closeBiometricPrompt() { + _uiState.update { currentState -> + currentState.copy( + cryptoPrompt = null, + closePromptRequested = true + ) + } + } + + fun biometricPromptClosed() { + _uiState.update { currentState -> + currentState.copy( + closePromptRequested = false + ) + } + } + + fun reset() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + changeMode(DeviceUnlockMode.BIOMETRIC_UNAVAILABLE) + } + } + + override fun onCleared() { + super.onCleared() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + deviceUnlockManager = null + } + } +} + +data class DeviceUnlockState( + val deviceUnlockMode: DeviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE, + val allowAdvancedUnlockMenu: Boolean = false, + val isCredentialRequired: Boolean = false, + val cipherEncryptDatabase: CipherEncryptDatabase? = null, + val cipherDecryptDatabase: CipherDecryptDatabase? = null, + val cryptoPrompt: DeviceUnlockCryptoPrompt? = null, + val closePromptRequested: Boolean = false, + val error: Exception? = null +) \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_advanced_unlock.xml b/app/src/main/res/layout-v23/fragment_advanced_unlock.xml similarity index 76% rename from app/src/main/res/layout/fragment_advanced_unlock.xml rename to app/src/main/res/layout-v23/fragment_advanced_unlock.xml index c19dccbb7..441af599e 100644 --- a/app/src/main/res/layout/fragment_advanced_unlock.xml +++ b/app/src/main/res/layout-v23/fragment_advanced_unlock.xml @@ -1,5 +1,5 @@ -Colourise password characters by type Entry background colour Could not recognise the database format. - Could not recognise advanced unlock print - Unable to initialise advanced unlock prompt. + Could not recognise device unlock print + Unable to initialise device unlock prompt. Initialising… Finalising… Cancelled! diff --git a/fastlane/metadata/android/en-US/changelogs/136.txt b/fastlane/metadata/android/en-US/changelogs/136.txt index 42780ecb1..da78f6bc3 100644 --- a/fastlane/metadata/android/en-US/changelogs/136.txt +++ b/fastlane/metadata/android/en-US/changelogs/136.txt @@ -1 +1 @@ - * \ No newline at end of file + * Fix UnlockManager #2098 #2101 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/136.txt b/fastlane/metadata/android/fr-FR/changelogs/136.txt index 42780ecb1..4ca55c8b9 100644 --- a/fastlane/metadata/android/fr-FR/changelogs/136.txt +++ b/fastlane/metadata/android/fr-FR/changelogs/136.txt @@ -1 +1 @@ - * \ No newline at end of file + * Correction UnlockManager #2098 #2101 \ No newline at end of file From 3a8245ee747893639cc619cabec937bd662946dc Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 10 Aug 2025 21:32:12 +0200 Subject: [PATCH 03/26] fix: Exception as Snackbar --- .../activities/MainCredentialActivity.kt | 17 +++- .../keepass/biometric/DeviceUnlockFragment.kt | 90 +++++-------------- .../keepass/biometric/DeviceUnlockManager.kt | 16 +++- .../keepass/view/DeviceUnlockView.kt | 11 --- .../viewmodels/DeviceUnlockViewModel.kt | 12 ++- app/src/main/res/values-ar/strings.xml | 1 - app/src/main/res/values-az/strings.xml | 1 - app/src/main/res/values-bg/strings.xml | 1 - app/src/main/res/values-ca/strings.xml | 1 - app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-da/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-el/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-et/strings.xml | 1 - app/src/main/res/values-eu/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-gl/strings.xml | 1 - app/src/main/res/values-hr/strings.xml | 1 - app/src/main/res/values-hu/strings.xml | 1 - app/src/main/res/values-in/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-iw/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-lt/strings.xml | 1 - app/src/main/res/values-nb/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt-rPT/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-ro/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sk/strings.xml | 1 - app/src/main/res/values-sq/strings.xml | 1 - app/src/main/res/values-ta/strings.xml | 1 - app/src/main/res/values-th/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-uk/strings.xml | 1 - app/src/main/res/values-vi/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - 43 files changed, 58 insertions(+), 126 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index 5afb94256..d1e23e726 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -57,13 +57,18 @@ import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.biometric.DeviceUnlockFragment import com.kunzisoft.keepass.biometric.DeviceUnlockManager +import com.kunzisoft.keepass.biometric.deviceUnlockError import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.hardware.HardwareKey -import com.kunzisoft.keepass.model.* +import com.kunzisoft.keepass.model.CipherDecryptDatabase +import com.kunzisoft.keepass.model.CipherEncryptDatabase +import com.kunzisoft.keepass.model.CredentialStorage +import com.kunzisoft.keepass.model.RegisterInfo +import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY @@ -81,8 +86,8 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.view.MainCredentialView import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.showActionErrorIfNeeded -import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel +import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel import kotlinx.coroutines.launch import java.io.FileNotFoundException @@ -244,6 +249,14 @@ class MainCredentialActivity : DatabaseModeActivity() { onCredentialDecrypted(cipherDecryptDatabase) mDeviceUnlockViewModel.consumeCredentialDecrypted() } + uiState.exception?.let { error -> + Snackbar.make( + coordinatorLayout, + deviceUnlockError(error, this@MainCredentialActivity), + Snackbar.LENGTH_LONG + ).asError().show() + } + mDeviceUnlockViewModel.exceptionShown() } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index 98391e983..bd61d119c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -25,7 +25,6 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.provider.Settings -import android.security.keystore.KeyPermanentlyInvalidatedException import android.util.Log import android.view.LayoutInflater import android.view.Menu @@ -52,7 +51,6 @@ import com.kunzisoft.keepass.view.showByFading import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.security.UnrecoverableKeyException import java.util.concurrent.Executors @RequiresApi(Build.VERSION_CODES.M) @@ -164,8 +162,6 @@ class DeviceUnlockFragment: Fragment() { closeBiometricPrompt() mDeviceUnlockViewModel.biometricPromptClosed() } - // Errors - setAdvancedUnlockedError(uiState.error) // Advanced menu mAllowAdvancedUnlockMenu = uiState.allowAdvancedUnlockMenu activity?.invalidateOptionsMenu() @@ -249,7 +245,7 @@ class DeviceUnlockFragment: Fragment() { DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode() } } catch (e: Exception) { - showGenericException(e) + mDeviceUnlockViewModel.setException(e) } } @@ -297,14 +293,6 @@ class DeviceUnlockFragment: Fragment() { } } - - fun showGenericException(e: Exception) { - lifecycleScope.launch(Dispatchers.Main) { - val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: "" - setAdvancedUnlockedMessageView(errorMessage) - } - } - private fun setNotAvailableMode() { lifecycleScope.launch(Dispatchers.Main) { showViews(false) @@ -346,7 +334,6 @@ class DeviceUnlockFragment: Fragment() { lifecycleScope.launch(Dispatchers.Main) { showViews(true) setAdvancedUnlockedTitleView(R.string.configure_biometric) - setAdvancedUnlockedMessageView("") openBiometricSetting() } } @@ -363,37 +350,26 @@ class DeviceUnlockFragment: Fragment() { lifecycleScope.launch(Dispatchers.Main) { showViews(true) setAdvancedUnlockedTitleView(R.string.unavailable) - setAdvancedUnlockedMessageView("") context?.let { context -> mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { - showError( - BiometricPrompt.ERROR_UNABLE_TO_PROCESS, + mDeviceUnlockViewModel.setException(SecurityException( context.getString(R.string.credential_before_click_advanced_unlock_button) - ) + )) } } } } - private fun showError(errorCode: Int, errString: CharSequence) { - lifecycleScope.launch(Dispatchers.Main) { - Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") - setAdvancedUnlockedMessageView(errString.toString()) - } - } - private fun setStoreCredentialMode() { lifecycleScope.launch(Dispatchers.Main) { showViews(true) setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric) - setAdvancedUnlockedMessageView("") context?.let { context -> mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { view -> storeCredentialButtonClickListener?.onClick(view) ?: run { - showError( - BiometricPrompt.ERROR_HW_UNAVAILABLE, + mDeviceUnlockViewModel.setException(SecurityException( context.getString(R.string.keystore_not_accessible) - ) + )) } storeCredentialButtonClickListener = null } @@ -405,14 +381,12 @@ class DeviceUnlockFragment: Fragment() { lifecycleScope.launch(Dispatchers.Main) { showViews(true) setAdvancedUnlockedTitleView(R.string.unlock) - setAdvancedUnlockedMessageView("") context?.let { context -> mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { view -> extractCredentialButtonClickListener?.onClick(view) ?: run { - showError( - BiometricPrompt.ERROR_HW_UNAVAILABLE, + mDeviceUnlockViewModel.setException(SecurityException( context.getString(R.string.keystore_not_accessible) - ) + )) } extractCredentialButtonClickListener = null } @@ -443,47 +417,25 @@ class DeviceUnlockFragment: Fragment() { } } - private fun setAdvancedUnlockedMessageView(textId: Int) { - lifecycleScope.launch(Dispatchers.Main) { - mDeviceUnlockView?.setMessage(textId) - } - } - - private fun setAdvancedUnlockedMessageView(text: CharSequence?) { - lifecycleScope.launch(Dispatchers.Main) { - mDeviceUnlockView?.setMessage(text) - } - } - - private fun setAdvancedUnlockedError(exception: Exception?) { - when (exception) { - is UnrecoverableKeyException -> { - setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) - } - is KeyPermanentlyInvalidatedException -> { - setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) - } - else -> { - setAdvancedUnlockedMessageView( - exception?.cause?.localizedMessage - ?: exception?.localizedMessage - ?: "") - } - } - } - private fun setAuthenticationError(errorCode: Int, errString: CharSequence) { - lifecycleScope.launch(Dispatchers.Main) { - Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") - setAdvancedUnlockedMessageView(errString.toString()) + Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") + when (errorCode) { + BiometricPrompt.ERROR_NEGATIVE_BUTTON -> + mDeviceUnlockViewModel.setException( + SecurityException(getString(R.string.error_cancel_by_user)) + ) + else -> + mDeviceUnlockViewModel.setException( + SecurityException(errString.toString()) + ) } } private fun setAuthenticationFailed() { - lifecycleScope.launch(Dispatchers.Main) { - Log.e(TAG, "Biometric authentication failed, biometric not recognized") - setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized) - } + Log.e(TAG, "Biometric authentication failed, biometric not recognized") + mDeviceUnlockViewModel.setException(SecurityException( + getString(R.string.advanced_unlock_not_recognized)) + ) } override fun onPause() { diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt index 20049bc2c..6f4df8dcd 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt @@ -369,10 +369,7 @@ class DeviceUnlockManager(private var appContext: Context) { deleteEntryKeyInKeystoreForBiometric(appContext) } catch (e: Exception) { Toast.makeText(appContext, - appContext.getString( - R.string.advanced_unlock_scanning_error, - e.localizedMessage - ), + deviceUnlockError(e, appContext), Toast.LENGTH_SHORT).show() } finally { CipherDatabaseAction.getInstance(appContext).deleteAll() @@ -381,6 +378,17 @@ class DeviceUnlockManager(private var appContext: Context) { } } +fun deviceUnlockError(error: Exception, context: Context): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && (error is UnrecoverableKeyException + || error is KeyPermanentlyInvalidatedException)) { + context.getString(R.string.advanced_unlock_invalid_key) + } else + error.cause?.localizedMessage + ?: error.localizedMessage + ?: error.toString() +} + fun isBiometricUnlockEnable(appContext: Context) = PreferencesUtil.isBiometricUnlockEnable(appContext) diff --git a/app/src/main/java/com/kunzisoft/keepass/view/DeviceUnlockView.kt b/app/src/main/java/com/kunzisoft/keepass/view/DeviceUnlockView.kt index 7c7f111b7..b840063cb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/DeviceUnlockView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/DeviceUnlockView.kt @@ -25,7 +25,6 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.widget.Button import android.widget.LinearLayout -import android.widget.Toast import androidx.annotation.RequiresApi import androidx.annotation.StringRes import com.kunzisoft.keepass.R @@ -60,14 +59,4 @@ class DeviceUnlockView @JvmOverloads constructor(context: Context, fun setTitle(@StringRes textId: Int) { title = context.getString(textId) } - - fun setMessage(text: CharSequence?) { - if (!text.isNullOrEmpty()) - Toast.makeText(context, text, Toast.LENGTH_LONG).show() - } - - fun setMessage(@StringRes textId: Int) { - Toast.makeText(context, textId, Toast.LENGTH_LONG).show() - } - } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt index 1ce5e62ec..7ab19f20e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -283,7 +283,15 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat fun setException(value: Exception?) { _uiState.update { currentState -> currentState.copy( - error = value + exception = value + ) + } + } + + fun exceptionShown() { + _uiState.update { currentState -> + currentState.copy( + exception = null ) } } @@ -384,5 +392,5 @@ data class DeviceUnlockState( val cipherDecryptDatabase: CipherDecryptDatabase? = null, val cryptoPrompt: DeviceUnlockCryptoPrompt? = null, val closePromptRequested: Boolean = false, - val error: Exception? = null + val exception: Exception? = null ) \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a092d50cc..04ec9d922 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -648,7 +648,6 @@ أظهر \"المعرّف العام المميز\" UUID رابط فتح الجهاز لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح. - خطأ في فتح الجهاز: %1$s المظاهر والألوان والسمات تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 5e2ec3f3f..0a0be5328 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -199,7 +199,6 @@ Açar ehtiyyatı düzgün formada başladılmadı. Cihaz kilidini açma linki Tarixçə - Cihaz kilidini açma xətası: %1$s Məlumat bazasını yenidən yükləmək lokal olaraq modifikasiya olunmuş faylları siləcəkdir. Fayla giriş fayl meneceri tərəfindən ləğv edildi, məlumat bazasını bağlayın və onu olduğu yerdən yenidən açın. Siz tətəbiqin zəngli saatdan istifadə etməsinə icazə verməmisiniz. Nəticədə, taymer tələb edən funksiyalar dəqiq bir zamanda işləməyəckdir. diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index df53582c3..a7b311de5 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -536,7 +536,6 @@ Използване на кошчето Премества групите и записите в групата „Кошче“ вместо да ги премахва директно Отключване с устройството - Грешка при отключване на устройството: %1$s Не може да бъде разпознато кога устройството е отключено Заявката за отключване не може да бъде подготвена. Подразбирана дължина на създаваните пароли diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 20c77cd16..5c658faca 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -613,7 +613,6 @@ Configura Cal actualitzar la seguretat biomètrica. Enllaç de desbloqueig del dispositiu - Error en desbloquejar el dispositiu: %1$s No disponible No s\'ha pogut inicialitzar l\'indicador de desbloqueig del dispositiu. Escriviu la contrasenya i, a continuació, feu clic en aquest botó. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index a2cb4bb27..5c6fdf04f 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -499,7 +499,6 @@ Heslo zařízení Zadejte heslo a pak klepněte na toto tlačítko. Nepodařilo se inicializovat nabídku pro odemykání zařízení. - Chyba při odemykání zařízení: %1$s Otisk pro odemykání zařízení nebyl rozpoznán Nepodařilo se načíst klíč odemykání zařízení. Odstraňte ho a opakujte proces rozpoznání odemknutí. Načíst údaj z databáze pomocí dat odemykání zařízení diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 13f3d66da..4bc7e27ee 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -507,7 +507,6 @@ Indhold Indtast adgangskoden, og klik derefter på denne knap. Kunne ikke initialisere oplåsningsprompt. - Fejl ved oplåsning: %1$s Kunne ikke genkende aftryk til oplåsning Oplåsningsnøgle kan ikke læses. Slet den og gentag proceduren for genkendelse af oplåsning. Enhedsoplåsningsgenkendelse diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e1cfd3f14..b0113e960 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -529,7 +529,6 @@ Geräteanmeldedaten Passwort eingeben und dann diese Taste drücken. Geräteentsperrungsabfrage konnte nicht gestartet werden. - Fehler bei Geräteentsperrung: %1$s Fingerabdruck für Geräteentsperrung wurde nicht erkannt Der Geräteentsperrschlüssel ist nicht lesbar. Bitte diesen löschen und den Vorgang zur Entsperr-Erkennung wiederholen. Datenbankanmeldedaten aus Geräteentsperrdaten gewinnen diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 060f369ff..fab4cc291 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -503,7 +503,6 @@ Πληκτρολογήστε τον κωδικό πρόσβασης, και στη συνέχεια κάντε κλικ αυτό το κουμπί. Δεν είναι δυνατή η προετοιμασία της προτροπής ξεκλειδώματος συσκευής. Δεν ήταν δυνατή η αναγνώριση αποτυπώματος ξεκλειδώματος συσκευής - Σφάλμα ξεκλειδώματος συσκευής: %1$s Δεν είναι δυνατή η ανάγνωση του κλειδιού ξεκλειδώματος της συσκευής. Διαγράψτε το και επαναλάβετε τη διαδικασία αναγνώρισης ξεκλειδώματος. Εξαγωγή διαπιστευτηρίων βάσης δεδομένων με δεδομένα ξεκλειδώματος συσκευής Συνδέστε τον κωδικό πρόσβασής σας με το σαρωμένο βιομετρικό ή τα διαπιστευτήρια της συσκευής σας για να ξεκλειδώσετε γρήγορα τη βάση δεδομένων σας. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c0cabd838..b2ac84eda 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -444,7 +444,6 @@ Credenciales del dispositivo Teclee la contraseña y luego pulse sobre este botón. No se puede inicializar el aviso de desbloqueo avanzado. - Error de desbloqueo del dispositivo: %1$s No se ha podido reconocer la impresión de desbloqueo avanzado No se puede leer la clave de desbloqueo del dispositivo. Por favor, bórrala y repite el procedimiento de reconocimiento del desbloqueo. Extraer la credencial de la base de datos con los datos de desbloqueo del dispositivo diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 6d3bd3a0f..8c0265a05 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -440,7 +440,6 @@ Mestimine õnnestus Vajalik on biomeetrilise turvalisuse uuendus. Krüptitud salasõna on salvestatud - Viga seadme lukustuse eemaldamisel: %1$s Ei õnnestunud tuvastada lukustuse eemaldamiseks vajalikku tunnust Seadme lukustuse eemaldamise päringu käivitamine ei õnnestu. Biomeetriline diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 3e09906fb..2cf927ded 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -489,7 +489,6 @@ Zure kutxa gotorraren pasahitz-nagusia gogoratu behar duzu naiz eta desblokeo aurreratuko ezagutzea erabili arren. Zifratutako pasahitza gordeta Datu-base honek ez du biltegiratuta kredentzialik. - Gailuaren desblokeatze errorea: %1$s Itxura KeePassDXekin erregistratu Gaitu betetze automatikoa beste aplikazioetako formularioak errez betetzeko diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6d9078467..ef35c6b58 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -509,7 +509,6 @@ Identifiant de l\'appareil Tapez le mot de passe, puis cliquez sur ce bouton. Impossible d\'initialiser l\'invite de déverrouillage avancé. - Erreur de déverrouillage avancé : %1$s Impossible de reconnaître l\'empreinte de déverrouillage de l\'appareil Impossible de lire la clé de déverrouillage de l\'appareil. Veuillez la supprimer et répéter la procédure de reconnaissance de déverrouillage. Extraire les identifiants de la base de données avec des données de déverrouillage de l\'appareil diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 1589f2991..cf436aeb2 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -425,7 +425,6 @@ Non foi posíbel ler a clave de desbloqueo avanzado. Por favor, bórrea e repita o procedemento de recoñecemento do desbloqueo. Contrasinal cifrado almacenado Non foi posíbel recoñecer a pegada do desbloqueo avanzado - Erro de desbloqueo avanzado: %1$s Servizo de autocompletado do KeePassDX Historial Aínda precisa lembrar a súa credencial principal se usar o recoñecemento de desbloqueo avanzado. diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 2b398d74d..9e1d116c0 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -490,7 +490,6 @@ Izbriši ključ za otključavanje uređaja Poveznica za otključavanje uređaja Nije moguće pokrenuti prozor za otključavanje uređaja. - Greška otključavanja uređaja: %1$s Izdvoji podatake za prijavu na bazu podataka pomoću podataka za otključavanje uređaja Nije bilo moguće prepoznati ispis za otključavanje uređaja Nije moguće pročitati ključ za otključavanje uređaja. Izbriši ga i ponovi postupak prepoznavanja otključavanja. diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index fdbd8f4d0..29745db40 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -537,7 +537,6 @@ Írja be a jelszót, majd kattintson erre a gombra. Ideiglenes eszközfeloldás Engedély - Eszközfeloldási hiba: %1$s Tartalom Koppintson az eszközfeloldási kulcsok törléséhez Eszköz hitelesítő adataival történő feloldás diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 44cc0a992..9252f367e 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -582,7 +582,6 @@ HURUF BESAR Huruf Judul Jumlah karakter: %1$d - Terjadi kesalahan buka kunci perangkat: %1$s Tidak dapat menginisialisasi perintah buka kunci perangkat. Bidang tipe huruf Simpan info terbagi diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ff9ee4e93..62d6e90c9 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -515,7 +515,6 @@ Collegamento allo sblocco con dispositivo Credenziali del dispositivo Inserisci la password, poi clicca questo pulsante. - Errore sblocco con dispositivo: %1$s Riconoscimento sblocco con dispositivo Elimina chiave di sblocco del dispositivo Non è possibile ricostruire la lista correttamente. diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 847993b7f..6265d4163 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -539,7 +539,6 @@ חלץ אישור מסד נתונים עם נתוני ביטול נעילת מכשיר לא ניתן לקרוא את מפתח ביטול נעילת המכשיר. נא למחוק אותו ולחזור על התהליך לזיהוי ביטול נעילה. לא היה ניתן לזהות טביעת ביטול נעילת מכשיר - שגיאת ביטול נעילת מכשיר: %1$s הקלד את הסיסמה, ואז לחץ על הכפתור הזה. הצג כפתור נעילה הצג את כפתור הנעילה בממשק המשתמש diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index b625262f9..d873e84c7 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -491,7 +491,6 @@ デバイス認証情報 パスワードを入力し、このボタンをタップします。 デバイスのロック解除プロンプトを初期化できません。 - デバイスのロック解除エラー: %1$s デバイスのロック解除キーを読み取ることができません。削除して、ロック解除認識手順を繰り返してください。 デバイスのロック解除データを使用してデータベースの資格情報を抽出する デバイスのロック解除認識 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 8799cb628..5f1a45faf 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -395,7 +395,6 @@ Konfigūruoti Reikalingas biometrinių duomenų saugumo atnaujinimas. Jei naudojate įrenginio atrakinimo atpažinimą, vis tiek turite prisiminti pagrindinį saugyklos raktą - Įrenginio atrakinimo klaida: %1$s Istorija Nustatymai Temos, spalvos, atributai diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index d2c5989ca..e1f2b28a0 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -405,7 +405,6 @@ Skjul ødelagte lenker i listen over nylige databaser Skjul ødelagte databaselenker Spør om lagring av data - Feil ved opplåsing: %1$s Det anbefales ikke å legge til en tom nøkkelfil. Legg til filen uansett\? Registreringsmodus diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index ab94b3f99..084086457 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -508,7 +508,6 @@ Apparaatreferentie Typ het wachtwoord en klik vervolgens op deze knop. Kan apparaatontgrendeling niet initialiseren. - Fout bij apparaatontgrendeling: %1$s Vingerafdruk niet herkent bij apparaatontgrendeling Kan de sleutel voor apparaatontgrendeling niet lezen. Verwijder deze en herhaal de herkenningsprocedure voor het ontgrendelen. Database uitpakken met gegevens voor apparaatontgrendeling diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 4ef0d73c9..584e5806a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -511,7 +511,6 @@ Stuknij, aby usunąć klucze odblokowywania urządzenia Zawartość Rozpoznawanie odblokowania urządzenia - Błąd odblokowania urządzenia: %1$s Nie można poprawnie odbudować listy. Nie można pobrać identyfikatora URI bazy danych. Dodano sugestie autouzupełniania. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index d129b59de..b5cfe5843 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -512,7 +512,6 @@ Propriedades Digite a senha e clique neste botão. Não foi possível inicializar o prompt de desbloqueio do dispositivo. - Erro de desbloqueio do dispositivo: %1$s Não foi possível reconhecer a impressão de desbloqueio Não é possível ler a chave de desbloqueio do dispositivo. Exclua-o e repita o procedimento de reconhecimento de desbloqueio. Extraia a credencial do banco de dados com os dados de desbloqueio do dispositivo diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index dab9cb67e..f3ff0657d 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -468,7 +468,6 @@ Credencial do dispositivo Digite a palavra-passe e depois clique neste botão. Não foi possível inicializar a solicitação de desbloqueio do dispositivo. - Erro de desbloqueio do dispositivo: %1$s Não foi possível reconhecer a impressão de desbloqueio do dispositivo Não é possível ler a chave de desbloqueio do dispositivo. Elimine-a e repita o procedimento de reconhecimento de desbloqueio. Extrair credencial da base de dados com dados de desbloqueio do dispositivo diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index adaca9784..681590345 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -446,7 +446,6 @@ Aceitar Credencial do dispositivo Não foi possível inicializar a solicitação de desbloqueio do dispositivo. - Erro de desbloqueio do dispositivo: %1$s Não foi possível reconhecer a impressão de desbloqueio do dispositivo Não é possível ler a chave de desbloqueio do dispositivo. Elimine-a e repita o procedimento de reconhecimento de desbloqueio. Extrair credencial da base de dados com dados de desbloqueio do dispositivo diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 881c43b16..56fbf95fd 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -597,7 +597,6 @@ Permisiunea de notificare este necesară pentru a utiliza funcția de notificare a clipboardului. Legătură la deblocarea dispozitivului Recunoașterea deblocării dispozitivului - Eroare de deblocare a dispozitivului: %1$s Legătură de deblocare a dispozitivului Nu se înrolează nicio credențială biometrică sau de dispozitiv. Selectați intrarea… diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b71c2f80a..6951537a9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -498,7 +498,6 @@ Распознавание разблокировки устройства При использовании разблокировки устройства вам всё равно необходимо помнить основные учётные данные. Удалить все ключи шифрования, связанные с распознаванием разблокировки устройства\? - Ошибка разблокировки устройства: %1$s Настройка разблокировки устройства Удалить ключ разблокировки устройства Ввод diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 6b30680fa..f262cb379 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -476,7 +476,6 @@ Téma aplikácie Ochrana Nie sú zaregistrované žiadne biometrické údaje ani poverenia zariadenia. - Chyba odomykania zariadenia: %1$s Zamknúť Vlastné polia Vyberte spôsob triedenia záznamov a skupín. diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index d16944083..d5b433f59 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -344,7 +344,6 @@ Ngjyra zërash Fshihi zërat e skaduar XML e keqformuar. - Gabim shkyçjeje pajisjeje: %1$s Blloko vetëplotësim Lejon prekjen e butoni “Hape”, nëse s’janë përzgjedhur kredenciale Sendërtim për Android i përgjegjësit KeePass të fjalëkalimeve diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 1a2ec8745..91827b298 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -176,7 +176,6 @@ முன் குழுக்கள் குறியாக்க விசை இல்லாமல் தொடரவா? தேர்ந்தெடுக்கப்பட்ட முனைகளை நிரந்தரமாக நீக்கவா? - சாதனம் திறத்தல் பிழை: %1$s தரவுத்தளத்தைத் திறக்க உங்கள் சாதன நற்சான்றிதழைப் பயன்படுத்தலாம் குறியாக்க விசைகளை நீக்கு சாதன திறத்தல் ஏற்பு தொடர்பான அனைத்து குறியாக்க விசைகளையும் நீக்கு diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 8b9531b22..0d8a5cdf6 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -525,7 +525,6 @@ อ่านกุญแจการปลดล็อกของอุปกรณ์ไม่ได้ โปรดลบข้อมูลออกและเพื่มข้อมูลการปลดล็อกด้วยอุปกรณ์อีกครั้ง ไม่รู้จักลายนิ้วมือ แยกข้อมูลประจำตัวออกด้วยข้อมูลการปลดล็อกด้วยอุปกรณ์ - การปลดล็อกด้วยอุปกรณ์ผิดพลาด: %1$s คุณสมบัติ ฐานข้อมูลนี้ยังไม่มีข้อมูลการเข้าสูระบบเลย ประวัติ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index dfe99a4f2..cf8ad0bbf 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -488,7 +488,6 @@ Parolayı yazın ve ardından bu düğmeye tıklayın. Cihaz kilit açma istemi başlatılamıyor. Kullanım dışı - Cihaz kilit açma hatası: %1$s Cihaz kilit açma parmak izi tanınamadı Cihazın kilit açma anahtarı okunamıyor. Lütfen silin ve kilit açma tanıma prosedürünü tekrarlayın. Cihaz kilit açma verileriyle veritabanı kimlik bilgilerini çıkarın diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 61eb1675f..a41f2f3fe 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -493,7 +493,6 @@ Облікові дані пристрою Введіть пароль, а потім натисніть цю кнопку. Не вдалося ініціалізувати запит на розблокування пристрою. - Помилка розблокування пристрою: %1$s Не вдалося розпізнати розблокування пристрою Не вдалося розпізнати ключ розблокування пристрою. Видаліть його й повторіть процедуру створення ключа. Витягування облікових даних бази даних за допомогою даних розблокування пристрою diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 9d56b6ed5..0a3692798 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -400,7 +400,6 @@ Đã lưu trữ mật khẩu được mã hóa Không thể đọc được mã mở khóa thiết bị. Vui lòng xóa nó và lặp lại quy trình nhận dạng mở khóa. Không thể nhận dạng vân tay mở khóa thiết bị - Lỗi mở khóa thiết bị: %1$s Không có sẵn Không thể khởi tạo lời nhắc mở khóa thiết bị. Nhập mật khẩu rồi nhấp vào nút này. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 5892d6de0..ca7a05801 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -492,7 +492,6 @@ 设备凭据 输入密码,然后点击这个按钮。 无法初始化设备解锁提示。 - 设备解锁出错:%1$s 无法识别设备解锁印记 无法读取设备解锁密钥。请删除它,并重复解锁识别步骤。 用设备解锁数据提取数据库凭据 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1166362d8..1d0f6ea78 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -29,7 +29,6 @@ 無法初始化裝置解鎖提示。 即使你使用裝置解鎖識別,你仍然需要記住你的解鎖憑證。 裝置解鎖連線 - 裝置解鎖出錯:%1$s 點擊刪除裝置解鎖密鑰 裝置解鎖超時 允許 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc426d1db..a49a08cee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -407,7 +407,6 @@ Encrypted password stored Cannot read the device unlock key. Please delete it and repeat the unlock recognition procedure. Could not recognize device unlock print - Device unlock error: %1$s Unavailable Unable to initialize device unlock prompt. Type in the password, and then click this button. From 0a0abef4d40e4a42906c57bc481c914373cd9105 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 10 Aug 2025 21:37:41 +0200 Subject: [PATCH 04/26] fix: Keystore exception --- .../com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index bd61d119c..b316f4d84 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -371,7 +371,6 @@ class DeviceUnlockFragment: Fragment() { context.getString(R.string.keystore_not_accessible) )) } - storeCredentialButtonClickListener = null } } } @@ -388,7 +387,6 @@ class DeviceUnlockFragment: Fragment() { context.getString(R.string.keystore_not_accessible) )) } - extractCredentialButtonClickListener = null } } } From f46c062c4e0923bc58981686a80dd4123fb7e704 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 10 Aug 2025 22:34:48 +0200 Subject: [PATCH 05/26] fix: Auto biometric prompt #2105 --- CHANGELOG | 1 + .../kunzisoft/keepass/biometric/DeviceUnlockFragment.kt | 9 ++++----- fastlane/metadata/android/en-US/changelogs/136.txt | 3 ++- fastlane/metadata/android/fr-FR/changelogs/136.txt | 3 ++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b6016826f..d7dc49151 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ KeePassDX(4.1.4) * Fix UnlockManager #2098 #2101 + * Auto device unlock prompt #2105 KeePassDX(4.1.3) * Fix Autofill Registration #2089 diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index b316f4d84..a97138f31 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -288,7 +288,6 @@ class DeviceUnlockFragment: Fragment() { // Auto open the biometric prompt if (mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())) { - mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false openExtractPrompt(cryptoPrompt) } } @@ -418,10 +417,10 @@ class DeviceUnlockFragment: Fragment() { private fun setAuthenticationError(errorCode: Int, errString: CharSequence) { Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") when (errorCode) { - BiometricPrompt.ERROR_NEGATIVE_BUTTON -> - mDeviceUnlockViewModel.setException( - SecurityException(getString(R.string.error_cancel_by_user)) - ) + BiometricPrompt.ERROR_NEGATIVE_BUTTON, + BiometricPrompt.ERROR_USER_CANCELED -> { + // Ignore negative button + } else -> mDeviceUnlockViewModel.setException( SecurityException(errString.toString()) diff --git a/fastlane/metadata/android/en-US/changelogs/136.txt b/fastlane/metadata/android/en-US/changelogs/136.txt index da78f6bc3..74a21bfed 100644 --- a/fastlane/metadata/android/en-US/changelogs/136.txt +++ b/fastlane/metadata/android/en-US/changelogs/136.txt @@ -1 +1,2 @@ - * Fix UnlockManager #2098 #2101 \ No newline at end of file + * Fix UnlockManager #2098 #2101 + * Auto device unlock prompt #2105 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/136.txt b/fastlane/metadata/android/fr-FR/changelogs/136.txt index 4ca55c8b9..9a152a373 100644 --- a/fastlane/metadata/android/fr-FR/changelogs/136.txt +++ b/fastlane/metadata/android/fr-FR/changelogs/136.txt @@ -1 +1,2 @@ - * Correction UnlockManager #2098 #2101 \ No newline at end of file + * Correction UnlockManager #2098 #2101 + * Invite de déverrouillage automatique de l'appareil #2105 \ No newline at end of file From 1369a3cad90c4e1a9a0320481a62af7dd68166ac Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 12 Aug 2025 10:26:32 +0200 Subject: [PATCH 06/26] fix: test dependency and gradle version --- build.gradle | 2 +- crypto/build.gradle | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 211606708..d3cc6d285 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:8.11.0' + classpath 'com.android.tools.build:gradle:8.11.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/crypto/build.gradle b/crypto/build.gradle index adbe03f94..9d538e84e 100644 --- a/crypto/build.gradle +++ b/crypto/build.gradle @@ -42,5 +42,6 @@ dependencies { // Crypto implementation 'org.bouncycastle:bcprov-jdk15on:1.70' + androidTestImplementation "androidx.test:runner:$android_test_version" testImplementation "androidx.test:runner:$android_test_version" } From b7619b45b15cac17afacc6f0e324d74526856829 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 12 Aug 2025 10:51:15 +0200 Subject: [PATCH 07/26] fix: Transition deprecation --- .../activities/FileDatabaseSelectActivity.kt | 1 + .../keepass/activities/stylish/StylishActivity.kt | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt index cfafd0853..2ba2638d3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -316,6 +316,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), private fun launchPasswordActivityWithPath(databaseUri: Uri) { launchPasswordActivity(databaseUri, null, null) // Delete flickering for kitkat <= + @Suppress("DEPRECATION") if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) overridePendingTransition(0, 0) } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt index 0cd9099db..0d4d58f46 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt @@ -22,6 +22,7 @@ package com.kunzisoft.keepass.activities.stylish import android.content.ActivityNotFoundException import android.content.Intent import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper @@ -77,7 +78,18 @@ abstract class StylishActivity : AppCompatActivity() { startActivity(intent) } finish() - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + @Suppress("DEPRECATION") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + overrideActivityTransition( + OVERRIDE_TRANSITION_OPEN, + android.R.anim.fade_in, + android.R.anim.fade_out + ) + else + overridePendingTransition( + android.R.anim.fade_in, + android.R.anim.fade_out + ) } override fun onCreate(savedInstanceState: Bundle?) { From 756454abc3f0839d89e2469c435e283a344d4c1b Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 12 Aug 2025 10:52:42 +0200 Subject: [PATCH 08/26] fix: update CHANGELOG --- CHANGELOG | 1 + fastlane/metadata/android/en-US/changelogs/136.txt | 3 ++- fastlane/metadata/android/fr-FR/changelogs/136.txt | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d7dc49151..4366f2cd9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ KeePassDX(4.1.4) * Fix UnlockManager #2098 #2101 * Auto device unlock prompt #2105 + * Small fixes ##2066 KeePassDX(4.1.3) * Fix Autofill Registration #2089 diff --git a/fastlane/metadata/android/en-US/changelogs/136.txt b/fastlane/metadata/android/en-US/changelogs/136.txt index 74a21bfed..031b8e16d 100644 --- a/fastlane/metadata/android/en-US/changelogs/136.txt +++ b/fastlane/metadata/android/en-US/changelogs/136.txt @@ -1,2 +1,3 @@ * Fix UnlockManager #2098 #2101 - * Auto device unlock prompt #2105 \ No newline at end of file + * Auto device unlock prompt #2105 + * Small fixes ##2066 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/136.txt b/fastlane/metadata/android/fr-FR/changelogs/136.txt index 9a152a373..7c211d24d 100644 --- a/fastlane/metadata/android/fr-FR/changelogs/136.txt +++ b/fastlane/metadata/android/fr-FR/changelogs/136.txt @@ -1,2 +1,3 @@ * Correction UnlockManager #2098 #2101 - * Invite de déverrouillage automatique de l'appareil #2105 \ No newline at end of file + * Invite de déverrouillage automatique de l'appareil #2105 + * Petites corrections ##2066 \ No newline at end of file From 2d398908de551be986ef57f355535849e50798bc Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 12 Aug 2025 11:11:12 +0200 Subject: [PATCH 09/26] fix: refresh when activate setting --- .../keepass/biometric/DeviceUnlockFragment.kt | 1 + .../keepass/viewmodels/DeviceUnlockViewModel.kt | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index a97138f31..012de3a96 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -172,6 +172,7 @@ class DeviceUnlockFragment: Fragment() { override fun onResume() { super.onResume() + mDeviceUnlockViewModel.checkUnlockAvailability() keepConnection = false } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt index 7ab19f20e..2c07838e2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -45,12 +45,11 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat } /** - * Check unlock availability and change the current mode depending of device's state + * Check unlock availability by verifying device settings and database mode */ - fun checkUnlockAvailability(databaseFileUri: Uri?) { - databaseUri = databaseFileUri + fun checkUnlockAvailability() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipherDatabase -> + cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipherDatabase -> if (PreferencesUtil.isBiometricUnlockEnable(getApplication())) { // biometric not supported (by API level or hardware) so keep option hidden // or manually disable @@ -80,6 +79,14 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat } } + /** + * Check unlock availability and change the current mode depending of device's state + */ + fun checkUnlockAvailability(databaseFileUri: Uri?) { + databaseUri = databaseFileUri + checkUnlockAvailability() + } + private fun isModeChanging(newMode: DeviceUnlockMode): Boolean { return _uiState.value.deviceUnlockMode != newMode } From 3cd65345c543722e110bcffb3be824b13bcdb1d1 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 12 Aug 2025 13:04:52 +0200 Subject: [PATCH 10/26] fix: Small biometric fixes --- .../keepass/biometric/DeviceUnlockFragment.kt | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index 012de3a96..b53b8eb95 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -87,28 +87,34 @@ class DeviceUnlockFragment: Fragment() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { // newly store the entered password in encrypted way mDeviceUnlockViewModel.retrieveCredentialForEncryption() + mBiometricPrompt = null } override fun onAuthenticationFailed() { setAuthenticationFailed() + mBiometricPrompt = null } override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { setAuthenticationError(errorCode, errString) + mBiometricPrompt = null } } private var extractAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { mDeviceUnlockViewModel.decryptCredential() + mBiometricPrompt = null } override fun onAuthenticationFailed() { setAuthenticationFailed() + mBiometricPrompt = null } override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { setAuthenticationError(errorCode, errString) + mBiometricPrompt = null } } @@ -176,23 +182,22 @@ class DeviceUnlockFragment: Fragment() { keepConnection = false } - fun openAdvancedUnlockPrompt( + fun openDeviceUnlockPrompt( cryptoPrompt: DeviceUnlockCryptoPrompt, authenticationCallback: BiometricPrompt.AuthenticationCallback ) { - // Init advanced unlock prompt - mBiometricPrompt = BiometricPrompt( - this@DeviceUnlockFragment, - Executors.newSingleThreadExecutor(), - authenticationCallback - ) - val promptTitle = getString(cryptoPrompt.titleId) val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId -> getString(descriptionId) } ?: "" if (cryptoPrompt.isBiometricOperation) { + // Init advanced unlock prompt + mBiometricPrompt = BiometricPrompt( + this@DeviceUnlockFragment, + Executors.newSingleThreadExecutor(), + authenticationCallback + ) val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply { setTitle(promptTitle) if (promptDescription.isNotEmpty()) @@ -227,6 +232,7 @@ class DeviceUnlockFragment: Fragment() { fun closeBiometricPrompt() { mBiometricPrompt?.cancelAuthentication() + mBiometricPrompt = null } private var currentCredentialMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE @@ -250,27 +256,28 @@ class DeviceUnlockFragment: Fragment() { } } - private fun manageEncryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { - if (cryptoPrompt.isDeviceCredentialOperation) { - keepConnection = true - } - storeCredentialButtonClickListener = View.OnClickListener { _ -> - try { - openAdvancedUnlockPrompt( - cryptoPrompt, - storeAuthenticationCallback - ) - } catch (e: Exception) { - Log.e(TAG, "Unable to open encryption prompt", e) - storeCredentialButtonClickListener = null - setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) - } + private fun openEncryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { + try { + openDeviceUnlockPrompt( + cryptoPrompt, + storeAuthenticationCallback + ) + } catch (e: Exception) { + Log.e(TAG, "Unable to open encryption prompt", e) + storeCredentialButtonClickListener = null + setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) } } - private fun openExtractPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { + private fun manageEncryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { + storeCredentialButtonClickListener = View.OnClickListener { _ -> + openEncryptionPrompt(cryptoPrompt) + } + } + + private fun openDecryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { try { - openAdvancedUnlockPrompt( + openDeviceUnlockPrompt( cryptoPrompt, extractAuthenticationCallback ) @@ -284,12 +291,12 @@ class DeviceUnlockFragment: Fragment() { private fun manageDecryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { // Set listener to open the biometric dialog and check credential extractCredentialButtonClickListener = View.OnClickListener { _ -> - openExtractPrompt(cryptoPrompt) + openDecryptionPrompt(cryptoPrompt) } // Auto open the biometric prompt if (mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())) { - openExtractPrompt(cryptoPrompt) + openDecryptionPrompt(cryptoPrompt) } } From dfd7ade416c59ef95231956fbcf4ad8d24dab066 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 12 Aug 2025 13:42:57 +0200 Subject: [PATCH 11/26] fix: Regression #2105 --- .../java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index b53b8eb95..60cf7d0ed 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -296,6 +296,7 @@ class DeviceUnlockFragment: Fragment() { // Auto open the biometric prompt if (mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())) { + mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false openDecryptionPrompt(cryptoPrompt) } } From fe526089d74b44d41e1b6ba1526b384b22868df8 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 12 Aug 2025 14:54:52 +0200 Subject: [PATCH 12/26] fix: auto prompt #2105 --- .../activities/MainCredentialActivity.kt | 16 ++++++++-------- .../activities/legacy/DatabaseLockActivity.kt | 19 +++++++++++-------- .../keepass/biometric/DeviceUnlockFragment.kt | 4 ++-- .../viewmodels/DeviceUnlockViewModel.kt | 1 + 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index d1e23e726..f209b6a75 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -51,7 +51,7 @@ import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode -import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity +import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity.Companion.UI_VISIBLE_DURING_LOCK import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper @@ -265,6 +265,11 @@ class MainCredentialActivity : DatabaseModeActivity() { override fun onResume() { super.onResume() + // Don't allow auto open prompt if lock become when UI visible + if (UI_VISIBLE_DURING_LOCK) { + mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false + } + // Init Biometric elements only if allowed if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && PreferencesUtil.isAdvancedUnlockEnable(this)) { @@ -291,11 +296,6 @@ class MainCredentialActivity : DatabaseModeActivity() { sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION)) } - // Don't allow auto open prompt if lock become when UI visible - if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) { - mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false - } - mDatabaseFileUri?.let { databaseFileUri -> mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri) } @@ -420,6 +420,7 @@ class MainCredentialActivity : DatabaseModeActivity() { // Check if database really loaded if (database.loaded) { clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true) + mDeviceUnlockViewModel.autoPromptAlreadyShown = false GroupActivity.launch(this, database, { onValidateSpecialMode() }, @@ -541,8 +542,7 @@ class MainCredentialActivity : DatabaseModeActivity() { override fun onPause() { // Reinit locking activity UI variable - DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null - + UI_VISIBLE_DURING_LOCK = false super.onPause() } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt index f129a24ed..24c98a0c1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt @@ -47,10 +47,14 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.utils.* +import com.kunzisoft.keepass.utils.LOCK_ACTION +import com.kunzisoft.keepass.utils.LockReceiver +import com.kunzisoft.keepass.utils.closeDatabase +import com.kunzisoft.keepass.utils.registerLockReceiver +import com.kunzisoft.keepass.utils.unregisterLockReceiver import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.viewmodels.NodesViewModel -import java.util.* +import java.util.UUID abstract class DatabaseLockActivity : DatabaseModeActivity(), PasswordEncodingDialogFragment.Listener { @@ -184,8 +188,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), mLockReceiver = LockReceiver { mDatabase = null closeDatabase(database) - if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null) - LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE + UI_VISIBLE_DURING_LOCK = UI_VISIBLE mExitLock = true closeOptionsMenu() finish() @@ -414,7 +417,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), invalidateOptionsMenu() - LOCKING_ACTIVITY_UI_VISIBLE = true + UI_VISIBLE = true } protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) { @@ -429,7 +432,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), } override fun onPause() { - LOCKING_ACTIVITY_UI_VISIBLE = false + UI_VISIBLE = false super.onPause() @@ -481,8 +484,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY" const val TIMEOUT_ENABLE_KEY_DEFAULT = true - private var LOCKING_ACTIVITY_UI_VISIBLE = false - var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null + var UI_VISIBLE: Boolean = false + var UI_VISIBLE_DURING_LOCK: Boolean = false } } diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index 60cf7d0ed..71d44c79b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -74,7 +74,6 @@ class DeviceUnlockFragment: Fragment() { private var mDeviceCredentialResultLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> - mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false // To wait resume if (keepConnection) { mDeviceUnlockViewModel.deviceCredentialAuthSucceeded = @@ -295,8 +294,9 @@ class DeviceUnlockFragment: Fragment() { } // Auto open the biometric prompt if (mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt + && !mDeviceUnlockViewModel.autoPromptAlreadyShown && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())) { - mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false + mDeviceUnlockViewModel.autoPromptAlreadyShown = true openDecryptionPrompt(cryptoPrompt) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt index 2c07838e2..4d9b7d392 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.update class DeviceUnlockViewModel(application: Application): AndroidViewModel(application) { var allowAutoOpenBiometricPrompt : Boolean = true + var autoPromptAlreadyShown : Boolean = false var deviceCredentialAuthSucceeded: Boolean? = null private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null From a680db97070253d3dd2f54335d2773fa3eb3fff5 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 12 Aug 2025 15:20:32 +0200 Subject: [PATCH 13/26] fix: initDecryptData #2105 --- .../keepass/biometric/DeviceUnlockManager.kt | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt index 6f4df8dcd..769838a8f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt @@ -196,15 +196,8 @@ class DeviceUnlockManager(private var appContext: Context) { @Synchronized fun initDecryptData( ivSpecValue: ByteArray, + firstLaunch: Boolean = true, actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit - ) { - initDecryptData(ivSpecValue, actionIfCypherInit, true) - } - - @Synchronized private fun initDecryptData( - ivSpecValue: ByteArray, - actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit, - firstLaunch: Boolean ) { try { // important to restore spec here that was used for decryption @@ -232,7 +225,7 @@ class DeviceUnlockManager(private var appContext: Context) { Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException) if (firstLaunch) { deleteKeystoreKey() - initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch) + initDecryptData(ivSpecValue, false, actionIfCypherInit) } else { throw unrecoverableKeyException } @@ -240,7 +233,7 @@ class DeviceUnlockManager(private var appContext: Context) { Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException) if (firstLaunch) { deleteAllEntryKeysInKeystoreForBiometric(appContext) - initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch) + initDecryptData(ivSpecValue, false, actionIfCypherInit) } else { throw invalidKeyException } From 9cae3f079443ccfb95b36a405fc8ad4a129a771a Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 12 Aug 2025 15:33:35 +0200 Subject: [PATCH 14/26] fix: initDecryptData #2105 --- .../keepass/biometric/DeviceUnlockManager.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt index 769838a8f..4c8ebc1f9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt @@ -134,12 +134,12 @@ class DeviceUnlockManager(private var appContext: Context) { @Synchronized fun initEncryptData( actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit ) { - initEncryptData(actionIfCypherInit, true) + initEncryptData(true, actionIfCypherInit) } @Synchronized private fun initEncryptData( - actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit, - firstLaunch: Boolean + firstLaunch: Boolean, + actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit ) { try { getSecretKey()?.let { secretKey -> @@ -168,7 +168,7 @@ class DeviceUnlockManager(private var appContext: Context) { Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException) if (firstLaunch) { deleteAllEntryKeysInKeystoreForBiometric(appContext) - initEncryptData(actionIfCypherInit, false) + initEncryptData(false, actionIfCypherInit) } else { throw invalidKeyException } @@ -195,6 +195,13 @@ class DeviceUnlockManager(private var appContext: Context) { } @Synchronized fun initDecryptData( + ivSpecValue: ByteArray, + actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit + ) { + initDecryptData(ivSpecValue, true, actionIfCypherInit) + } + + @Synchronized private fun initDecryptData( ivSpecValue: ByteArray, firstLaunch: Boolean = true, actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit From 192d6eedd0afaca01d024e45147e577942e11be6 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 12 Aug 2025 15:51:41 +0200 Subject: [PATCH 15/26] fix: autoprompt thread #2105 --- .../keepass/biometric/DeviceUnlockFragment.kt | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index 71d44c79b..191caad37 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -256,15 +256,17 @@ class DeviceUnlockFragment: Fragment() { } private fun openEncryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { - try { - openDeviceUnlockPrompt( - cryptoPrompt, - storeAuthenticationCallback - ) - } catch (e: Exception) { - Log.e(TAG, "Unable to open encryption prompt", e) - storeCredentialButtonClickListener = null - setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) + lifecycleScope.launch(Dispatchers.Main) { + try { + openDeviceUnlockPrompt( + cryptoPrompt, + storeAuthenticationCallback + ) + } catch (e: Exception) { + Log.e(TAG, "Unable to open encryption prompt", e) + storeCredentialButtonClickListener = null + setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) + } } } @@ -275,15 +277,17 @@ class DeviceUnlockFragment: Fragment() { } private fun openDecryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { - try { - openDeviceUnlockPrompt( - cryptoPrompt, - extractAuthenticationCallback - ) - } catch (e: Exception) { - Log.e(TAG, "Unable to open decryption prompt", e) - extractCredentialButtonClickListener = null - setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) + lifecycleScope.launch(Dispatchers.Main) { + try { + openDeviceUnlockPrompt( + cryptoPrompt, + extractAuthenticationCallback + ) + } catch (e: Exception) { + Log.e(TAG, "Unable to open decryption prompt", e) + extractCredentialButtonClickListener = null + setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) + } } } @@ -295,7 +299,8 @@ class DeviceUnlockFragment: Fragment() { // Auto open the biometric prompt if (mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt && !mDeviceUnlockViewModel.autoPromptAlreadyShown - && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())) { + && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext()) + ) { mDeviceUnlockViewModel.autoPromptAlreadyShown = true openDecryptionPrompt(cryptoPrompt) } From c8c232639fe785f794943c9431f38725d6e794d2 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 12 Aug 2025 19:49:31 +0200 Subject: [PATCH 16/26] fix: better cipher and prompt workflow #2105 --- .../activities/MainCredentialActivity.kt | 6 +- .../keepass/biometric/DeviceUnlockFragment.kt | 253 ++++++++---------- .../keepass/biometric/DeviceUnlockManager.kt | 2 + .../viewmodels/DeviceUnlockViewModel.kt | 66 +++-- 4 files changed, 155 insertions(+), 172 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index f209b6a75..330c9c678 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -235,9 +235,10 @@ class MainCredentialActivity : DatabaseModeActivity() { mDeviceUnlockViewModel.uiState.collect { uiState -> // New value received if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (uiState.isCredentialRequired) { + uiState.cipherCredentialRequired?.let { cipher -> mDeviceUnlockViewModel.encryptCredential( - getCredentialForEncryption() + credential = getCredentialForEncryption(), + cipher = cipher ) } } @@ -420,7 +421,6 @@ class MainCredentialActivity : DatabaseModeActivity() { // Check if database really loaded if (database.loaded) { clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true) - mDeviceUnlockViewModel.autoPromptAlreadyShown = false GroupActivity.launch(this, database, { onValidateSpecialMode() }, diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index 191caad37..265b33b22 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -44,14 +44,15 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.view.DeviceUnlockView import com.kunzisoft.keepass.view.hideByFading import com.kunzisoft.keepass.view.showByFading +import com.kunzisoft.keepass.viewmodels.DeviceUnlockPromptMode 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() { @@ -65,55 +66,51 @@ class DeviceUnlockFragment: Fragment() { // Only to fix multiple fingerprint menu #332 private var mAllowAdvancedUnlockMenu = false - // Only keep connection when we request a device credential activity - private var keepConnection = false - - private var storeCredentialButtonClickListener: View.OnClickListener? = null - private var extractCredentialButtonClickListener: View.OnClickListener? = null - - private var mDeviceCredentialResultLauncher = registerForActivityResult( + private var mDeviceCredentialEncryptionResultLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> - // To wait resume - if (keepConnection) { - mDeviceUnlockViewModel.deviceCredentialAuthSucceeded = - result.resultCode == Activity.RESULT_OK + if (result.resultCode == Activity.RESULT_OK) { + // TODO onEncryptionPromptSucceeded() + } else { + setAuthenticationFailed() } - keepConnection = false } - private var storeAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() { + private var mDeviceCredentialDecryptionResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + // TODO onDecryptionPromptSucceeded() + } else { + setAuthenticationFailed() + } + } + + private var encryptionAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - // newly store the entered password in encrypted way - mDeviceUnlockViewModel.retrieveCredentialForEncryption() - mBiometricPrompt = null + onEncryptionPromptSucceeded(result.cryptoObject?.cipher) } override fun onAuthenticationFailed() { setAuthenticationFailed() - mBiometricPrompt = null } override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { setAuthenticationError(errorCode, errString) - mBiometricPrompt = null } } - private var extractAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() { + private var decryptionAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - mDeviceUnlockViewModel.decryptCredential() - mBiometricPrompt = null + onDecryptionPromptSucceeded(result.cryptoObject?.cipher) } override fun onAuthenticationFailed() { setAuthenticationFailed() - mBiometricPrompt = null } override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { setAuthenticationError(errorCode, errString) - mBiometricPrompt = null } } @@ -154,19 +151,10 @@ class DeviceUnlockFragment: Fragment() { // Change mode toggleDeviceCredentialMode(uiState.deviceUnlockMode) // Prompt - uiState.cryptoPrompt?.let { prompt -> - mDeviceUnlockViewModel.promptShown() - when (prompt.type) { - DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION -> - manageEncryptionPrompt(prompt) - DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION -> - manageDecryptionPrompt(prompt) - } - } - if (uiState.closePromptRequested) { - closeBiometricPrompt() - mDeviceUnlockViewModel.biometricPromptClosed() - } + manageDeviceCredentialPrompt( + uiState.cryptoPrompt, + uiState.cryptoPromptState + ) // Advanced menu mAllowAdvancedUnlockMenu = uiState.allowAdvancedUnlockMenu activity?.invalidateOptionsMenu() @@ -178,55 +166,6 @@ class DeviceUnlockFragment: Fragment() { override fun onResume() { super.onResume() mDeviceUnlockViewModel.checkUnlockAvailability() - keepConnection = false - } - - fun openDeviceUnlockPrompt( - cryptoPrompt: DeviceUnlockCryptoPrompt, - authenticationCallback: BiometricPrompt.AuthenticationCallback - ) { - val promptTitle = getString(cryptoPrompt.titleId) - val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId -> - getString(descriptionId) - } ?: "" - - if (cryptoPrompt.isBiometricOperation) { - // Init advanced unlock prompt - mBiometricPrompt = BiometricPrompt( - this@DeviceUnlockFragment, - Executors.newSingleThreadExecutor(), - authenticationCallback - ) - 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.CryptoObject(cryptoPrompt.cipher)) - } - else if (cryptoPrompt.isDeviceCredentialOperation) { - context?.let { context -> - val keyGuardManager = ContextCompat.getSystemService( - context, - KeyguardManager::class.java - ) - @Suppress("DEPRECATION") - mDeviceCredentialResultLauncher.launch( - keyGuardManager?.createConfirmDeviceCredentialIntent( - promptTitle, - promptDescription - ) - ) - } - } } fun closeBiometricPrompt() { @@ -255,57 +194,88 @@ class DeviceUnlockFragment: Fragment() { } } - private fun openEncryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { - lifecycleScope.launch(Dispatchers.Main) { - try { - openDeviceUnlockPrompt( - cryptoPrompt, - storeAuthenticationCallback - ) - } catch (e: Exception) { - Log.e(TAG, "Unable to open encryption prompt", e) - storeCredentialButtonClickListener = null - setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) + private fun manageDeviceCredentialPrompt( + cryptoPrompt: DeviceUnlockCryptoPrompt?, + 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 -> { + openPrompt(cryptoPrompt) + mDeviceUnlockViewModel.promptShown() + } + DeviceUnlockPromptMode.CLOSE -> { + closeBiometricPrompt() + mDeviceUnlockViewModel.biometricPromptClosed() + } } } } - private fun manageEncryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { - storeCredentialButtonClickListener = View.OnClickListener { _ -> - openEncryptionPrompt(cryptoPrompt) - } - } - - private fun openDecryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { + private fun openPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { lifecycleScope.launch(Dispatchers.Main) { try { - openDeviceUnlockPrompt( - cryptoPrompt, - extractAuthenticationCallback - ) + val promptTitle = getString(cryptoPrompt.titleId) + val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId -> + getString(descriptionId) + } ?: "" + + 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.CryptoObject(cryptoPrompt.cipher)) + } + 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( + promptTitle, + promptDescription + ) + ) + } + } } catch (e: Exception) { - Log.e(TAG, "Unable to open decryption prompt", e) - extractCredentialButtonClickListener = null + Log.e(TAG, "Unable to open prompt", e) setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) } } } - private fun manageDecryptionPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { - // Set listener to open the biometric dialog and check credential - extractCredentialButtonClickListener = View.OnClickListener { _ -> - openDecryptionPrompt(cryptoPrompt) - } - // Auto open the biometric prompt - if (mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt - && !mDeviceUnlockViewModel.autoPromptAlreadyShown - && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext()) - ) { - mDeviceUnlockViewModel.autoPromptAlreadyShown = true - openDecryptionPrompt(cryptoPrompt) - } - } - private fun setNotAvailableMode() { lifecycleScope.launch(Dispatchers.Main) { showViews(false) @@ -379,11 +349,7 @@ class DeviceUnlockFragment: Fragment() { setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric) context?.let { context -> mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { view -> - storeCredentialButtonClickListener?.onClick(view) ?: run { - mDeviceUnlockViewModel.setException(SecurityException( - context.getString(R.string.keystore_not_accessible) - )) - } + mDeviceUnlockViewModel.showPrompt() } } } @@ -395,11 +361,7 @@ class DeviceUnlockFragment: Fragment() { setAdvancedUnlockedTitleView(R.string.unlock) context?.let { context -> mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { view -> - extractCredentialButtonClickListener?.onClick(view) ?: run { - mDeviceUnlockViewModel.setException(SecurityException( - context.getString(R.string.keystore_not_accessible) - )) - } + mDeviceUnlockViewModel.showPrompt() } } } @@ -428,9 +390,21 @@ 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, BiometricPrompt.ERROR_USER_CANCELED -> { // Ignore negative button @@ -444,19 +418,12 @@ class DeviceUnlockFragment: Fragment() { private fun setAuthenticationFailed() { Log.e(TAG, "Biometric authentication failed, biometric not recognized") + mBiometricPrompt = null mDeviceUnlockViewModel.setException(SecurityException( getString(R.string.advanced_unlock_not_recognized)) ) } - override fun onPause() { - if (!keepConnection) { - // If close prompt, bug "user not authenticated in Android R" - mDeviceUnlockViewModel.disconnect() - } - super.onPause() - } - override fun onDestroyView() { mDeviceUnlockView = null super.onDestroyView() diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt index 4c8ebc1f9..534e90484 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt @@ -180,6 +180,7 @@ class DeviceUnlockManager(private var appContext: Context) { @Synchronized fun encryptData( value: ByteArray, + cipher: Cipher?, handleEncryptedResult: (encryptedValue: ByteArray, ivSpec: ByteArray) -> Unit ) { try { @@ -252,6 +253,7 @@ class DeviceUnlockManager(private var appContext: Context) { @Synchronized fun decryptData( encryptedValue: ByteArray, + cipher: Cipher?, handleDecryptedResult: (decryptedValue: ByteArray) -> Unit ) { try { diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt index 4d9b7d392..b1ee5e3a0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -18,12 +18,11 @@ import com.kunzisoft.keepass.settings.PreferencesUtil import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import javax.crypto.Cipher class DeviceUnlockViewModel(application: Application): AndroidViewModel(application) { var allowAutoOpenBiometricPrompt : Boolean = true - var autoPromptAlreadyShown : Boolean = false - var deviceCredentialAuthSucceeded: Boolean? = null private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null @@ -152,40 +151,31 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat // To get device credential unlock result, only if same database uri if (databaseUri != null && PreferencesUtil.isAdvancedUnlockEnable(getApplication())) { - deviceCredentialAuthSucceeded?.let { authSucceeded -> - if (databaseUri == this.databaseUri) { - if (authSucceeded) { - retrieveCredentialForEncryption() - } - } else { - disconnect() - } - } ?: run { - if (databaseUri != this.databaseUri) { - connect(databaseUri) - } + if (databaseUri != this.databaseUri) { + connect(databaseUri) } } else { disconnect() } - deviceCredentialAuthSucceeded = null } - fun retrieveCredentialForEncryption() { + fun retrieveCredentialForEncryption(cipher: Cipher?) { _uiState.update { currentState -> currentState.copy( - isCredentialRequired = true + cipherCredentialRequired = cipher ) } } @RequiresApi(Build.VERSION_CODES.M) fun encryptCredential( - credential: ByteArray + credential: ByteArray, + cipher: Cipher? ) { try { deviceUnlockManager?.encryptData( value = credential, + cipher = cipher, handleEncryptedResult = { encryptedValue, ivSpec -> databaseUri?.let { databaseUri -> onCredentialEncrypted( @@ -204,13 +194,13 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat } _uiState.update { currentState -> currentState.copy( - isCredentialRequired = false + cipherCredentialRequired = null ) } } @RequiresApi(Build.VERSION_CODES.M) - fun decryptCredential() { + fun decryptCredential(cipher: Cipher?) { // retrieve the encrypted value from preferences databaseUri?.let { databaseUri -> cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase -> @@ -218,6 +208,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat try { deviceUnlockManager?.decryptData( encryptedValue = encryptedCredential, + cipher = cipher, handleDecryptedResult = { decryptedValue -> // Load database directly with password retrieve onCredentialDecrypted( @@ -280,10 +271,28 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat } } + fun checkAutoOpenPrompt() { + // Auto open the biometric prompt + if (allowAutoOpenBiometricPrompt + && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(getApplication()) + ) { + if (uiState.value.cryptoPrompt != null) + showPrompt() + } + } + + fun showPrompt() { + _uiState.update { currentState -> + currentState.copy( + cryptoPromptState = DeviceUnlockPromptMode.SHOW + ) + } + } + fun promptShown() { _uiState.update { currentState -> currentState.copy( - cryptoPrompt = null + cryptoPromptState = DeviceUnlockPromptMode.IDLE ) } } @@ -323,6 +332,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat try { deviceUnlockManager?.initDecryptData(cipherDatabase.specParameters) { cryptoPrompt -> onPromptRequested(cryptoPrompt) + checkAutoOpenPrompt() } ?: setException(Exception("AdvancedUnlockManager not initialized")) } catch (e: Exception) { setException(e) @@ -363,8 +373,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat fun closeBiometricPrompt() { _uiState.update { currentState -> currentState.copy( - cryptoPrompt = null, - closePromptRequested = true + cryptoPromptState = DeviceUnlockPromptMode.CLOSE ) } } @@ -372,7 +381,8 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat fun biometricPromptClosed() { _uiState.update { currentState -> currentState.copy( - closePromptRequested = false + cryptoPrompt = null, + cryptoPromptState = DeviceUnlockPromptMode.SHOW ) } } @@ -392,13 +402,17 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat } } +enum class DeviceUnlockPromptMode { + IDLE, SHOW, CLOSE +} + data class DeviceUnlockState( val deviceUnlockMode: DeviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE, val allowAdvancedUnlockMenu: Boolean = false, - val isCredentialRequired: Boolean = false, + val cipherCredentialRequired: Cipher? = null, val cipherEncryptDatabase: CipherEncryptDatabase? = null, val cipherDecryptDatabase: CipherDecryptDatabase? = null, val cryptoPrompt: DeviceUnlockCryptoPrompt? = null, - val closePromptRequested: Boolean = false, + val cryptoPromptState: DeviceUnlockPromptMode = DeviceUnlockPromptMode.IDLE, val exception: Exception? = null ) \ No newline at end of file From df3bd7e0a11523ea2effb848fa4e8147f4ae8ef7 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 13 Aug 2025 11:03:50 +0200 Subject: [PATCH 17/26] fix: cipher call #2105 --- .../activities/MainCredentialActivity.kt | 2 +- .../keepass/biometric/DeviceUnlockFragment.kt | 121 +++++------------- .../viewmodels/DeviceUnlockViewModel.kt | 55 ++++++-- 3 files changed, 79 insertions(+), 99 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index 330c9c678..03aefd3c6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -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 diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index 265b33b22..2499a5d18 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -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? = 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)) ) } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt index b1ee5e3a0..b3f9208d2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -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, From 6de02384c1df61a45cdf3029c1b92a567f70ac0b Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 13 Aug 2025 11:25:36 +0200 Subject: [PATCH 18/26] fix: close prompt #2105 --- .../keepass/biometric/DeviceUnlockFragment.kt | 73 +++++++++---------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index 2499a5d18..bf86643c2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -151,9 +151,8 @@ class DeviceUnlockFragment: Fragment() { mDeviceUnlockViewModel.checkUnlockAvailability() } - fun destroyBiometricPrompt() { + fun cancelBiometricPrompt() { mBiometricPrompt?.cancelAuthentication() - mBiometricPrompt = null } private var currentCredentialMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE @@ -189,7 +188,7 @@ class DeviceUnlockFragment: Fragment() { mDeviceUnlockViewModel.promptShown() } DeviceUnlockPromptMode.CLOSE -> { - destroyBiometricPrompt() + cancelBiometricPrompt() mDeviceUnlockViewModel.biometricPromptClosed() } } @@ -197,45 +196,43 @@ class DeviceUnlockFragment: Fragment() { } private fun openPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { - lifecycleScope.launch(Dispatchers.Main) { - try { - val promptTitle = getString(cryptoPrompt.titleId) - val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId -> - getString(descriptionId) - } ?: "" + try { + val promptTitle = getString(cryptoPrompt.titleId) + val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId -> + getString(descriptionId) + } ?: "" - if (cryptoPrompt.isBiometricOperation) { - mBiometricPrompt?.authenticate( - 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) { - context?.let { context -> - @Suppress("DEPRECATION") - mDeviceCredentialResultLauncher?.launch( - ContextCompat.getSystemService( - context, - KeyguardManager::class.java - )?.createConfirmDeviceCredentialIntent( - promptTitle, - promptDescription - ) + if (cryptoPrompt.isBiometricOperation) { + mBiometricPrompt?.authenticate( + 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) { + context?.let { context -> + @Suppress("DEPRECATION") + mDeviceCredentialResultLauncher?.launch( + ContextCompat.getSystemService( + context, + KeyguardManager::class.java + )?.createConfirmDeviceCredentialIntent( + promptTitle, + promptDescription ) - } + ) } - } catch (e: Exception) { - Log.e(TAG, "Unable to open prompt", e) - mDeviceUnlockViewModel.setException(e) } + } catch (e: Exception) { + Log.e(TAG, "Unable to open prompt", e) + mDeviceUnlockViewModel.setException(e) } } From 698e3b7fb110187384125ee6fcb96b56411a6b48 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 13 Aug 2025 11:49:54 +0200 Subject: [PATCH 19/26] fix: auto open prompt #2105 --- .../activities/MainCredentialActivity.kt | 7 +---- .../keepass/biometric/DeviceUnlockFragment.kt | 12 +++++++++ .../viewmodels/DeviceUnlockViewModel.kt | 26 +++++++++---------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index 03aefd3c6..c13d6c859 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -256,8 +256,8 @@ class MainCredentialActivity : DatabaseModeActivity() { deviceUnlockError(error, this@MainCredentialActivity), Snackbar.LENGTH_LONG ).asError().show() + mDeviceUnlockViewModel.exceptionShown() } - mDeviceUnlockViewModel.exceptionShown() } } } @@ -266,11 +266,6 @@ class MainCredentialActivity : DatabaseModeActivity() { override fun onResume() { super.onResume() - // Don't allow auto open prompt if lock become when UI visible - if (UI_VISIBLE_DURING_LOCK) { - mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false - } - // Init Biometric elements only if allowed if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && PreferencesUtil.isAdvancedUnlockEnable(this)) { diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index bf86643c2..f68e8f700 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -45,6 +45,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity.Companion.UI_VISIBLE_DURING_LOCK import com.kunzisoft.keepass.view.DeviceUnlockView import com.kunzisoft.keepass.view.hideByFading import com.kunzisoft.keepass.view.showByFading @@ -148,6 +149,12 @@ class DeviceUnlockFragment: Fragment() { override fun onResume() { super.onResume() + + // Don't allow auto open prompt if lock become when UI visible + if (UI_VISIBLE_DURING_LOCK) { + mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false + } + mDeviceUnlockViewModel.checkUnlockAvailability() } @@ -370,6 +377,11 @@ class DeviceUnlockFragment: Fragment() { ) } + override fun onPause() { + super.onPause() + mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = true + } + override fun onDestroyView() { mDeviceUnlockView = null super.onDestroyView() diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt index b3f9208d2..a86efd511 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -295,23 +295,22 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat } } - fun onPromptRequested(cryptoPrompt: DeviceUnlockCryptoPrompt) { + fun onPromptRequested( + cryptoPrompt: DeviceUnlockCryptoPrompt, + autoOpen: Boolean = false + ) { _uiState.update { currentState -> currentState.copy( - cryptoPrompt = cryptoPrompt + cryptoPrompt = cryptoPrompt, + cryptoPromptState = if ( + autoOpen + && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(getApplication())) + DeviceUnlockPromptMode.SHOW + else uiState.value.cryptoPromptState ) } } - fun checkAutoOpenPrompt() { - // Auto open the biometric prompt - if (allowAutoOpenBiometricPrompt - && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(getApplication()) - ) { - showPrompt() - } - } - fun showPrompt() { _uiState.update { currentState -> currentState.copy( @@ -321,6 +320,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat } fun promptShown() { + allowAutoOpenBiometricPrompt = false _uiState.update { currentState -> currentState.copy( cryptoPromptState = DeviceUnlockPromptMode.IDLE @@ -362,8 +362,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat cipherDatabase?.let { try { deviceUnlockManager?.initDecryptData(cipherDatabase.specParameters) { cryptoPrompt -> - onPromptRequested(cryptoPrompt) - checkAutoOpenPrompt() + onPromptRequested(cryptoPrompt, autoOpen = allowAutoOpenBiometricPrompt) } ?: setException(Exception("AdvancedUnlockManager not initialized")) } catch (e: Exception) { setException(e) @@ -445,5 +444,6 @@ data class DeviceUnlockState( val cipherDecryptDatabase: CipherDecryptDatabase? = null, val cryptoPrompt: DeviceUnlockCryptoPrompt? = null, val cryptoPromptState: DeviceUnlockPromptMode = DeviceUnlockPromptMode.IDLE, + val autoOpenPrompt: Boolean = false, val exception: Exception? = null ) \ No newline at end of file From 04bcc6631cf766e123d00fbc018128f8f19add08 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 13 Aug 2025 17:59:56 +0200 Subject: [PATCH 20/26] fix: open auto prompt too often #2105 --- .../kunzisoft/keepass/activities/MainCredentialActivity.kt | 2 ++ .../com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt | 5 ----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index c13d6c859..567d3752f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -87,6 +87,7 @@ import com.kunzisoft.keepass.view.MainCredentialView import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel +import com.kunzisoft.keepass.viewmodels.DeviceUnlockState import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel import kotlinx.coroutines.launch import java.io.FileNotFoundException @@ -415,6 +416,7 @@ class MainCredentialActivity : DatabaseModeActivity() { private fun launchGroupActivityIfLoaded(database: ContextualDatabase) { // Check if database really loaded if (database.loaded) { + mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = true clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true) GroupActivity.launch(this, database, diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index f68e8f700..7dd3e9509 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -377,11 +377,6 @@ class DeviceUnlockFragment: Fragment() { ) } - override fun onPause() { - super.onPause() - mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = true - } - override fun onDestroyView() { mDeviceUnlockView = null super.onDestroyView() From da0f02e536700c8050c12e68ddd7ba0cef83bf4e Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 13 Aug 2025 18:27:51 +0200 Subject: [PATCH 21/26] fix: better prompt variable management #2105 --- .../keepass/biometric/DeviceUnlockFragment.kt | 42 +++++++++---------- .../viewmodels/DeviceUnlockViewModel.kt | 22 ++++++---- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index 7dd3e9509..5814623f3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -133,10 +133,12 @@ class DeviceUnlockFragment: Fragment() { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { mDeviceUnlockViewModel.uiState.collect { uiState -> // Change mode - toggleDeviceCredentialMode(uiState.deviceUnlockMode) + toggleDeviceCredentialMode( + uiState.newDeviceUnlockMode, + uiState.deviceUnlockModeChange + ) // Prompt manageDeviceCredentialPrompt( - uiState.cryptoPrompt, uiState.cryptoPromptState ) // Advanced menu @@ -162,36 +164,32 @@ class DeviceUnlockFragment: Fragment() { mBiometricPrompt?.cancelAuthentication() } - private var currentCredentialMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE - private fun toggleDeviceCredentialMode(deviceUnlockMode: DeviceUnlockMode) { - if (currentCredentialMode == deviceUnlockMode) { - return - } - currentCredentialMode = deviceUnlockMode - try { - when (deviceUnlockMode) { - DeviceUnlockMode.BIOMETRIC_UNAVAILABLE -> setNotAvailableMode() - DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> setSecurityUpdateRequiredMode() - DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> setNotConfiguredMode() - DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE -> setKeyManagerNotAvailableMode() - DeviceUnlockMode.WAIT_CREDENTIAL -> setWaitCredentialMode() - DeviceUnlockMode.STORE_CREDENTIAL -> setStoreCredentialMode() - DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode() + private fun toggleDeviceCredentialMode(deviceUnlockMode: DeviceUnlockMode, modeChanged: Boolean) { + if (modeChanged) { + try { + when (deviceUnlockMode) { + DeviceUnlockMode.BIOMETRIC_UNAVAILABLE -> setNotAvailableMode() + DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> setSecurityUpdateRequiredMode() + DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> setNotConfiguredMode() + DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE -> setKeyManagerNotAvailableMode() + DeviceUnlockMode.WAIT_CREDENTIAL -> setWaitCredentialMode() + DeviceUnlockMode.STORE_CREDENTIAL -> setStoreCredentialMode() + DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode() + } + } catch (e: Exception) { + mDeviceUnlockViewModel.setException(e) } - } catch (e: Exception) { - mDeviceUnlockViewModel.setException(e) } } private fun manageDeviceCredentialPrompt( - cryptoPrompt: DeviceUnlockCryptoPrompt?, state: DeviceUnlockPromptMode ) { - cryptoPrompt?.let { + mDeviceUnlockViewModel.cryptoPrompt?.let { prompt -> when (state) { DeviceUnlockPromptMode.IDLE -> {} DeviceUnlockPromptMode.SHOW -> { - openPrompt(cryptoPrompt) + openPrompt(prompt) mDeviceUnlockViewModel.promptShown() } DeviceUnlockPromptMode.CLOSE -> { diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt index a86efd511..a428e13a1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -34,6 +34,9 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat private var deviceUnlockManager: DeviceUnlockManager? = null private var databaseUri: Uri? = null + private var deviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE + var cryptoPrompt: DeviceUnlockCryptoPrompt? = null + // TODO Retrieve credential storage from app database var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT @@ -91,7 +94,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat } private fun isModeChanging(newMode: DeviceUnlockMode): Boolean { - return _uiState.value.deviceUnlockMode != newMode + return deviceUnlockMode != newMode } @RequiresApi(Build.VERSION_CODES.M) @@ -165,7 +168,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat fun onAuthenticationSucceeded( activityResult: ActivityResult ) { - uiState.value.cryptoPrompt?.let { prompt -> + cryptoPrompt?.let { prompt -> when (prompt.type) { DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION -> retrieveCredentialForEncryption( prompt.cipher) @@ -179,7 +182,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat fun onAuthenticationSucceeded( result: BiometricPrompt.AuthenticationResult ) { - uiState.value.cryptoPrompt?.type?.let { type -> + cryptoPrompt?.type?.let { type -> when (type) { DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION -> retrieveCredentialForEncryption(result.cryptoObject?.cipher) @@ -299,9 +302,9 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat cryptoPrompt: DeviceUnlockCryptoPrompt, autoOpen: Boolean = false ) { + this@DeviceUnlockViewModel.cryptoPrompt = cryptoPrompt _uiState.update { currentState -> currentState.copy( - cryptoPrompt = cryptoPrompt, cryptoPromptState = if ( autoOpen && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(getApplication())) @@ -374,10 +377,13 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat @RequiresApi(Build.VERSION_CODES.M) private fun changeMode(deviceUnlockMode: DeviceUnlockMode) { + val modeChanged = this.deviceUnlockMode == deviceUnlockMode + this.deviceUnlockMode = deviceUnlockMode cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher -> _uiState.update { currentState -> currentState.copy( - deviceUnlockMode = deviceUnlockMode, + newDeviceUnlockMode = deviceUnlockMode, + deviceUnlockModeChange = modeChanged, allowAdvancedUnlockMenu = containsCipher && deviceUnlockMode != DeviceUnlockMode.BIOMETRIC_UNAVAILABLE && deviceUnlockMode != DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE @@ -409,9 +415,9 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat } fun biometricPromptClosed() { + cryptoPrompt = null _uiState.update { currentState -> currentState.copy( - cryptoPrompt = null, cryptoPromptState = DeviceUnlockPromptMode.IDLE ) } @@ -437,12 +443,12 @@ enum class DeviceUnlockPromptMode { } data class DeviceUnlockState( - val deviceUnlockMode: DeviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE, + val newDeviceUnlockMode: DeviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE, + val deviceUnlockModeChange: Boolean = true, val allowAdvancedUnlockMenu: Boolean = false, val credentialRequiredCipher: Cipher? = null, val cipherEncryptDatabase: CipherEncryptDatabase? = null, val cipherDecryptDatabase: CipherDecryptDatabase? = null, - val cryptoPrompt: DeviceUnlockCryptoPrompt? = null, val cryptoPromptState: DeviceUnlockPromptMode = DeviceUnlockPromptMode.IDLE, val autoOpenPrompt: Boolean = false, val exception: Exception? = null From c12eb3d643c8a2577529e0dadb57a3496e11524f Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 13 Aug 2025 19:03:15 +0200 Subject: [PATCH 22/26] fix: error code 28 #2105 --- .../keepass/biometric/DeviceUnlockFragment.kt | 35 ++++++++----------- .../viewmodels/DeviceUnlockViewModel.kt | 17 ++------- 2 files changed, 16 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index 5814623f3..e0faaffd7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -133,14 +133,9 @@ class DeviceUnlockFragment: Fragment() { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { mDeviceUnlockViewModel.uiState.collect { uiState -> // Change mode - toggleDeviceCredentialMode( - uiState.newDeviceUnlockMode, - uiState.deviceUnlockModeChange - ) + toggleDeviceCredentialMode(uiState.newDeviceUnlockMode) // Prompt - manageDeviceCredentialPrompt( - uiState.cryptoPromptState - ) + manageDeviceCredentialPrompt(uiState.cryptoPromptState) // Advanced menu mAllowAdvancedUnlockMenu = uiState.allowAdvancedUnlockMenu activity?.invalidateOptionsMenu() @@ -164,21 +159,19 @@ class DeviceUnlockFragment: Fragment() { mBiometricPrompt?.cancelAuthentication() } - private fun toggleDeviceCredentialMode(deviceUnlockMode: DeviceUnlockMode, modeChanged: Boolean) { - if (modeChanged) { - try { - when (deviceUnlockMode) { - DeviceUnlockMode.BIOMETRIC_UNAVAILABLE -> setNotAvailableMode() - DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> setSecurityUpdateRequiredMode() - DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> setNotConfiguredMode() - DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE -> setKeyManagerNotAvailableMode() - DeviceUnlockMode.WAIT_CREDENTIAL -> setWaitCredentialMode() - DeviceUnlockMode.STORE_CREDENTIAL -> setStoreCredentialMode() - DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode() - } - } catch (e: Exception) { - mDeviceUnlockViewModel.setException(e) + private fun toggleDeviceCredentialMode(deviceUnlockMode: DeviceUnlockMode) { + try { + when (deviceUnlockMode) { + DeviceUnlockMode.BIOMETRIC_UNAVAILABLE -> setNotAvailableMode() + DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> setSecurityUpdateRequiredMode() + DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> setNotConfiguredMode() + DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE -> setKeyManagerNotAvailableMode() + DeviceUnlockMode.WAIT_CREDENTIAL -> setWaitCredentialMode() + DeviceUnlockMode.STORE_CREDENTIAL -> setStoreCredentialMode() + DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode() } + } catch (e: Exception) { + mDeviceUnlockViewModel.setException(e) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt index a428e13a1..e5694aab5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -93,26 +93,16 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat checkUnlockAvailability() } - private fun isModeChanging(newMode: DeviceUnlockMode): Boolean { - return deviceUnlockMode != newMode - } - @RequiresApi(Build.VERSION_CODES.M) fun selectMode(containsCipherDatabase: Boolean) { try { if (isConditionToStoreCredentialVerified) { - if (deviceUnlockManager == null - || isModeChanging(DeviceUnlockMode.STORE_CREDENTIAL)) { - deviceUnlockManager = DeviceUnlockManager(getApplication()) - } + deviceUnlockManager = DeviceUnlockManager(getApplication()) // listen for encryption changeMode(DeviceUnlockMode.STORE_CREDENTIAL) initEncryptData() } else if (containsCipherDatabase) { - if (deviceUnlockManager == null - || isModeChanging(DeviceUnlockMode.EXTRACT_CREDENTIAL)) { - deviceUnlockManager = DeviceUnlockManager(getApplication()) - } + deviceUnlockManager = DeviceUnlockManager(getApplication()) // biometric available but no stored password found yet for this DB // listen for decryption changeMode(DeviceUnlockMode.EXTRACT_CREDENTIAL) @@ -377,13 +367,11 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat @RequiresApi(Build.VERSION_CODES.M) private fun changeMode(deviceUnlockMode: DeviceUnlockMode) { - val modeChanged = this.deviceUnlockMode == deviceUnlockMode this.deviceUnlockMode = deviceUnlockMode cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher -> _uiState.update { currentState -> currentState.copy( newDeviceUnlockMode = deviceUnlockMode, - deviceUnlockModeChange = modeChanged, allowAdvancedUnlockMenu = containsCipher && deviceUnlockMode != DeviceUnlockMode.BIOMETRIC_UNAVAILABLE && deviceUnlockMode != DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE @@ -444,7 +432,6 @@ enum class DeviceUnlockPromptMode { data class DeviceUnlockState( val newDeviceUnlockMode: DeviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE, - val deviceUnlockModeChange: Boolean = true, val allowAdvancedUnlockMenu: Boolean = false, val credentialRequiredCipher: Cipher? = null, val cipherEncryptDatabase: CipherEncryptDatabase? = null, From 7a9469e59d63c9235b051058d580437bf19000e6 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 13 Aug 2025 19:21:42 +0200 Subject: [PATCH 23/26] fix: code improvement #2105 --- .../keepass/viewmodels/DeviceUnlockViewModel.kt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt index e5694aab5..7d09aac11 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -293,15 +293,8 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat autoOpen: Boolean = false ) { this@DeviceUnlockViewModel.cryptoPrompt = cryptoPrompt - _uiState.update { currentState -> - currentState.copy( - cryptoPromptState = if ( - autoOpen - && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(getApplication())) - DeviceUnlockPromptMode.SHOW - else uiState.value.cryptoPromptState - ) - } + if (autoOpen && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(getApplication())) + showPrompt() } fun showPrompt() { From 9e1f6d29a57c8d7ab76337184840bffd88428d12 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 14 Aug 2025 12:28:22 +0200 Subject: [PATCH 24/26] fix: update Gemfile.lock --- Gemfile.lock | 75 ++++++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e806840ad..d5b03e6cc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,31 +9,35 @@ GEM public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.1009.0) - aws-sdk-core (3.213.0) + aws-eventstream (1.4.0) + aws-partitions (1.1146.0) + aws-sdk-core (3.229.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) + base64 + bigdecimal jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.95.0) - aws-sdk-core (~> 3, >= 3.210.0) + logger + aws-sdk-kms (1.110.0) + aws-sdk-core (~> 3, >= 3.228.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.171.0) - aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-s3 (1.196.1) + aws-sdk-core (~> 3, >= 3.228.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.1) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - base64 (0.2.0) + base64 (0.3.0) + bigdecimal (3.2.2) claide (1.1.0) colored (1.2) colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) declarative (0.0.20) - digest-crc (0.6.5) + digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) domain_name (0.6.20240107) dotenv (2.8.1) @@ -55,11 +59,11 @@ GEM faraday (>= 0.8.0) http-cookie (~> 1.0.0) faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) + faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) + faraday-multipart (1.1.1) + multipart-post (~> 2.0) faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) @@ -67,8 +71,8 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.3.1) - fastlane (2.225.0) + fastimage (2.4.0) + fastlane (2.228.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -108,7 +112,7 @@ GEM tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) + xcpretty (~> 0.4.1) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-plugin-versioning_android (0.1.1) fastlane-sirp (1.0.0) @@ -130,12 +134,12 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.1) + google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.4.0) + google-cloud-errors (1.5.0) google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) @@ -151,36 +155,39 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.7) + http-cookie (1.0.8) domain_name (~> 0.5) - httpclient (2.8.3) + httpclient (2.9.0) + mutex_m jmespath (1.6.2) - json (2.8.2) - jwt (2.9.3) + json (2.13.2) + jwt (2.10.2) base64 + logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - multi_json (1.15.0) + multi_json (1.17.0) multipart-post (2.4.1) + mutex_m (0.3.0) nanaimo (0.4.0) - naturally (2.2.1) + naturally (2.3.0) nkf (0.2.0) optparse (0.6.0) os (1.1.4) - plist (3.7.1) - public_suffix (6.0.1) - rake (13.2.1) + plist (3.7.2) + public_suffix (6.0.2) + rake (13.3.0) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.9) - rouge (2.0.7) + rexml (3.4.1) + rouge (3.28.0) ruby2_keywords (0.0.5) - rubyzip (2.3.2) + rubyzip (2.4.1) security (0.1.5) - signet (0.19.0) + signet (0.20.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -207,8 +214,8 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.4.0) rexml (>= 3.3.6, < 4.0) - xcpretty (0.3.0) - rouge (~> 2.0.7) + xcpretty (0.4.1) + rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) @@ -220,4 +227,4 @@ DEPENDENCIES fastlane-plugin-versioning_android BUNDLED WITH - 2.5.10 + 2.6.9 From 23bebf9597ade5fa00883a8ee5cbce1c64696752 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 14 Aug 2025 17:02:40 +0200 Subject: [PATCH 25/26] fix: Auto prompt #2111 --- CHANGELOG | 3 +++ app/build.gradle | 4 ++-- .../keepass/activities/MainCredentialActivity.kt | 9 --------- .../activities/legacy/DatabaseLockActivity.kt | 13 +++++++------ .../keepass/biometric/DeviceUnlockFragment.kt | 7 ------- .../keepass/viewmodels/DeviceUnlockViewModel.kt | 14 +++++++++----- fastlane/metadata/android/en-US/changelogs/137.txt | 1 + fastlane/metadata/android/fr-FR/changelogs/137.txt | 1 + 8 files changed, 23 insertions(+), 29 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/137.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/137.txt diff --git a/CHANGELOG b/CHANGELOG index 4366f2cd9..6933962b2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +KeePassDX(4.1.4) + * Fix auto prompt #2111 + KeePassDX(4.1.4) * Fix UnlockManager #2098 #2101 * Auto device unlock prompt #2105 diff --git a/app/build.gradle b/app/build.gradle index d78671178..d5c441d4f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 15 targetSdkVersion 34 - versionCode = 136 - versionName = "4.1.4" + versionCode = 137 + versionName = "4.1.5" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index 567d3752f..07d6e00d6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -51,7 +51,6 @@ import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode -import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity.Companion.UI_VISIBLE_DURING_LOCK import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper @@ -87,7 +86,6 @@ import com.kunzisoft.keepass.view.MainCredentialView import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel -import com.kunzisoft.keepass.viewmodels.DeviceUnlockState import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel import kotlinx.coroutines.launch import java.io.FileNotFoundException @@ -416,7 +414,6 @@ class MainCredentialActivity : DatabaseModeActivity() { private fun launchGroupActivityIfLoaded(database: ContextualDatabase) { // Check if database really loaded if (database.loaded) { - mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = true clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true) GroupActivity.launch(this, database, @@ -537,12 +534,6 @@ class MainCredentialActivity : DatabaseModeActivity() { } } - override fun onPause() { - // Reinit locking activity UI variable - UI_VISIBLE_DURING_LOCK = false - super.onPause() - } - override fun onSaveInstanceState(outState: Bundle) { outState.putBoolean(KEY_READ_ONLY, mReadOnly) super.onSaveInstanceState(outState) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt index 24c98a0c1..72cbe3872 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt @@ -53,6 +53,7 @@ import com.kunzisoft.keepass.utils.closeDatabase import com.kunzisoft.keepass.utils.registerLockReceiver import com.kunzisoft.keepass.utils.unregisterLockReceiver import com.kunzisoft.keepass.view.showActionErrorIfNeeded +import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel.Companion.isAutoOpenBiometricPromptAllowed import com.kunzisoft.keepass.viewmodels.NodesViewModel import java.util.UUID @@ -70,6 +71,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), protected var mMergeDataAllowed: Boolean = false private var mAutoSaveEnable: Boolean = true + private var isDatabaseUiVisible: Boolean = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -188,7 +191,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), mLockReceiver = LockReceiver { mDatabase = null closeDatabase(database) - UI_VISIBLE_DURING_LOCK = UI_VISIBLE + // Don't allow auto open prompt if lock become when UI visible + isAutoOpenBiometricPromptAllowed = !isDatabaseUiVisible mExitLock = true closeOptionsMenu() finish() @@ -417,7 +421,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), invalidateOptionsMenu() - UI_VISIBLE = true + isDatabaseUiVisible = true } protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) { @@ -432,7 +436,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), } override fun onPause() { - UI_VISIBLE = false + isDatabaseUiVisible = false super.onPause() @@ -483,9 +487,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY" const val TIMEOUT_ENABLE_KEY_DEFAULT = true - - var UI_VISIBLE: Boolean = false - var UI_VISIBLE_DURING_LOCK: Boolean = false } } diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index e0faaffd7..1b49a601a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -45,7 +45,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity.Companion.UI_VISIBLE_DURING_LOCK import com.kunzisoft.keepass.view.DeviceUnlockView import com.kunzisoft.keepass.view.hideByFading import com.kunzisoft.keepass.view.showByFading @@ -146,12 +145,6 @@ class DeviceUnlockFragment: Fragment() { override fun onResume() { super.onResume() - - // Don't allow auto open prompt if lock become when UI visible - if (UI_VISIBLE_DURING_LOCK) { - mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false - } - mDeviceUnlockViewModel.checkUnlockAvailability() } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt index 7d09aac11..7ff3ff902 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -24,9 +24,6 @@ import kotlinx.coroutines.flow.update import javax.crypto.Cipher class DeviceUnlockViewModel(application: Application): AndroidViewModel(application) { - - var allowAutoOpenBiometricPrompt : Boolean = true - private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null private var isConditionToStoreCredentialVerified: Boolean = false @@ -306,7 +303,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat } fun promptShown() { - allowAutoOpenBiometricPrompt = false + isAutoOpenBiometricPromptAllowed = false _uiState.update { currentState -> currentState.copy( cryptoPromptState = DeviceUnlockPromptMode.IDLE @@ -348,7 +345,10 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat cipherDatabase?.let { try { deviceUnlockManager?.initDecryptData(cipherDatabase.specParameters) { cryptoPrompt -> - onPromptRequested(cryptoPrompt, autoOpen = allowAutoOpenBiometricPrompt) + onPromptRequested( + cryptoPrompt, + autoOpen = isAutoOpenBiometricPromptAllowed + ) } ?: setException(Exception("AdvancedUnlockManager not initialized")) } catch (e: Exception) { setException(e) @@ -417,6 +417,10 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat deviceUnlockManager = null } } + + companion object { + var isAutoOpenBiometricPromptAllowed = true + } } enum class DeviceUnlockPromptMode { diff --git a/fastlane/metadata/android/en-US/changelogs/137.txt b/fastlane/metadata/android/en-US/changelogs/137.txt new file mode 100644 index 000000000..eccbfbe7e --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/137.txt @@ -0,0 +1 @@ + * Fix auto prompt #2111 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/137.txt b/fastlane/metadata/android/fr-FR/changelogs/137.txt new file mode 100644 index 000000000..454d844ab --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/137.txt @@ -0,0 +1 @@ + * Correction invite de commande auto #2111 \ No newline at end of file From 845d1a581b3359de3f7bafd84b27da98852331c2 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 14 Aug 2025 17:14:01 +0200 Subject: [PATCH 26/26] fix: warnings --- .../keepass/biometric/DeviceUnlockFragment.kt | 14 +++++--------- .../keepass/viewmodels/DeviceUnlockViewModel.kt | 4 +--- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt index 1b49a601a..6ec9a61a3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -70,7 +70,7 @@ class DeviceUnlockFragment: Fragment() { ActivityResultContracts.StartActivityForResult() ) { result -> if (result.resultCode == Activity.RESULT_OK) { - mDeviceUnlockViewModel.onAuthenticationSucceeded(result) + mDeviceUnlockViewModel.onAuthenticationSucceeded() } else { setAuthenticationFailed() } @@ -298,10 +298,8 @@ class DeviceUnlockFragment: Fragment() { lifecycleScope.launch(Dispatchers.Main) { showViews(true) setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric) - context?.let { context -> - mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { view -> - mDeviceUnlockViewModel.showPrompt() - } + mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ -> + mDeviceUnlockViewModel.showPrompt() } } } @@ -310,10 +308,8 @@ class DeviceUnlockFragment: Fragment() { lifecycleScope.launch(Dispatchers.Main) { showViews(true) setAdvancedUnlockedTitleView(R.string.unlock) - context?.let { context -> - mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { view -> - mDeviceUnlockViewModel.showPrompt() - } + mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ -> + mDeviceUnlockViewModel.showPrompt() } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt index 7ff3ff902..a9b37409f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -152,9 +152,7 @@ class DeviceUnlockViewModel(application: Application): AndroidViewModel(applicat } @RequiresApi(Build.VERSION_CODES.M) - fun onAuthenticationSucceeded( - activityResult: ActivityResult - ) { + fun onAuthenticationSucceeded() { cryptoPrompt?.let { prompt -> when (prompt.type) { DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->