Merge branch 'develop' into feature/Passkeys

This commit is contained in:
J-Jamet
2025-08-14 17:39:52 +02:00
66 changed files with 1418 additions and 1485 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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?) {

View File

@@ -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()

View File

@@ -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)

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Intent>
) {
// 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()
}
}
}

View File

@@ -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
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Intent>? = 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
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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)
)
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
})

View File

@@ -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()
}
}

View File

@@ -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<DeviceUnlockState> = _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
}
}

View File

@@ -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<DeviceUnlockState> = _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
)

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<com.kunzisoft.keepass.view.AdvancedUnlockInfoView
<com.kunzisoft.keepass.view.DeviceUnlockView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/advanced_unlock_view"
android:layout_width="match_parent"

View File

@@ -648,7 +648,6 @@
<string name="show_uuid_title">أظهر \"المعرّف العام المميز\" UUID</string>
<string name="unlock_and_link_biometric">رابط فتح الجهاز</string>
<string name="advanced_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</string>
<string name="advanced_unlock_scanning_error">خطأ في فتح الجهاز: %1$s</string>
<string name="menu_appearance_settings_summary">المظاهر والألوان والسمات</string>
<string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
<string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string>

View File

@@ -199,7 +199,6 @@
<string name="keystore_not_accessible">Açar ehtiyyatı düzgün formada başladılmadı.</string>
<string name="advanced_unlock_prompt_store_credential_title">Cihaz kilidini açma linki</string>
<string name="database_history">Tarixçə</string>
<string name="advanced_unlock_scanning_error">Cihaz kilidini açma xətası: %1$s</string>
<string name="warning_database_info_reloaded">Məlumat bazasını yenidən yükləmək lokal olaraq modifikasiya olunmuş faylları siləcəkdir.</string>
<string name="warning_database_revoked">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.</string>
<string name="warning_exact_alarm">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.</string>

View File

@@ -536,7 +536,6 @@
<string name="recycle_bin_title">Използване на кошчето</string>
<string name="recycle_bin_summary">Премества групите и записите в групата „Кошче“ вместо да ги премахва директно</string>
<string name="advanced_unlock_prompt_store_credential_title">Отключване с устройството</string>
<string name="advanced_unlock_scanning_error">Грешка при отключване на устройството: %1$s</string>
<string name="advanced_unlock_not_recognized">Не може да бъде разпознато кога устройството е отключено</string>
<string name="advanced_unlock_prompt_not_initialized">Заявката за отключване не може да бъде подготвена.</string>
<string name="password_size_summary">Подразбирана дължина на създаваните пароли</string>

View File

@@ -613,7 +613,6 @@
<string name="configure">Configura</string>
<string name="biometric_security_update_required">Cal actualitzar la seguretat biomètrica.</string>
<string name="unlock_and_link_biometric">Enllaç de desbloqueig del dispositiu</string>
<string name="advanced_unlock_scanning_error">Error en desbloquejar el dispositiu: %1$s</string>
<string name="unavailable">No disponible</string>
<string name="advanced_unlock_prompt_not_initialized">No s\'ha pogut inicialitzar l\'indicador de desbloqueig del dispositiu.</string>
<string name="credential_before_click_advanced_unlock_button">Escriviu la contrasenya i, a continuació, feu clic en aquest botó.</string>

View File

@@ -499,7 +499,6 @@
<string name="device_credential">Heslo zařízení</string>
<string name="credential_before_click_advanced_unlock_button">Zadejte heslo a pak klepněte na toto tlačítko.</string>
<string name="advanced_unlock_prompt_not_initialized">Nepodařilo se inicializovat nabídku pro odemykání zařízení.</string>
<string name="advanced_unlock_scanning_error">Chyba při odemykání zařízení: %1$s</string>
<string name="advanced_unlock_not_recognized">Otisk pro odemykání zařízení nebyl rozpoznán</string>
<string name="advanced_unlock_invalid_key">Nepodařilo se načíst klíč odemykání zařízení. Odstraňte ho a opakujte proces rozpoznání odemknutí.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Načíst údaj z databáze pomocí dat odemykání zařízení</string>

View File

@@ -507,7 +507,6 @@
<string name="content">Indhold</string>
<string name="credential_before_click_advanced_unlock_button">Indtast adgangskoden, og klik derefter på denne knap.</string>
<string name="advanced_unlock_prompt_not_initialized">Kunne ikke initialisere oplåsningsprompt.</string>
<string name="advanced_unlock_scanning_error">Fejl ved oplåsning: %1$s</string>
<string name="advanced_unlock_not_recognized">Kunne ikke genkende aftryk til oplåsning</string>
<string name="advanced_unlock_invalid_key">Oplåsningsnøgle kan ikke læses. Slet den og gentag proceduren for genkendelse af oplåsning.</string>
<string name="advanced_unlock_prompt_extract_credential_title">Enhedsoplåsningsgenkendelse</string>

View File

@@ -529,7 +529,6 @@
<string name="device_credential">Geräteanmeldedaten</string>
<string name="credential_before_click_advanced_unlock_button">Passwort eingeben und dann diese Taste drücken.</string>
<string name="advanced_unlock_prompt_not_initialized">Geräteentsperrungsabfrage konnte nicht gestartet werden.</string>
<string name="advanced_unlock_scanning_error">Fehler bei Geräteentsperrung: %1$s</string>
<string name="advanced_unlock_not_recognized">Fingerabdruck für Geräteentsperrung wurde nicht erkannt</string>
<string name="advanced_unlock_invalid_key">Der Geräteentsperrschlüssel ist nicht lesbar. Bitte diesen löschen und den Vorgang zur Entsperr-Erkennung wiederholen.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Datenbankanmeldedaten aus Geräteentsperrdaten gewinnen</string>

View File

@@ -503,7 +503,6 @@
<string name="credential_before_click_advanced_unlock_button">Πληκτρολογήστε τον κωδικό πρόσβασης, και στη συνέχεια κάντε κλικ αυτό το κουμπί.</string>
<string name="advanced_unlock_prompt_not_initialized">Δεν είναι δυνατή η προετοιμασία της προτροπής ξεκλειδώματος συσκευής.</string>
<string name="advanced_unlock_not_recognized">Δεν ήταν δυνατή η αναγνώριση αποτυπώματος ξεκλειδώματος συσκευής</string>
<string name="advanced_unlock_scanning_error">Σφάλμα ξεκλειδώματος συσκευής: %1$s</string>
<string name="advanced_unlock_invalid_key">Δεν είναι δυνατή η ανάγνωση του κλειδιού ξεκλειδώματος της συσκευής. Διαγράψτε το και επαναλάβετε τη διαδικασία αναγνώρισης ξεκλειδώματος.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Εξαγωγή διαπιστευτηρίων βάσης δεδομένων με δεδομένα ξεκλειδώματος συσκευής</string>
<string name="education_advanced_unlock_summary">Συνδέστε τον κωδικό πρόσβασής σας με το σαρωμένο βιομετρικό ή τα διαπιστευτήρια της συσκευής σας για να ξεκλειδώσετε γρήγορα τη βάση δεδομένων σας.</string>

View File

@@ -5,8 +5,8 @@
<string name="colorize_password_summary">Colourise password characters by type</string>
<string name="content_description_entry_background_color">Entry background colour</string>
<string name="invalid_db_sig">Could not recognise the database format.</string>
<string name="advanced_unlock_not_recognized">Could not recognise advanced unlock print</string>
<string name="advanced_unlock_prompt_not_initialized">Unable to initialise advanced unlock prompt.</string>
<string name="advanced_unlock_not_recognized">Could not recognise device unlock print</string>
<string name="advanced_unlock_prompt_not_initialized">Unable to initialise device unlock prompt.</string>
<string name="download_initialization">Initialising…</string>
<string name="download_finalization">Finalising…</string>
<string name="download_canceled">Cancelled!</string>

View File

@@ -444,7 +444,6 @@
<string name="device_credential">Credenciales del dispositivo</string>
<string name="credential_before_click_advanced_unlock_button">Teclee la contraseña y luego pulse sobre este botón.</string>
<string name="advanced_unlock_prompt_not_initialized">No se puede inicializar el aviso de desbloqueo avanzado.</string>
<string name="advanced_unlock_scanning_error">Error de desbloqueo del dispositivo: %1$s</string>
<string name="advanced_unlock_not_recognized">No se ha podido reconocer la impresión de desbloqueo avanzado</string>
<string name="advanced_unlock_invalid_key">No se puede leer la clave de desbloqueo del dispositivo. Por favor, bórrala y repite el procedimiento de reconocimiento del desbloqueo.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Extraer la credencial de la base de datos con los datos de desbloqueo del dispositivo</string>

View File

@@ -440,7 +440,6 @@
<string name="merge_success">Mestimine õnnestus</string>
<string name="biometric_security_update_required">Vajalik on biomeetrilise turvalisuse uuendus.</string>
<string name="encrypted_value_stored">Krüptitud salasõna on salvestatud</string>
<string name="advanced_unlock_scanning_error">Viga seadme lukustuse eemaldamisel: %1$s</string>
<string name="advanced_unlock_not_recognized">Ei õnnestunud tuvastada lukustuse eemaldamiseks vajalikku tunnust</string>
<string name="advanced_unlock_prompt_not_initialized">Seadme lukustuse eemaldamise päringu käivitamine ei õnnestu.</string>
<string name="biometric">Biomeetriline</string>

View File

@@ -489,7 +489,6 @@
<string name="advanced_unlock_prompt_store_credential_message">Zure kutxa gotorraren pasahitz-nagusia gogoratu behar duzu naiz eta desblokeo aurreratuko ezagutzea erabili arren.</string>
<string name="encrypted_value_stored">Zifratutako pasahitza gordeta</string>
<string name="unavailable">Datu-base honek ez du biltegiratuta kredentzialik.</string>
<string name="advanced_unlock_scanning_error">Gailuaren desblokeatze errorea: %1$s</string>
<string name="menu_appearance_settings">Itxura</string>
<string name="autofill_sign_in_prompt">KeePassDXekin erregistratu</string>
<string name="autofill_explanation_summary">Gaitu betetze automatikoa beste aplikazioetako formularioak errez betetzeko</string>

View File

@@ -509,7 +509,6 @@
<string name="device_credential">Identifiant de l\'appareil</string>
<string name="credential_before_click_advanced_unlock_button">Tapez le mot de passe, puis cliquez sur ce bouton.</string>
<string name="advanced_unlock_prompt_not_initialized">Impossible d\'initialiser l\'invite de déverrouillage avancé.</string>
<string name="advanced_unlock_scanning_error">Erreur de déverrouillage avancé : %1$s</string>
<string name="advanced_unlock_not_recognized">Impossible de reconnaître l\'empreinte de déverrouillage de l\'appareil</string>
<string name="advanced_unlock_invalid_key">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.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Extraire les identifiants de la base de données avec des données de déverrouillage de l\'appareil</string>

View File

@@ -425,7 +425,6 @@
<string name="advanced_unlock_invalid_key">Non foi posíbel ler a clave de desbloqueo avanzado. Por favor, bórrea e repita o procedemento de recoñecemento do desbloqueo.</string>
<string name="encrypted_value_stored">Contrasinal cifrado almacenado</string>
<string name="advanced_unlock_not_recognized">Non foi posíbel recoñecer a pegada do desbloqueo avanzado</string>
<string name="advanced_unlock_scanning_error">Erro de desbloqueo avanzado: %1$s</string>
<string name="autofill_service_name">Servizo de autocompletado do KeePassDX</string>
<string name="database_history">Historial</string>
<string name="advanced_unlock_prompt_store_credential_message">Aínda precisa lembrar a súa credencial principal se usar o recoñecemento de desbloqueo avanzado.</string>

View File

@@ -490,7 +490,6 @@
<string name="menu_keystore_remove_key">Izbriši ključ za otključavanje uređaja</string>
<string name="advanced_unlock_prompt_store_credential_title">Poveznica za otključavanje uređaja</string>
<string name="advanced_unlock_prompt_not_initialized">Nije moguće pokrenuti prozor za otključavanje uređaja.</string>
<string name="advanced_unlock_scanning_error">Greška otključavanja uređaja: %1$s</string>
<string name="advanced_unlock_prompt_extract_credential_message">Izdvoji podatake za prijavu na bazu podataka pomoću podataka za otključavanje uređaja</string>
<string name="advanced_unlock_not_recognized">Nije bilo moguće prepoznati ispis za otključavanje uređaja</string>
<string name="advanced_unlock_invalid_key">Nije moguće pročitati ključ za otključavanje uređaja. Izbriši ga i ponovi postupak prepoznavanja otključavanja.</string>

View File

@@ -537,7 +537,6 @@
<string name="credential_before_click_advanced_unlock_button">Írja be a jelszót, majd kattintson erre a gombra.</string>
<string name="temp_advanced_unlock_enable_title">Ideiglenes eszközfeloldás</string>
<string name="permission">Engedély</string>
<string name="advanced_unlock_scanning_error">Eszközfeloldási hiba: %1$s</string>
<string name="content">Tartalom</string>
<string name="advanced_unlock_tap_delete">Koppintson az eszközfeloldási kulcsok törléséhez</string>
<string name="device_credential_unlock_enable_title">Eszköz hitelesítő adataival történő feloldás</string>

View File

@@ -582,7 +582,6 @@
<string name="upper_case">HURUF BESAR</string>
<string name="title_case">Huruf Judul</string>
<string name="character_count">Jumlah karakter: %1$d</string>
<string name="advanced_unlock_scanning_error">Terjadi kesalahan buka kunci perangkat: %1$s</string>
<string name="advanced_unlock_prompt_not_initialized">Tidak dapat menginisialisasi perintah buka kunci perangkat.</string>
<string name="monospace_font_fields_enable_title">Bidang tipe huruf</string>
<string name="keyboard_save_search_info_title">Simpan info terbagi</string>

View File

@@ -515,7 +515,6 @@
<string name="advanced_unlock_prompt_store_credential_title">Collegamento allo sblocco con dispositivo</string>
<string name="device_credential">Credenziali del dispositivo</string>
<string name="credential_before_click_advanced_unlock_button">Inserisci la password, poi clicca questo pulsante.</string>
<string name="advanced_unlock_scanning_error">Errore sblocco con dispositivo: %1$s</string>
<string name="advanced_unlock_prompt_extract_credential_title">Riconoscimento sblocco con dispositivo</string>
<string name="menu_keystore_remove_key">Elimina chiave di sblocco del dispositivo</string>
<string name="error_rebuild_list">Non è possibile ricostruire la lista correttamente.</string>

View File

@@ -539,7 +539,6 @@
<string name="advanced_unlock_prompt_extract_credential_message">חלץ אישור מסד נתונים עם נתוני ביטול נעילת מכשיר</string>
<string name="advanced_unlock_invalid_key">לא ניתן לקרוא את מפתח ביטול נעילת המכשיר. נא למחוק אותו ולחזור על התהליך לזיהוי ביטול נעילה.</string>
<string name="advanced_unlock_not_recognized">לא היה ניתן לזהות טביעת ביטול נעילת מכשיר</string>
<string name="advanced_unlock_scanning_error">שגיאת ביטול נעילת מכשיר: %1$s</string>
<string name="credential_before_click_advanced_unlock_button">הקלד את הסיסמה, ואז לחץ על הכפתור הזה.</string>
<string name="lock_database_show_button_title">הצג כפתור נעילה</string>
<string name="lock_database_show_button_summary">הצג את כפתור הנעילה בממשק המשתמש</string>

View File

@@ -491,7 +491,6 @@
<string name="device_credential">デバイス認証情報</string>
<string name="credential_before_click_advanced_unlock_button">パスワードを入力し、このボタンをタップします。</string>
<string name="advanced_unlock_prompt_not_initialized">デバイスのロック解除プロンプトを初期化できません。</string>
<string name="advanced_unlock_scanning_error">デバイスのロック解除エラー: %1$s</string>
<string name="advanced_unlock_invalid_key">デバイスのロック解除キーを読み取ることができません。削除して、ロック解除認識手順を繰り返してください。</string>
<string name="advanced_unlock_prompt_extract_credential_message">デバイスのロック解除データを使用してデータベースの資格情報を抽出する</string>
<string name="advanced_unlock_prompt_extract_credential_title">デバイスのロック解除認識</string>

View File

@@ -395,7 +395,6 @@
<string name="configure">Konfigūruoti</string>
<string name="biometric_security_update_required">Reikalingas biometrinių duomenų saugumo atnaujinimas.</string>
<string name="advanced_unlock_prompt_store_credential_message">Jei naudojate įrenginio atrakinimo atpažinimą, vis tiek turite prisiminti pagrindinį saugyklos raktą</string>
<string name="advanced_unlock_scanning_error">Įrenginio atrakinimo klaida: %1$s</string>
<string name="database_history">Istorija</string>
<string name="properties">Nustatymai</string>
<string name="menu_appearance_settings_summary">Temos, spalvos, atributai</string>

View File

@@ -405,7 +405,6 @@
<string name="hide_broken_locations_summary">Skjul ødelagte lenker i listen over nylige databaser</string>
<string name="hide_broken_locations_title">Skjul ødelagte databaselenker</string>
<string name="autofill_ask_to_save_data_title">Spør om lagring av data</string>
<string name="advanced_unlock_scanning_error">Feil ved opplåsing: %1$s</string>
<string name="warning_empty_keyfile">Det anbefales ikke å legge til en tom nøkkelfil.</string>
<string name="warning_sure_add_file">Legg til filen uansett\?</string>
<string name="registration_mode">Registreringsmodus</string>

View File

@@ -508,7 +508,6 @@
<string name="device_credential">Apparaatreferentie</string>
<string name="credential_before_click_advanced_unlock_button">Typ het wachtwoord en klik vervolgens op deze knop.</string>
<string name="advanced_unlock_prompt_not_initialized">Kan apparaatontgrendeling niet initialiseren.</string>
<string name="advanced_unlock_scanning_error">Fout bij apparaatontgrendeling: %1$s</string>
<string name="advanced_unlock_not_recognized">Vingerafdruk niet herkent bij apparaatontgrendeling</string>
<string name="advanced_unlock_invalid_key">Kan de sleutel voor apparaatontgrendeling niet lezen. Verwijder deze en herhaal de herkenningsprocedure voor het ontgrendelen.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Database uitpakken met gegevens voor apparaatontgrendeling</string>

View File

@@ -511,7 +511,6 @@
<string name="advanced_unlock_tap_delete">Stuknij, aby usunąć klucze odblokowywania urządzenia</string>
<string name="content">Zawartość</string>
<string name="advanced_unlock_prompt_extract_credential_title">Rozpoznawanie odblokowania urządzenia</string>
<string name="advanced_unlock_scanning_error">Błąd odblokowania urządzenia: %1$s</string>
<string name="error_rebuild_list">Nie można poprawnie odbudować listy.</string>
<string name="error_database_uri_null">Nie można pobrać identyfikatora URI bazy danych.</string>
<string name="autofill_inline_suggestions_keyboard">Dodano sugestie autouzupełniania.</string>

View File

@@ -512,7 +512,6 @@
<string name="properties">Propriedades</string>
<string name="credential_before_click_advanced_unlock_button">Digite a senha e clique neste botão.</string>
<string name="advanced_unlock_prompt_not_initialized">Não foi possível inicializar o prompt de desbloqueio do dispositivo.</string>
<string name="advanced_unlock_scanning_error">Erro de desbloqueio do dispositivo: %1$s</string>
<string name="advanced_unlock_not_recognized">Não foi possível reconhecer a impressão de desbloqueio</string>
<string name="advanced_unlock_invalid_key">Não é possível ler a chave de desbloqueio do dispositivo. Exclua-o e repita o procedimento de reconhecimento de desbloqueio.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Extraia a credencial do banco de dados com os dados de desbloqueio do dispositivo</string>

View File

@@ -468,7 +468,6 @@
<string name="device_credential">Credencial do dispositivo</string>
<string name="credential_before_click_advanced_unlock_button">Digite a palavra-passe e depois clique neste botão.</string>
<string name="advanced_unlock_prompt_not_initialized">Não foi possível inicializar a solicitação de desbloqueio do dispositivo.</string>
<string name="advanced_unlock_scanning_error">Erro de desbloqueio do dispositivo: %1$s</string>
<string name="advanced_unlock_not_recognized">Não foi possível reconhecer a impressão de desbloqueio do dispositivo</string>
<string name="advanced_unlock_invalid_key">Não é possível ler a chave de desbloqueio do dispositivo. Elimine-a e repita o procedimento de reconhecimento de desbloqueio.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Extrair credencial da base de dados com dados de desbloqueio do dispositivo</string>

View File

@@ -446,7 +446,6 @@
<string name="accept">Aceitar</string>
<string name="device_credential">Credencial do dispositivo</string>
<string name="advanced_unlock_prompt_not_initialized">Não foi possível inicializar a solicitação de desbloqueio do dispositivo.</string>
<string name="advanced_unlock_scanning_error">Erro de desbloqueio do dispositivo: %1$s</string>
<string name="advanced_unlock_not_recognized">Não foi possível reconhecer a impressão de desbloqueio do dispositivo</string>
<string name="advanced_unlock_invalid_key">Não é possível ler a chave de desbloqueio do dispositivo. Elimine-a e repita o procedimento de reconhecimento de desbloqueio.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Extrair credencial da base de dados com dados de desbloqueio do dispositivo</string>

View File

@@ -597,7 +597,6 @@
<string name="warning_copy_permission">Permisiunea de notificare este necesară pentru a utiliza funcția de notificare a clipboardului.</string>
<string name="advanced_unlock_prompt_store_credential_title">Legătură la deblocarea dispozitivului</string>
<string name="advanced_unlock_prompt_extract_credential_title">Recunoașterea deblocării dispozitivului</string>
<string name="advanced_unlock_scanning_error">Eroare de deblocare a dispozitivului: %1$s</string>
<string name="unlock_and_link_biometric">Legătură de deblocare a dispozitivului</string>
<string name="configure_biometric">Nu se înrolează nicio credențială biometrică sau de dispozitiv.</string>
<string name="autofill_select_entry">Selectați intrarea…</string>

View File

@@ -498,7 +498,6 @@
<string name="advanced_unlock_prompt_extract_credential_title">Распознавание разблокировки устройства</string>
<string name="advanced_unlock_prompt_store_credential_message">При использовании разблокировки устройства вам всё равно необходимо помнить основные учётные данные.</string>
<string name="advanced_unlock_delete_all_key_warning">Удалить все ключи шифрования, связанные с распознаванием разблокировки устройства\?</string>
<string name="advanced_unlock_scanning_error">Ошибка разблокировки устройства: %1$s</string>
<string name="advanced_unlock_prompt_store_credential_title">Настройка разблокировки устройства</string>
<string name="menu_keystore_remove_key">Удалить ключ разблокировки устройства</string>
<string name="enter">Ввод</string>

View File

@@ -476,7 +476,6 @@
<string name="style_choose_title">Téma aplikácie</string>
<string name="protection">Ochrana</string>
<string name="configure_biometric">Nie sú zaregistrované žiadne biometrické údaje ani poverenia zariadenia.</string>
<string name="advanced_unlock_scanning_error">Chyba odomykania zariadenia: %1$s</string>
<string name="lock">Zamknúť</string>
<string name="custom_fields">Vlastné polia</string>
<string name="education_sort_summary">Vyberte spôsob triedenia záznamov a skupín.</string>

View File

@@ -344,7 +344,6 @@
<string name="show_entry_colors_title">Ngjyra zërash</string>
<string name="hide_expired_entries_title">Fshihi zërat e skaduar</string>
<string name="error_XML_malformed">XML e keqformuar.</string>
<string name="advanced_unlock_scanning_error">Gabim shkyçjeje pajisjeje: %1$s</string>
<string name="autofill_block">Blloko vetëplotësim</string>
<string name="allow_no_password_summary">Lejon prekjen e butoni “Hape”, nëse sjanë përzgjedhur kredenciale</string>
<string name="about_description">Sendërtim për Android i përgjegjësit KeePass të fjalëkalimeve</string>

View File

@@ -176,7 +176,6 @@
<string name="sort_groups_before">முன் குழுக்கள்</string>
<string name="warning_no_encryption_key">குறியாக்க விசை இல்லாமல் தொடரவா?</string>
<string name="warning_permanently_delete_nodes">தேர்ந்தெடுக்கப்பட்ட முனைகளை நிரந்தரமாக நீக்கவா?</string>
<string name="advanced_unlock_scanning_error">சாதனம் திறத்தல் பிழை: %1$s</string>
<string name="device_credential_unlock_enable_summary">தரவுத்தளத்தைத் திறக்க உங்கள் சாதன நற்சான்றிதழைப் பயன்படுத்தலாம்</string>
<string name="biometric_delete_all_key_title">குறியாக்க விசைகளை நீக்கு</string>
<string name="biometric_delete_all_key_summary">சாதன திறத்தல் ஏற்பு தொடர்பான அனைத்து குறியாக்க விசைகளையும் நீக்கு</string>

View File

@@ -525,7 +525,6 @@
<string name="advanced_unlock_invalid_key">อ่านกุญแจการปลดล็อกของอุปกรณ์ไม่ได้ โปรดลบข้อมูลออกและเพื่มข้อมูลการปลดล็อกด้วยอุปกรณ์อีกครั้ง</string>
<string name="advanced_unlock_not_recognized">ไม่รู้จักลายนิ้วมือ</string>
<string name="advanced_unlock_prompt_extract_credential_message">แยกข้อมูลประจำตัวออกด้วยข้อมูลการปลดล็อกด้วยอุปกรณ์</string>
<string name="advanced_unlock_scanning_error">การปลดล็อกด้วยอุปกรณ์ผิดพลาด: %1$s</string>
<string name="properties">คุณสมบัติ</string>
<string name="unavailable">ฐานข้อมูลนี้ยังไม่มีข้อมูลการเข้าสูระบบเลย</string>
<string name="database_history">ประวัติ</string>

View File

@@ -488,7 +488,6 @@
<string name="credential_before_click_advanced_unlock_button">Parolayı yazın ve ardından bu düğmeye tıklayın.</string>
<string name="advanced_unlock_prompt_not_initialized">Cihaz kilit açma istemi başlatılamıyor.</string>
<string name="unavailable">Kullanım dışı</string>
<string name="advanced_unlock_scanning_error">Cihaz kilit açma hatası: %1$s</string>
<string name="advanced_unlock_not_recognized">Cihaz kilit açma parmak izi tanınamadı</string>
<string name="advanced_unlock_invalid_key">Cihazın kilit açma anahtarı okunamıyor. Lütfen silin ve kilit açma tanıma prosedürünü tekrarlayın.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Cihaz kilit açma verileriyle veritabanı kimlik bilgilerini çıkarın</string>

View File

@@ -493,7 +493,6 @@
<string name="device_credential">Облікові дані пристрою</string>
<string name="credential_before_click_advanced_unlock_button">Введіть пароль, а потім натисніть цю кнопку.</string>
<string name="advanced_unlock_prompt_not_initialized">Не вдалося ініціалізувати запит на розблокування пристрою.</string>
<string name="advanced_unlock_scanning_error">Помилка розблокування пристрою: %1$s</string>
<string name="advanced_unlock_not_recognized">Не вдалося розпізнати розблокування пристрою</string>
<string name="advanced_unlock_invalid_key">Не вдалося розпізнати ключ розблокування пристрою. Видаліть його й повторіть процедуру створення ключа.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Витягування облікових даних бази даних за допомогою даних розблокування пристрою</string>

View File

@@ -400,7 +400,6 @@
<string name="encrypted_value_stored">Đã lưu trữ mật khẩu được mã hóa</string>
<string name="advanced_unlock_invalid_key">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.</string>
<string name="advanced_unlock_not_recognized">Không thể nhận dạng vân tay mở khóa thiết bị</string>
<string name="advanced_unlock_scanning_error">Lỗi mở khóa thiết bị: %1$s</string>
<string name="unavailable">Không có sẵn</string>
<string name="advanced_unlock_prompt_not_initialized">Không thể khởi tạo lời nhắc mở khóa thiết bị.</string>
<string name="credential_before_click_advanced_unlock_button">Nhập mật khẩu rồi nhấp vào nút này.</string>

View File

@@ -492,7 +492,6 @@
<string name="device_credential">设备凭据</string>
<string name="credential_before_click_advanced_unlock_button">输入密码,然后点击这个按钮。</string>
<string name="advanced_unlock_prompt_not_initialized">无法初始化设备解锁提示。</string>
<string name="advanced_unlock_scanning_error">设备解锁出错:%1$s</string>
<string name="advanced_unlock_not_recognized">无法识别设备解锁印记</string>
<string name="advanced_unlock_invalid_key">无法读取设备解锁密钥。请删除它,并重复解锁识别步骤。</string>
<string name="advanced_unlock_prompt_extract_credential_message">用设备解锁数据提取数据库凭据</string>

View File

@@ -29,7 +29,6 @@
<string name="advanced_unlock_prompt_not_initialized">無法初始化裝置解鎖提示。</string>
<string name="advanced_unlock_prompt_store_credential_message">即使你使用裝置解鎖識別,你仍然需要記住你的解鎖憑證。</string>
<string name="advanced_unlock_prompt_store_credential_title">裝置解鎖連線</string>
<string name="advanced_unlock_scanning_error">裝置解鎖出錯:%1$s</string>
<string name="advanced_unlock_tap_delete">點擊刪除裝置解鎖密鑰</string>
<string name="advanced_unlock_timeout">裝置解鎖超時</string>
<string name="allow">允許</string>

View File

@@ -407,7 +407,6 @@
<string name="encrypted_value_stored">Encrypted password stored</string>
<string name="advanced_unlock_invalid_key">Cannot read the device unlock key. Please delete it and repeat the unlock recognition procedure.</string>
<string name="advanced_unlock_not_recognized">Could not recognize device unlock print</string>
<string name="advanced_unlock_scanning_error">Device unlock error: %1$s</string>
<string name="unavailable">Unavailable</string>
<string name="advanced_unlock_prompt_not_initialized">Unable to initialize device unlock prompt.</string>
<string name="credential_before_click_advanced_unlock_button">Type in the password, and then click this button.</string>

View File

@@ -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"
}
}

View File

@@ -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"
}

View File

@@ -0,0 +1,3 @@
* Fix UnlockManager #2098 #2101
* Auto device unlock prompt #2105
* Small fixes ##2066

View File

@@ -0,0 +1 @@
* Fix auto prompt #2111

View File

@@ -0,0 +1,3 @@
* Correction UnlockManager #2098 #2101
* Invite de déverrouillage automatique de l'appareil #2105
* Petites corrections ##2066

View File

@@ -0,0 +1 @@
* Correction invite de commande auto #2111