diff --git a/CHANGELOG b/CHANGELOG index 933856781..6933962b2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +KeePassDX(4.1.4) + * Fix auto prompt #2111 + +KeePassDX(4.1.4) + * Fix UnlockManager #2098 #2101 + * Auto device unlock prompt #2105 + * Small fixes ##2066 + KeePassDX(4.1.3) * Fix Autofill Registration #2089 * Fix Biometric errors #2081 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 diff --git a/app/build.gradle b/app/build.gradle index 4d0f72c4c..186c54ea9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 19 targetSdkVersion 34 - versionCode = 135 - versionName = "4.1.3" + versionCode = 137 + versionName = "4.1.5" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" 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 5db3f3cd6..f26a7723a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -315,6 +315,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/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index 68267a94e..67c3294ae 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -49,10 +49,10 @@ import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper -import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity -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.biometric.deviceUnlockError import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher import com.kunzisoft.keepass.credentialprovider.SpecialMode @@ -64,7 +64,11 @@ 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 @@ -82,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.AdvancedUnlockViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel +import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel import kotlinx.coroutines.launch import java.io.FileNotFoundException @@ -99,10 +103,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) @@ -169,8 +173,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() @@ -225,20 +230,31 @@ 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) { + uiState.credentialRequiredCipher?.let { cipher -> + mDeviceUnlockViewModel.encryptCredential( + credential = getCredentialForEncryption(), + cipher = cipher + ) + } } uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase -> onCredentialEncrypted(cipherEncryptDatabase) - mAdvancedUnlockViewModel.consumeCredentialEncrypted() + mDeviceUnlockViewModel.consumeCredentialEncrypted() } uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase -> onCredentialDecrypted(cipherDecryptDatabase) - mAdvancedUnlockViewModel.consumeCredentialDecrypted() + mDeviceUnlockViewModel.consumeCredentialDecrypted() + } + uiState.exception?.let { error -> + Snackbar.make( + coordinatorLayout, + deviceUnlockError(error, this@MainCredentialActivity), + Snackbar.LENGTH_LONG + ).asError().show() + mDeviceUnlockViewModel.exceptionShown() } } } @@ -249,11 +265,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, @@ -273,11 +290,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) { - mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false - } - mDatabaseFileUri?.let { databaseFileUri -> mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri) } @@ -493,7 +505,7 @@ class MainCredentialActivity : DatabaseModeActivity() { loadDatabase() } else { // Init Biometric elements - mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri) + mDeviceUnlockViewModel.databaseFileLoaded(databaseFileUri) } enableConfirmationButton() @@ -521,13 +533,6 @@ class MainCredentialActivity : DatabaseModeActivity() { } } - override fun onPause() { - // Reinit locking activity UI variable - DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null - - super.onPause() - } - override fun onSaveInstanceState(outState: Bundle) { outState.putBoolean(KEY_READ_ONLY, mReadOnly) super.onSaveInstanceState(outState) @@ -653,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/activities/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt index 9965446d1..4ade284c0 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,15 @@ 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.DeviceUnlockViewModel.Companion.isAutoOpenBiometricPromptAllowed import com.kunzisoft.keepass.viewmodels.NodesViewModel -import java.util.* +import java.util.UUID abstract class DatabaseLockActivity : DatabaseModeActivity(), PasswordEncodingDialogFragment.Listener { @@ -66,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) @@ -184,8 +191,8 @@ 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 + // Don't allow auto open prompt if lock become when UI visible + isAutoOpenBiometricPromptAllowed = !isDatabaseUiVisible mExitLock = true closeOptionsMenu() finish() @@ -414,7 +421,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), invalidateOptionsMenu() - LOCKING_ACTIVITY_UI_VISIBLE = true + isDatabaseUiVisible = true } protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) { @@ -429,7 +436,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), } override fun onPause() { - LOCKING_ACTIVITY_UI_VISIBLE = false + isDatabaseUiVisible = false super.onPause() @@ -480,9 +487,6 @@ 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 } } 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?) { 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/AdvancedUnlockManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt deleted file mode 100644 index 764396dde..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt +++ /dev/null @@ -1,506 +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.KeyguardManager -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Build -import android.security.keystore.KeyGenParameterSpec -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.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) { - - 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 - } - - 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( - 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) - } - } 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 - // and the constrains (purposes) in the constructor of the Builder - keyGenerator?.init( - KeyGenParameterSpec.Builder( - ADVANCED_UNLOCK_KEYSTORE_KEY, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES) - .setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING) - .apply { - // Require the user to authenticate with a fingerprint to authorize every use - // of the key, don't use it for device credential because it's the user authentication - if (biometricUnlockEnable) { - setUserAuthenticationRequired(true) - } - // To store in the security chip - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P - && retrieveContext().packageManager.hasSystemFeature( - PackageManager.FEATURE_STRONGBOX_KEYSTORE)) { - setIsStrongBoxBacked(true) - } - } - .build()) - keyGenerator?.generateKey() - } - } catch (e: Exception) { - Log.e(TAG, "Unable to create a key in keystore", e) - advancedUnlockCallback?.onGenericException(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) - } - return null - } - - @Synchronized fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) { - initEncryptData(actionIfCypherInit, true) - } - - @Synchronized private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit, - firstLaunch: Boolean) { - if (!isKeyManagerInitialized) { - return - } - 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()) - ) - } - } - } catch (unrecoverableKeyException: UnrecoverableKeyException) { - Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException) - advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException) - } catch (invalidKeyException: KeyPermanentlyInvalidatedException) { - Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException) - if (firstLaunch) { - deleteAllEntryKeysInKeystoreForBiometric(retrieveContext()) - initEncryptData(actionIfCypherInit, false) - } else { - advancedUnlockCallback?.onInvalidKeyException(invalidKeyException) - } - } catch (e: Exception) { - Log.e(TAG, "Unable to initialize encrypt data", e) - advancedUnlockCallback?.onGenericException(e) - } - } - - @Synchronized fun encryptData(value: ByteArray) { - if (!isKeyManagerInitialized) { - return - } - 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) - } - } catch (e: Exception) { - Log.e(TAG, "Unable to encrypt data", e) - advancedUnlockCallback?.onGenericException(e) - } - } - - @Synchronized fun initDecryptData(ivSpecValue: ByteArray, - actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { - initDecryptData(ivSpecValue, actionIfCypherInit, true) - } - - @Synchronized private fun initDecryptData(ivSpecValue: ByteArray, - actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit, - firstLaunch: Boolean = true) { - if (!isKeyManagerInitialized) { - return - } - 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()) - ) - } - } - } catch (unrecoverableKeyException: UnrecoverableKeyException) { - Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException) - if (firstLaunch) { - deleteKeystoreKey() - initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch) - } else { - advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException) - } - } catch (invalidKeyException: KeyPermanentlyInvalidatedException) { - Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException) - if (firstLaunch) { - deleteAllEntryKeysInKeystoreForBiometric(retrieveContext()) - initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch) - } else { - advancedUnlockCallback?.onInvalidKeyException(invalidKeyException) - } - } catch (e: Exception) { - Log.e(TAG, "Unable to initialize decrypt data", e) - advancedUnlockCallback?.onGenericException(e) - } - } - - @Synchronized fun decryptData(encryptedValue: ByteArray) { - if (!isKeyManagerInitialized) { - return - } - try { - // actual decryption here - cipher?.doFinal(encryptedValue)?.let { decrypted -> - advancedUnlockCallback?.handleDecryptedResult(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) - } - } - - @Synchronized fun deleteKeystoreKey() { - try { - keyStore?.load(null) - keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY) - } catch (e: Exception) { - Log.e(TAG, "Unable to delete entry key in keystore", e) - advancedUnlockCallback?.onGenericException(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 const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore" - private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key" - private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES - private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC - private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 - - @RequiresApi(api = Build.VERSION_CODES.M) - fun canAuthenticate(context: Context): Int { - return try { - BiometricManager.from(context).canAuthenticate( - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - && PreferencesUtil.isDeviceCredentialUnlockEnable(context)) { - BIOMETRIC_STRONG or DEVICE_CREDENTIAL - } else { - BIOMETRIC_STRONG - } - ) - } catch (e: Exception) { - Log.e(TAG, "Unable to authenticate with strong biometric.", e) - try { - BiometricManager.from(context).canAuthenticate( - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - && PreferencesUtil.isDeviceCredentialUnlockEnable(context)) { - BIOMETRIC_WEAK or DEVICE_CREDENTIAL - } else { - BIOMETRIC_WEAK - } - ) - } catch (e: Exception) { - Log.e(TAG, "Unable to authenticate with weak biometric.", e) - BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE - } - } - } - - fun isDeviceSecure(context: Context): Boolean { - return ContextCompat.getSystemService(context, KeyguardManager::class.java) - ?.isDeviceSecure ?: false - } - - fun biometricUnlockSupported(context: Context): Boolean { - val biometricCanAuthenticate = try { - BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG) - } catch (e: Exception) { - Log.e(TAG, "Unable to authenticate with strong biometric.", e) - try { - BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK) - } catch (e: Exception) { - Log.e(TAG, "Unable to authenticate with weak biometric.", e) - BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE - } - } - return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED - ) - } - - fun deviceCredentialUnlockSupported(context: Context): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL) - (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED - ) - } else { - true - } - } - - /** - * 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) - } - } - 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) - } - }) - } - CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll() - } - } - -} \ 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..6ec9a61a3 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockFragment.kt @@ -0,0 +1,373 @@ +/* + * 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.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.ActivityResultLauncher +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.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 + +@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 + + private var mDeviceCredentialResultLauncher: ActivityResultLauncher? = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + mDeviceUnlockViewModel.onAuthenticationSucceeded() + } else { + setAuthenticationFailed() + } + } + + private var biometricAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + mDeviceUnlockViewModel.onAuthenticationSucceeded(result) + } + + 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) + + // Init device unlock prompt + mBiometricPrompt = BiometricPrompt( + this@DeviceUnlockFragment, + Executors.newSingleThreadExecutor(), + biometricAuthenticationCallback + ) + + activity?.addMenuProvider(menuProvider, viewLifecycleOwner) + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + mDeviceUnlockViewModel.uiState.collect { uiState -> + // Change mode + toggleDeviceCredentialMode(uiState.newDeviceUnlockMode) + // Prompt + manageDeviceCredentialPrompt(uiState.cryptoPromptState) + // Advanced menu + mAllowAdvancedUnlockMenu = uiState.allowAdvancedUnlockMenu + activity?.invalidateOptionsMenu() + } + } + } + } + + override fun onResume() { + super.onResume() + mDeviceUnlockViewModel.checkUnlockAvailability() + } + + fun cancelBiometricPrompt() { + mBiometricPrompt?.cancelAuthentication() + } + + 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) + } + } + + private fun manageDeviceCredentialPrompt( + state: DeviceUnlockPromptMode + ) { + mDeviceUnlockViewModel.cryptoPrompt?.let { prompt -> + when (state) { + DeviceUnlockPromptMode.IDLE -> {} + DeviceUnlockPromptMode.SHOW -> { + openPrompt(prompt) + mDeviceUnlockViewModel.promptShown() + } + DeviceUnlockPromptMode.CLOSE -> { + cancelBiometricPrompt() + mDeviceUnlockViewModel.biometricPromptClosed() + } + } + } + } + + private fun openPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) { + 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 + ) + ) + } + } + } catch (e: Exception) { + Log.e(TAG, "Unable to open prompt", e) + mDeviceUnlockViewModel.setException(e) + } + } + + 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) + 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) + context?.let { context -> + mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { + mDeviceUnlockViewModel.setException(SecurityException( + context.getString(R.string.credential_before_click_advanced_unlock_button) + )) + } + } + } + } + + private fun setStoreCredentialMode() { + lifecycleScope.launch(Dispatchers.Main) { + showViews(true) + setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric) + mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ -> + mDeviceUnlockViewModel.showPrompt() + } + } + } + + private fun setExtractCredentialMode() { + lifecycleScope.launch(Dispatchers.Main) { + showViews(true) + setAdvancedUnlockedTitleView(R.string.unlock) + mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ -> + mDeviceUnlockViewModel.showPrompt() + } + } + } + + 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 setAuthenticationError(errorCode: Int, errString: CharSequence) { + Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") + when (errorCode) { + BiometricPrompt.ERROR_CANCELED, + BiometricPrompt.ERROR_NEGATIVE_BUTTON, + BiometricPrompt.ERROR_USER_CANCELED -> { + // Ignore negative button + } + else -> + mDeviceUnlockViewModel.setException(SecurityException(errString.toString())) + } + } + + private fun setAuthenticationFailed() { + Log.e(TAG, "Biometric authentication failed, biometric not recognized") + mDeviceUnlockViewModel.setException( + SecurityException(getString(R.string.advanced_unlock_not_recognized)) + ) + } + + 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/DeviceUnlockManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt new file mode 100644 index 000000000..534e90484 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/DeviceUnlockManager.kt @@ -0,0 +1,430 @@ +/* + * 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.KeyguardManager +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyPermanentlyInvalidatedException +import android.security.keystore.KeyProperties +import android.util.Log +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.biometric.BiometricManager +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 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 javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.IvParameterSpec + +@RequiresApi(api = Build.VERSION_CODES.M) +class DeviceUnlockManager(private var appContext: Context) { + + private var keyStore: KeyStore? = null + private var keyGenerator: KeyGenerator? = null + private var cipher: Cipher? = null + + private var biometricUnlockEnable = isBiometricUnlockEnable(appContext) + private var deviceCredentialUnlockEnable = isDeviceCredentialUnlockEnable(appContext) + + init { + 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 + ) + 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") + } + } + } + + @Synchronized private fun getSecretKey(): SecretKey? { + 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 + // and the constrains (purposes) in the constructor of the Builder + keyGenerator?.init( + KeyGenParameterSpec.Builder( + ADVANCED_UNLOCK_KEYSTORE_KEY, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES) + .setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING) + .apply { + // Require the user to authenticate with a fingerprint to authorize every use + // of the key, don't use it for device credential because it's the user authentication + if (biometricUnlockEnable) { + setUserAuthenticationRequired(true) + } + // To store in the security chip + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + && appContext.packageManager.hasSystemFeature( + PackageManager.FEATURE_STRONGBOX_KEYSTORE)) { + setIsStrongBoxBacked(true) + } + } + .build()) + keyGenerator?.generateKey() + } + } catch (e: Exception) { + Log.e(TAG, "Unable to create a key in keystore", 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) + throw e + } + return null + } + + @Synchronized fun initEncryptData( + actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit + ) { + initEncryptData(true, actionIfCypherInit) + } + + @Synchronized private fun initEncryptData( + firstLaunch: Boolean, + actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit + ) { + try { + getSecretKey()?.let { secretKey -> + cipher?.let { cipher -> + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + actionIfCypherInit.invoke( + 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) + throw unrecoverableKeyException + } catch (invalidKeyException: KeyPermanentlyInvalidatedException) { + Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException) + if (firstLaunch) { + deleteAllEntryKeysInKeystoreForBiometric(appContext) + initEncryptData(false, actionIfCypherInit) + } else { + throw invalidKeyException + } + } catch (e: Exception) { + Log.e(TAG, "Unable to initialize encrypt data", e) + throw e + } + } + + @Synchronized fun encryptData( + value: ByteArray, + cipher: Cipher?, + 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 -> + handleEncryptedResult.invoke(encrypted, spec.iv) + } + } catch (e: Exception) { + Log.e(TAG, "Unable to encrypt data", e) + throw e + } + } + + @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 + ) { + 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( + 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 + ) + ) + ) + } + } + } catch (unrecoverableKeyException: UnrecoverableKeyException) { + Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException) + if (firstLaunch) { + deleteKeystoreKey() + initDecryptData(ivSpecValue, false, actionIfCypherInit) + } else { + throw unrecoverableKeyException + } + } catch (invalidKeyException: KeyPermanentlyInvalidatedException) { + Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException) + if (firstLaunch) { + deleteAllEntryKeysInKeystoreForBiometric(appContext) + initDecryptData(ivSpecValue, false, actionIfCypherInit) + } else { + throw invalidKeyException + } + } catch (e: Exception) { + Log.e(TAG, "Unable to initialize decrypt data", e) + throw e + } + } + + @Synchronized fun decryptData( + encryptedValue: ByteArray, + cipher: Cipher?, + handleDecryptedResult: (decryptedValue: ByteArray) -> Unit + ) { + try { + // actual decryption here + cipher?.doFinal(encryptedValue)?.let { decrypted -> + handleDecryptedResult.invoke(decrypted) + } + } catch (e: Exception) { + Log.e(TAG, "Unable to decrypt data", e) + throw e + } + } + + @Synchronized fun deleteKeystoreKey() { + try { + keyStore?.load(null) + keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY) + } catch (e: Exception) { + Log.e(TAG, "Unable to delete entry key in keystore", e) + throw e + } + } + + companion object { + + 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" + private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC + private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 + + @RequiresApi(api = Build.VERSION_CODES.M) + fun canAuthenticate(context: Context): Int { + return try { + BiometricManager.from(context).canAuthenticate( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && PreferencesUtil.isDeviceCredentialUnlockEnable(context)) { + BIOMETRIC_STRONG or DEVICE_CREDENTIAL + } else { + BIOMETRIC_STRONG + } + ) + } catch (e: Exception) { + Log.e(TAG, "Unable to authenticate with strong biometric.", e) + try { + BiometricManager.from(context).canAuthenticate( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && PreferencesUtil.isDeviceCredentialUnlockEnable(context)) { + BIOMETRIC_WEAK or DEVICE_CREDENTIAL + } else { + BIOMETRIC_WEAK + } + ) + } catch (e: Exception) { + Log.e(TAG, "Unable to authenticate with weak biometric.", e) + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE + } + } + } + + fun isDeviceSecure(context: Context): Boolean { + return ContextCompat.getSystemService(context, KeyguardManager::class.java) + ?.isDeviceSecure ?: false + } + + fun biometricUnlockSupported(context: Context): Boolean { + val biometricCanAuthenticate = try { + BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG) + } catch (e: Exception) { + Log.e(TAG, "Unable to authenticate with strong biometric.", e) + try { + BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK) + } catch (e: Exception) { + Log.e(TAG, "Unable to authenticate with weak biometric.", e) + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE + } + } + return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED + ) + } + + fun deviceCredentialUnlockSupported(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL) + (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED + ) + } else { + true + } + } + + /** + * Remove entry key in keystore + */ + fun deleteEntryKeyInKeystoreForBiometric( + appContext: Context + ) { + DeviceUnlockManager(appContext).apply { + deleteKeystoreKey() + } + } + + fun deleteAllEntryKeysInKeystoreForBiometric(appContext: Context) { + try { + deleteEntryKeyInKeystoreForBiometric(appContext) + } catch (e: Exception) { + Toast.makeText(appContext, + deviceUnlockError(e, appContext), + Toast.LENGTH_SHORT).show() + } finally { + CipherDatabaseAction.getInstance(appContext).deleteAll() + } + } + } +} + +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) + +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 75% 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..b840063cb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/AdvancedUnlockInfoView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/DeviceUnlockView.kt @@ -25,15 +25,14 @@ 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 @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 +44,7 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context, biometricButtonView = findViewById(R.id.biometric_button) } - fun setIconViewClickListener(listener: OnClickListener?) { + fun setDeviceUnlockButtonViewClickListener(listener: OnClickListener?) { biometricButtonView?.setOnClickListener(listener) } @@ -60,14 +59,4 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context, fun setTitle(@StringRes textId: Int) { title = context.getString(textId) } - - fun setMessage(text: CharSequence) { - if (text.isNotEmpty()) - 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/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..a9b37409f --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DeviceUnlockViewModel.kt @@ -0,0 +1,437 @@ +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 +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 +import javax.crypto.Cipher + +class DeviceUnlockViewModel(application: Application): AndroidViewModel(application) { + private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null + + private var isConditionToStoreCredentialVerified: Boolean = false + + 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 + + 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 by verifying device settings and database mode + */ + fun checkUnlockAvailability() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + cipherDatabaseAction.containsCipherDatabase(databaseUri) { 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) + } + } + } + } + } + + /** + * Check unlock availability and change the current mode depending of device's state + */ + fun checkUnlockAvailability(databaseFileUri: Uri?) { + databaseUri = databaseFileUri + checkUnlockAvailability() + } + + @RequiresApi(Build.VERSION_CODES.M) + fun selectMode(containsCipherDatabase: Boolean) { + try { + if (isConditionToStoreCredentialVerified) { + deviceUnlockManager = DeviceUnlockManager(getApplication()) + // listen for encryption + changeMode(DeviceUnlockMode.STORE_CREDENTIAL) + initEncryptData() + } else if (containsCipherDatabase) { + 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 + 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())) { + if (databaseUri != this.databaseUri) { + connect(databaseUri) + } + } else { + disconnect() + } + } + + @RequiresApi(Build.VERSION_CODES.M) + fun onAuthenticationSucceeded() { + 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 + ) { + 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( + credentialRequiredCipher = cipher + ) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + fun encryptCredential( + credential: ByteArray, + cipher: Cipher? + ) { + try { + deviceUnlockManager?.encryptData( + value = credential, + cipher = cipher, + 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) + } finally { + // Reinit credential storage request + _uiState.update { currentState -> + currentState.copy( + credentialRequiredCipher = null + ) + } + } + } + + @RequiresApi(Build.VERSION_CODES.M) + fun decryptCredential(cipher: Cipher?) { + // retrieve the encrypted value from preferences + databaseUri?.let { databaseUri -> + cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase -> + cipherDatabase?.encryptedValue?.let { encryptedCredential -> + try { + deviceUnlockManager?.decryptData( + encryptedValue = encryptedCredential, + cipher = cipher, + 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, + autoOpen: Boolean = false + ) { + this@DeviceUnlockViewModel.cryptoPrompt = cryptoPrompt + if (autoOpen && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(getApplication())) + showPrompt() + } + + fun showPrompt() { + _uiState.update { currentState -> + currentState.copy( + cryptoPromptState = DeviceUnlockPromptMode.SHOW + ) + } + } + + fun promptShown() { + isAutoOpenBiometricPromptAllowed = false + _uiState.update { currentState -> + currentState.copy( + cryptoPromptState = DeviceUnlockPromptMode.IDLE + ) + } + } + + fun setException(value: Exception?) { + _uiState.update { currentState -> + currentState.copy( + exception = value + ) + } + } + + fun exceptionShown() { + _uiState.update { currentState -> + currentState.copy( + exception = null + ) + } + } + + @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, + autoOpen = isAutoOpenBiometricPromptAllowed + ) + } ?: setException(Exception("AdvancedUnlockManager not initialized")) + } catch (e: Exception) { + setException(e) + } + } ?: deleteEncryptedDatabaseKey() + } + } ?: setException(UnknownDatabaseLocationException()) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun changeMode(deviceUnlockMode: DeviceUnlockMode) { + this.deviceUnlockMode = deviceUnlockMode + cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher -> + _uiState.update { currentState -> + currentState.copy( + newDeviceUnlockMode = 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( + cryptoPromptState = DeviceUnlockPromptMode.CLOSE + ) + } + } + + fun biometricPromptClosed() { + cryptoPrompt = null + _uiState.update { currentState -> + currentState.copy( + cryptoPromptState = DeviceUnlockPromptMode.IDLE + ) + } + } + + 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 + } + } + + companion object { + var isAutoOpenBiometricPromptAllowed = true + } +} + +enum class DeviceUnlockPromptMode { + IDLE, SHOW, CLOSE +} + +data class DeviceUnlockState( + val newDeviceUnlockMode: DeviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE, + val allowAdvancedUnlockMenu: Boolean = false, + val credentialRequiredCipher: Cipher? = null, + val cipherEncryptDatabase: CipherEncryptDatabase? = null, + val cipherDecryptDatabase: CipherDecryptDatabase? = null, + val cryptoPromptState: DeviceUnlockPromptMode = DeviceUnlockPromptMode.IDLE, + val autoOpenPrompt: Boolean = false, + val exception: 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 @@ -أظهر \"المعرّف العام المميز\" 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-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 3381902a0..0b0338b13 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -5,8 +5,8 @@ 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/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 cf7ffb018..ec231268d 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. diff --git a/build.gradle b/build.gradle index 180564429..3ec094017 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 fb1c56b5a..09c32d4f0 100644 --- a/crypto/build.gradle +++ b/crypto/build.gradle @@ -42,11 +42,11 @@ android { } } - dependencies { // Crypto implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1' - androidTestImplementation "androidx.test:runner:$android_test_version" androidTestImplementation 'org.testng:testng:6.9.6' + androidTestImplementation "androidx.test:runner:$android_test_version" + testImplementation "androidx.test:runner:$android_test_version" } 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..031b8e16d --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/136.txt @@ -0,0 +1,3 @@ + * Fix UnlockManager #2098 #2101 + * Auto device unlock prompt #2105 + * Small fixes ##2066 \ No newline at end of file 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/136.txt b/fastlane/metadata/android/fr-FR/changelogs/136.txt new file mode 100644 index 000000000..7c211d24d --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/136.txt @@ -0,0 +1,3 @@ + * Correction UnlockManager #2098 #2101 + * Invite de déverrouillage automatique de l'appareil #2105 + * Petites corrections ##2066 \ 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