From 7be554a3786832e8396937d6053894cf814f05c1 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sun, 10 Aug 2025 12:24:23 +0200 Subject: [PATCH] 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 (85%) 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 85% 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