diff --git a/CHANGELOG b/CHANGELOG
index 7aefac7cf..cf95e5f68 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,8 @@
+KeePassDX(2.9.5)
+ * Unlock database by device credentials (PIN/Password/Pattern) with Android M+ #102 #152 #811
+ * Prevent auto switch back to previous keyboard if otp field exists #814
+ * Fix timeout reset #817
+
KeePassDX(2.9.4)
* Fix small bugs #812
* Argon2ID implementation #791
diff --git a/app/build.gradle b/app/build.gradle
index c761f7d7a..cf10312de 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -12,8 +12,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 30
- versionCode = 48
- versionName = "2.9.4"
+ versionCode = 49
+ versionName = "2.9.5"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
index 8849d26fe..ef526b953 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
@@ -40,6 +40,7 @@ import com.google.android.material.appbar.CollapsingToolbarLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
+import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
@@ -133,7 +134,7 @@ class EntryActivity : LockingActivity() {
}
// Focus view to reinitialize timeout
- resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
+ coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
// Init the clipboard helper
clipboardHelper = ClipboardHelper(this)
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
index 698b69c4d..5134f1ffb 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
@@ -48,6 +48,7 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
+import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.icon.IconImage
@@ -134,7 +135,7 @@ class EntryEditActivity : LockingActivity(),
}
// Focus view to reinitialize timeout
- resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
+ coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt
index 2c5560487..acd3c5c37 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt
@@ -37,6 +37,7 @@ import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
+import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment
@@ -148,6 +149,8 @@ class EntryEditFragment: StylishFragment() {
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
taIconColor?.recycle()
+ rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext())
+
// Retrieve the new entry after an orientation change
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
index 6a55ac16f..17bcea82d 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
@@ -434,8 +434,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
when (item.itemId) {
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
}
-
- return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
+ MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
+ return super.onOptionsItemSelected(item)
}
companion object {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
index b8b1b57cf..d47e65241 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
@@ -50,6 +50,7 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity
+import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.Database
@@ -153,7 +154,7 @@ class GroupActivity : LockingActivity(),
taTextColor.recycle()
// Focus view to reinitialize timeout
- resetAppTimeoutWhenViewFocusedOrChanged(rootContainerView)
+ rootContainerView?.resetAppTimeoutWhenViewFocusedOrChanged(this)
// Retrieve elements after an orientation change
if (savedInstanceState != null) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt
index 755e3e493..5f0aaa811 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt
@@ -37,8 +37,8 @@ import android.widget.*
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
-import androidx.biometric.BiometricManager
import androidx.core.app.ActivityCompat
+import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
@@ -50,8 +50,7 @@ import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.autofill.AutofillHelper
-import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
-import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
+import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
@@ -69,14 +68,13 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
-import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException
-open class PasswordActivity : SpecialModeActivity() {
+open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
// Views
private var toolbar: Toolbar? = null
@@ -86,9 +84,8 @@ open class PasswordActivity : SpecialModeActivity() {
private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null
- private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
+ private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private var infoContainerView: ViewGroup? = null
- private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
@@ -114,7 +111,6 @@ open class PasswordActivity : SpecialModeActivity() {
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
- private var advancedUnlockedManager: AdvancedUnlockedManager? = null
private var mAllowAutoOpenBiometricPrompt: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
@@ -134,7 +130,6 @@ open class PasswordActivity : SpecialModeActivity() {
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
- advancedUnlockInfoView = findViewById(R.id.biometric_info)
infoContainerView = findViewById(R.id.activity_password_info_container)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
@@ -161,10 +156,6 @@ open class PasswordActivity : SpecialModeActivity() {
}
})
- enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
- enableOrNotTheConfirmationButton()
- }
-
// If is a view intent
getUriFromIntent(intent)
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
@@ -174,6 +165,24 @@ open class PasswordActivity : SpecialModeActivity() {
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
}
+ // Init Biometric elements
+ advancedUnlockFragment = supportFragmentManager
+ .findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
+ if (advancedUnlockFragment == null) {
+ advancedUnlockFragment = AdvancedUnlockFragment()
+ supportFragmentManager.commit {
+ replace(R.id.fragment_advanced_unlock_container_view,
+ advancedUnlockFragment!!,
+ UNLOCK_FRAGMENT_TAG)
+ }
+ }
+
+ // Listen password checkbox to init advanced unlock and confirmation button
+ checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
+ advancedUnlockFragment?.checkUnlockAvailability()
+ enableOrNotTheConfirmationButton()
+ }
+
// Observe if default database
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
mDefaultDatabase = isDefaultDatabase
@@ -207,12 +216,7 @@ open class PasswordActivity : SpecialModeActivity() {
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
// Recheck advanced unlock if error
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- if (PreferencesUtil.isAdvancedUnlockEnable(this@PasswordActivity)) {
- // Stay with the same mode and init it
- advancedUnlockedManager?.initAdvancedUnlockMode()
- }
- }
+ advancedUnlockFragment?.initAdvancedUnlockMode()
if (result.isSuccess) {
mDatabaseKeyFileUri = null
@@ -320,6 +324,33 @@ open class PasswordActivity : SpecialModeActivity() {
finish()
}
+ override fun retrieveCredentialForEncryption(): String {
+ return passwordView?.text?.toString() ?: ""
+ }
+
+ override fun conditionToStoreCredential(): Boolean {
+ return checkboxPasswordView?.isChecked == true
+ }
+
+ override fun onCredentialEncrypted(databaseUri: Uri,
+ encryptedCredential: String,
+ ivSpec: String) {
+ // Load the database if password is registered with biometric
+ verifyCheckboxesAndLoadDatabase(
+ CipherDatabaseEntity(
+ databaseUri.toString(),
+ encryptedCredential,
+ ivSpec)
+ )
+ }
+
+ override fun onCredentialDecrypted(databaseUri: Uri,
+ decryptedCredential: String) {
+ // Load the database if password is retrieve from biometric
+ // Retrieve from biometric
+ verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
+ }
+
private val onEditorActionListener = object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId == IME_ACTION_DONE) {
@@ -386,48 +417,9 @@ open class PasswordActivity : SpecialModeActivity() {
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
} else {
// Init Biometric elements
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- if (PreferencesUtil.isAdvancedUnlockEnable(this)) {
- if (advancedUnlockedManager == null
- && databaseFileUri != null) {
- advancedUnlockedManager = AdvancedUnlockedManager(this,
- databaseFileUri,
- advancedUnlockInfoView,
- checkboxPasswordView,
- enableButtonOnCheckedChangeListener,
- passwordView,
- { passwordEncrypted, ivSpec ->
- // Load the database if password is registered with biometric
- if (passwordEncrypted != null && ivSpec != null) {
- verifyCheckboxesAndLoadDatabase(
- CipherDatabaseEntity(
- databaseFileUri.toString(),
- passwordEncrypted,
- ivSpec)
- )
- }
- },
- { passwordDecrypted ->
- // Load the database if password is retrieve from biometric
- passwordDecrypted?.let {
- // Retrieve from biometric
- verifyKeyFileCheckboxAndLoadDatabase(it)
- }
- })
- }
- advancedUnlockedManager?.isBiometricPromptAutoOpenEnable =
- mAllowAutoOpenBiometricPrompt && mProgressDatabaseTaskProvider?.isBinded() != true
- advancedUnlockedManager?.checkBiometricAvailability()
- } else {
- advancedUnlockInfoView?.visibility = View.GONE
- advancedUnlockedManager?.destroy()
- advancedUnlockedManager = null
- }
- }
- if (advancedUnlockedManager == null) {
- checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
- }
- checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
+ advancedUnlockFragment?.loadDatabase(databaseFileUri,
+ mAllowAutoOpenBiometricPrompt
+ && mProgressDatabaseTaskProvider?.isBinded() != true)
}
enableOrNotTheConfirmationButton()
@@ -479,11 +471,6 @@ open class PasswordActivity : SpecialModeActivity() {
override fun onPause() {
mProgressDatabaseTaskProvider?.unregisterProgressTask()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- advancedUnlockedManager?.destroy()
- advancedUnlockedManager = null
- }
-
// Reinit locking activity UI variable
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
mAllowAutoOpenBiometricPrompt = true
@@ -592,11 +579,6 @@ open class PasswordActivity : SpecialModeActivity() {
MenuUtil.defaultMenuInflater(inflater, menu)
}
- if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- // biometric menu
- advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
- }
-
super.onCreateOptionsMenu(menu)
launchEducation(menu)
@@ -672,21 +654,14 @@ open class PasswordActivity : SpecialModeActivity() {
performedNextEducation(passwordActivityEducation, menu)
})
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
- && !readOnlyEducationPerformed) {
- val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(this)
- PreferencesUtil.isAdvancedUnlockEnable(applicationContext)
- && (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
- && advancedUnlockInfoView != null && advancedUnlockInfoView?.visibility == View.VISIBLE
- && advancedUnlockInfoView?.unlockIconImageView != null
- && passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
- {
- performedNextEducation(passwordActivityEducation, menu)
- },
- {
- performedNextEducation(passwordActivityEducation, menu)
- })
- }
+ advancedUnlockFragment?.performEducation(passwordActivityEducation,
+ readOnlyEducationPerformed,
+ {
+ performedNextEducation(passwordActivityEducation, menu)
+ },
+ {
+ performedNextEducation(passwordActivityEducation, menu)
+ })
}
}
@@ -708,10 +683,7 @@ open class PasswordActivity : SpecialModeActivity() {
readOnly = !readOnly
changeOpenFileReadIcon(item)
}
- R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- advancedUnlockedManager?.deleteEncryptedDatabaseKey()
- }
- else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
+ else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
}
return super.onOptionsItemSelected(item)
@@ -725,6 +697,9 @@ open class PasswordActivity : SpecialModeActivity() {
mAllowAutoOpenBiometricPrompt = false
+ // To get device credential unlock result
+ advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
+
// To get entry in result
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
@@ -758,6 +733,8 @@ open class PasswordActivity : SpecialModeActivity() {
private val TAG = PasswordActivity::class.java.name
+ private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
+
private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW"
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt
index d7f15bdcd..2e4fa32cb 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt
@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.activities.lock
import android.annotation.SuppressLint
+import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MotionEvent
@@ -163,35 +164,6 @@ abstract class LockingActivity : SpecialModeActivity() {
sendBroadcast(Intent(LOCK_ACTION))
}
- /**
- * To reset the app timeout when a view is focused or changed
- */
- @SuppressLint("ClickableViewAccessibility")
- protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
- views.forEach {
- it?.setOnTouchListener { _, event ->
- when (event.action) {
- MotionEvent.ACTION_DOWN -> {
- // Log.d(TAG, "View touched, try to reset app timeout")
- TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
- }
- }
- false
- }
- it?.setOnFocusChangeListener { _, hasFocus ->
- if (hasFocus) {
- // Log.d(TAG, "View focused, try to reset app timeout")
- TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
- }
- }
- if (it is ViewGroup) {
- for (i in 0..it.childCount) {
- resetAppTimeoutWhenViewFocusedOrChanged(it.getChildAt(i))
- }
- }
- }
- }
-
override fun onBackPressed() {
if (mTimeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
@@ -204,7 +176,7 @@ abstract class LockingActivity : SpecialModeActivity() {
companion object {
- private const val TAG = "LockingActivity"
+ const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450
@@ -215,3 +187,28 @@ abstract class LockingActivity : SpecialModeActivity() {
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
}
}
+
+/**
+ * To reset the app timeout when a view is focused or changed
+ */
+@SuppressLint("ClickableViewAccessibility")
+fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context) {
+ setOnTouchListener { _, event ->
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ //Log.d(LockingActivity.TAG, "View touched, try to reset app timeout")
+ TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
+ }
+ }
+ false
+ }
+ setOnFocusChangeListener { _, _ ->
+ //Log.d(LockingActivity.TAG, "View focused, try to reset app timeout")
+ TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
+ }
+ if (this is ViewGroup) {
+ for (i in 0..childCount) {
+ getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context)
+ }
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockCryptoPrompt.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockCryptoPrompt.kt
new file mode 100644
index 000000000..26e9baff4
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockCryptoPrompt.kt
@@ -0,0 +1,10 @@
+package com.kunzisoft.keepass.biometric
+
+import androidx.annotation.StringRes
+import javax.crypto.Cipher
+
+data class AdvancedUnlockCryptoPrompt(var cipher: Cipher,
+ @StringRes var promptTitleId: Int,
+ @StringRes var promptDescriptionId: Int? = null,
+ var isDeviceCredentialOperation: Boolean,
+ var isBiometricOperation: Boolean)
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt
new file mode 100644
index 000000000..b00528d54
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt
@@ -0,0 +1,620 @@
+/*
+ * Copyright 2020 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.biometric
+
+import android.content.Context
+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.*
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import com.getkeepsafe.taptargetview.TapTargetView
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.activities.stylish.StylishFragment
+import com.kunzisoft.keepass.app.database.CipherDatabaseAction
+import com.kunzisoft.keepass.database.exception.IODatabaseException
+import com.kunzisoft.keepass.education.PasswordActivityEducation
+import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
+import com.kunzisoft.keepass.settings.PreferencesUtil
+import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
+
+class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
+
+ private var mBuilderListener: BuilderListener? = null
+
+ 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
+
+ /**
+ * Manage setting to auto open biometric prompt
+ */
+ private var mAutoOpenPrompt: Boolean = false
+ get() {
+ return field && mAutoOpenPromptEnabled
+ }
+
+ // 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.DatabaseListener? = null
+
+ // 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
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+
+ mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(context)
+ mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ mBuilderListener = context as BuilderListener
+ }
+ } catch (e: ClassCastException) {
+ throw ClassCastException(context.toString()
+ + " must implement " + BuilderListener::class.java.name)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ retainInstance = true
+ setHasOptionsMenu(true)
+
+ cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ super.onCreateView(inflater, container, savedInstanceState)
+
+ val rootView = inflater.cloneInContext(contextThemed)
+ .inflate(R.layout.fragment_advanced_unlock, container, false)
+
+ mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view)
+
+ return rootView
+ }
+
+ private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?)
+ private var activityResult: ActivityResult? = null
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ // To wait resume
+ activityResult = ActivityResult(requestCode, resultCode, data)
+ keepConnection = false
+
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(requireContext())
+ mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())
+ keepConnection = false
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ super.onCreateOptionsMenu(menu, inflater)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ // biometric menu
+ if (mAllowAdvancedUnlockMenu)
+ inflater.inflate(R.menu.advanced_unlock, menu)
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ deleteEncryptedDatabaseKey()
+ }
+ }
+
+ return super.onOptionsItemSelected(item)
+ }
+
+ fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ // To get device credential unlock result, only if same database uri
+ if (databaseUri != null
+ && mAdvancedUnlockEnabled) {
+ activityResult?.let {
+ if (databaseUri == databaseFileUri) {
+ advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode)
+ } else {
+ disconnect()
+ }
+ } ?: run {
+ connect(databaseUri)
+ this.mAutoOpenPrompt = autoOpenPrompt
+ }
+ } else {
+ disconnect()
+ }
+ activityResult = null
+ }
+ }
+
+ /**
+ * Check unlock availability and change the current mode depending of device's state
+ */
+ fun checkUnlockAvailability() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ allowOpenBiometricPrompt = true
+ if (PreferencesUtil.isBiometricUnlockEnable(requireContext())) {
+ mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
+
+ // biometric not supported (by API level or hardware) so keep option hidden
+ // or manually disable
+ val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
+ if (!PreferencesUtil.isAdvancedUnlockEnable(requireContext())
+ || 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(requireContext())) {
+ mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
+ if (AdvancedUnlockManager.isDeviceSecure(requireContext())) {
+ 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 (mBuilderListener?.conditionToStoreCredential() == true) {
+ // 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 {
+ // wait for typing
+ Mode.WAIT_CREDENTIAL
+ })
+ }
+ } ?: throw IODatabaseException()
+ }
+ }
+ }
+
+ @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(false, null)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun openBiometricSetting() {
+ mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
+ // ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
+ requireContext().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.no_credentials_stored)
+ setAdvancedUnlockedMessageView("")
+
+ mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
+ onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
+ requireContext().getString(R.string.credential_before_click_advanced_unlock_button))
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
+ requireActivity().runOnUiThread {
+ if (allowOpenBiometricPrompt) {
+ if (cryptoPrompt.isDeviceCredentialOperation)
+ keepConnection = true
+ try {
+ advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt)
+ } 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.open_advanced_unlock_prompt_store_credential)
+ setAdvancedUnlockedMessageView("")
+
+ advancedUnlockManager?.initEncryptData { cryptoPrompt ->
+ // Set listener to open the biometric dialog and save credential
+ mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
+ openAdvancedUnlockPrompt(cryptoPrompt)
+ }
+ } ?: throw Exception("AdvancedUnlockHelper not initialized")
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initDecryptData() {
+ showViews(true)
+ setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database)
+ 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 (mAutoOpenPrompt) {
+ mAutoOpenPrompt = false
+ openAdvancedUnlockPrompt(cryptoPrompt)
+ }
+ }
+ } ?: deleteEncryptedDatabaseKey()
+ }
+ } ?: throw IODatabaseException()
+ } ?: throw Exception("AdvancedUnlockHelper not initialized")
+ }
+
+ @Synchronized
+ fun initAdvancedUnlockMode() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ mAllowAdvancedUnlockMenu = false
+ 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()
+ }
+ 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
+ requireActivity().invalidateOptionsMenu()
+ }
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ fun connect(databaseUri: Uri) {
+ showViews(true)
+ this.databaseFileUri = databaseUri
+ cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
+ override fun onDatabaseCleared() {
+ deleteEncryptedDatabaseKey()
+ }
+ }
+ 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() {
+ allowOpenBiometricPrompt = false
+ mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
+ advancedUnlockManager?.closeBiometricPrompt()
+ databaseFileUri?.let { databaseUri ->
+ cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
+ checkUnlockAvailability()
+ }
+ }
+ }
+
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ requireActivity().runOnUiThread {
+ Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
+ setAdvancedUnlockedMessageView(errString.toString())
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onAuthenticationFailed() {
+ requireActivity().runOnUiThread {
+ Log.e(TAG, "Biometric authentication failed, biometric not recognized")
+ setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onAuthenticationSucceeded() {
+ requireActivity().runOnUiThread {
+ 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
+ mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
+ advancedUnlockManager?.encryptData(credential)
+ }
+ AdvancedUnlockNotificationService.startServiceForTimeout(requireContext())
+ }
+ Mode.EXTRACT_CREDENTIAL -> {
+ // retrieve the encrypted value from preferences
+ databaseFileUri?.let { databaseUri ->
+ cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
+ cipherDatabase?.encryptedValue?.let { value ->
+ advancedUnlockManager?.decryptData(value)
+ } ?: deleteEncryptedDatabaseKey()
+ }
+ } ?: throw IODatabaseException()
+ }
+ }
+ }
+ }
+
+ override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
+ databaseFileUri?.let { databaseUri ->
+ mBuilderListener?.onCredentialEncrypted(databaseUri, encryptedValue, ivSpec)
+ }
+ }
+
+ override fun handleDecryptedResult(decryptedValue: String) {
+ // Load database directly with password retrieve
+ databaseFileUri?.let {
+ mBuilderListener?.onCredentialDecrypted(it, decryptedValue)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onInvalidKeyException(e: Exception) {
+ setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
+ }
+
+ override fun onGenericException(e: Exception) {
+ val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
+ setAdvancedUnlockedMessageView(errorMessage)
+ }
+
+ private fun showViews(show: Boolean) {
+ requireActivity().runOnUiThread {
+ mAdvancedUnlockInfoView?.visibility = if (show)
+ View.VISIBLE
+ else {
+ View.GONE
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun setAdvancedUnlockedTitleView(textId: Int) {
+ requireActivity().runOnUiThread {
+ mAdvancedUnlockInfoView?.setTitle(textId)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun setAdvancedUnlockedMessageView(textId: Int) {
+ requireActivity().runOnUiThread {
+ mAdvancedUnlockInfoView?.setMessage(textId)
+ }
+ }
+
+ private fun setAdvancedUnlockedMessageView(text: CharSequence) {
+ requireActivity().runOnUiThread {
+ mAdvancedUnlockInfoView?.message = text
+ }
+ }
+
+ fun performEducation(passwordActivityEducation: PasswordActivityEducation,
+ readOnlyEducationPerformed: Boolean,
+ onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
+ onOuterViewClick: ((TapTargetView?) -> Unit)? = null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && !readOnlyEducationPerformed) {
+ val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
+ PreferencesUtil.isAdvancedUnlockEnable(requireContext())
+ && (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
+ || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
+ && mAdvancedUnlockInfoView != null && mAdvancedUnlockInfoView?.visibility == View.VISIBLE
+ && mAdvancedUnlockInfoView?.unlockIconImageView != null
+ && passwordActivityEducation.checkAndPerformedBiometricEducation(mAdvancedUnlockInfoView!!.unlockIconImageView!!,
+ onEducationViewClick,
+ onOuterViewClick)
+ }
+ }
+
+ enum class Mode {
+ BIOMETRIC_UNAVAILABLE,
+ BIOMETRIC_SECURITY_UPDATE_REQUIRED,
+ DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
+ KEY_MANAGER_UNAVAILABLE,
+ WAIT_CREDENTIAL,
+ STORE_CREDENTIAL,
+ EXTRACT_CREDENTIAL
+ }
+
+ interface BuilderListener {
+ fun retrieveCredentialForEncryption(): String
+ fun conditionToStoreCredential(): Boolean
+ fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String)
+ fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String)
+ }
+
+ 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
+ mBuilderListener = null
+ }
+
+ super.onDestroy()
+ }
+
+ override fun onDetach() {
+ mBuilderListener = null
+
+ super.onDetach()
+ }
+
+ companion object {
+
+ private val TAG = AdvancedUnlockFragment::class.java.name
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt
similarity index 51%
rename from app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt
rename to app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt
index b0b060073..c183bc893 100644
--- a/app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 Jeremy Jamet / Kunzisoft.
+ * Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -19,6 +19,7 @@
*/
package com.kunzisoft.keepass.biometric
+import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
@@ -31,6 +32,7 @@ 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.settings.PreferencesUtil
@@ -44,48 +46,74 @@ import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
@RequiresApi(api = Build.VERSION_CODES.M)
-class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
-
- private var biometricPrompt: BiometricPrompt? = null
+class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) {
private var keyStore: KeyStore? = null
private var keyGenerator: KeyGenerator? = null
private var cipher: Cipher? = null
- private var keyguardManager: KeyguardManager? = null
- private var cryptoObject: BiometricPrompt.CryptoObject? = 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
- var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
- var biometricUnlockCallback: BiometricUnlockCallback? = null
- private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(context)
+ private val biometricUnlockEnable = PreferencesUtil.isBiometricUnlockEnable(retrieveContext())
+ private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(retrieveContext())
val isKeyManagerInitialized: Boolean
get() {
if (!isKeyManagerInit) {
- biometricUnlockCallback?.onBiometricException(Exception("Biometric not initialized"))
+ 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 (allowInitKeyStore(context)) {
- this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
+ if (isDeviceSecure(retrieveContext())
+ && (biometricUnlockEnable || deviceCredentialUnlockEnable)) {
try {
- this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
- this.keyGenerator = KeyGenerator.getInstance(BIOMETRIC_KEY_ALGORITHM, BIOMETRIC_KEYSTORE)
+ this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
+ this.keyGenerator = KeyGenerator.getInstance(ADVANCED_UNLOCK_KEY_ALGORITHM, ADVANCED_UNLOCK_KEYSTORE)
this.cipher = Cipher.getInstance(
- BIOMETRIC_KEY_ALGORITHM + "/"
- + BIOMETRIC_BLOCKS_MODES + "/"
- + BIOMETRIC_ENCRYPTION_PADDING)
- this.cryptoObject = BiometricPrompt.CryptoObject(cipher!!)
+ 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
- biometricUnlockCallback?.onBiometricException(e)
+ advancedUnlockCallback?.onGenericException(e)
}
} else {
// really not much to do when no fingerprint support found
@@ -103,22 +131,20 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
keyStore.load(null)
try {
- if (!keyStore.containsAlias(BIOMETRIC_KEYSTORE_KEY)) {
+ 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(
- BIOMETRIC_KEYSTORE_KEY,
+ ADVANCED_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
// Require the user to authenticate with a fingerprint to authorize every use
- // of the key
- .setUserAuthenticationRequired(true)
+ // of the key, don't use it for device credential because it's the user authentication
.apply {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
- && deviceCredentialUnlockEnable) {
- setUserAuthenticationParameters(0, KeyProperties.AUTH_DEVICE_CREDENTIAL)
+ if (biometricUnlockEnable) {
+ setUserAuthenticationRequired(true)
}
}
.build())
@@ -126,56 +152,46 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
}
} catch (e: Exception) {
Log.e(TAG, "Unable to create a key in keystore", e)
- biometricUnlockCallback?.onBiometricException(e)
+ advancedUnlockCallback?.onGenericException(e)
}
- return keyStore.getKey(BIOMETRIC_KEYSTORE_KEY, null) as SecretKey?
+ return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve the key in keystore", e)
- biometricUnlockCallback?.onBiometricException(e)
+ advancedUnlockCallback?.onGenericException(e)
}
return null
}
fun initEncryptData(actionIfCypherInit
- : (biometricPrompt: BiometricPrompt?,
- cryptoObject: BiometricPrompt.CryptoObject?,
- promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
+ : (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
if (!isKeyManagerInitialized) {
return
}
try {
- // TODO if (keyguardManager?.isDeviceSecure == true) {
getSecretKey()?.let { secretKey ->
- cipher?.init(Cipher.ENCRYPT_MODE, secretKey)
-
- initBiometricPrompt()
-
- val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder().apply {
- setTitle(context.getString(R.string.advanced_unlock_prompt_store_credential_title))
- setDescription(context.getString(R.string.advanced_unlock_prompt_store_credential_message))
- setConfirmationRequired(true)
- if (deviceCredentialUnlockEnable) {
- setAllowedAuthenticators(DEVICE_CREDENTIAL)
- } else {
- setNegativeButtonText(context.getString(android.R.string.cancel))
- }
- }.build()
-
- actionIfCypherInit.invoke(biometricPrompt,
- cryptoObject,
- promptInfoStoreCredential)
+ 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)
- biometricUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
+ advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
- biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
+ advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e)
- biometricUnlockCallback?.onBiometricException(e)
+ advancedUnlockCallback?.onGenericException(e)
}
}
@@ -190,57 +206,46 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
// passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP)
- biometricUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
+ advancedUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
}
} catch (e: Exception) {
- val exception = Exception(context.getString(R.string.keystore_not_accessible), e)
Log.e(TAG, "Unable to encrypt data", e)
- biometricUnlockCallback?.onBiometricException(exception)
+ advancedUnlockCallback?.onGenericException(e)
}
}
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
- : (biometricPrompt: BiometricPrompt?,
- cryptoObject: BiometricPrompt.CryptoObject?,
- promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
+ : (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
if (!isKeyManagerInitialized) {
return
}
try {
- // TODO if (keyguardManager?.isDeviceSecure == true) {
// important to restore spec here that was used for decryption
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP)
val spec = IvParameterSpec(iv)
getSecretKey()?.let { secretKey ->
- cipher?.init(Cipher.DECRYPT_MODE, secretKey, spec)
-
- initBiometricPrompt()
-
- val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
- setTitle(context.getString(R.string.advanced_unlock_prompt_extract_credential_title))
- //setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
- setConfirmationRequired(false)
- if (deviceCredentialUnlockEnable) {
- setAllowedAuthenticators(DEVICE_CREDENTIAL)
- } else {
- setNegativeButtonText(context.getString(android.R.string.cancel))
- }
- }.build()
-
- actionIfCypherInit.invoke(biometricPrompt,
- cryptoObject,
- promptInfoExtractCredential)
+ 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)
deleteKeystoreKey()
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
- biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
+ advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e)
- biometricUnlockCallback?.onBiometricException(e)
+ advancedUnlockCallback?.onGenericException(e)
}
}
@@ -252,33 +257,73 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
// actual decryption here
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP)
cipher?.doFinal(encrypted)?.let { decrypted ->
- biometricUnlockCallback?.handleDecryptedResult(String(decrypted))
+ advancedUnlockCallback?.handleDecryptedResult(String(decrypted))
}
} catch (badPaddingException: BadPaddingException) {
Log.e(TAG, "Unable to decrypt data", badPaddingException)
- biometricUnlockCallback?.onInvalidKeyException(badPaddingException)
+ advancedUnlockCallback?.onInvalidKeyException(badPaddingException)
} catch (e: Exception) {
- val exception = Exception(context.getString(R.string.keystore_not_accessible), e)
- Log.e(TAG, "Unable to decrypt data", exception)
- biometricUnlockCallback?.onBiometricException(exception)
+ Log.e(TAG, "Unable to decrypt data", e)
+ advancedUnlockCallback?.onGenericException(e)
}
}
fun deleteKeystoreKey() {
try {
keyStore?.load(null)
- keyStore?.deleteEntry(BIOMETRIC_KEYSTORE_KEY)
+ keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
} catch (e: Exception) {
Log.e(TAG, "Unable to delete entry key in keystore", e)
- biometricUnlockCallback?.onBiometricException(e)
+ advancedUnlockCallback?.onGenericException(e)
}
}
+ @Suppress("DEPRECATION")
@Synchronized
- fun initBiometricPrompt() {
+ fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
+ // Init advanced unlock prompt
if (biometricPrompt == null) {
- authenticationCallback?.let {
- biometricPrompt = BiometricPrompt(context, Executors.newSingleThreadExecutor(), it)
+ 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)
+ retrieveContext().startActivityForResult(
+ keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription),
+ REQUEST_DEVICE_CREDENTIAL)
+ }
+ }
+
+ @Synchronized
+ fun onActivityResult(requestCode: Int, resultCode: Int) {
+ if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
+ if (resultCode == Activity.RESULT_OK) {
+ advancedUnlockCallback?.onAuthenticationSucceeded()
+ } else {
+ advancedUnlockCallback?.onAuthenticationFailed()
}
}
}
@@ -287,25 +332,30 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
biometricPrompt?.cancelAuthentication()
}
- interface BiometricUnlockErrorCallback {
+ interface AdvancedUnlockErrorCallback {
fun onInvalidKeyException(e: Exception)
- fun onBiometricException(e: Exception)
+ fun onGenericException(e: Exception)
}
- interface BiometricUnlockCallback : BiometricUnlockErrorCallback {
+ interface AdvancedUnlockCallback : AdvancedUnlockErrorCallback {
+ fun onAuthenticationSucceeded()
+ fun onAuthenticationFailed()
+ fun onAuthenticationError(errorCode: Int, errString: CharSequence)
fun handleEncryptedResult(encryptedValue: String, ivSpec: String)
fun handleDecryptedResult(decryptedValue: String)
}
companion object {
- private val TAG = BiometricUnlockDatabaseHelper::class.java.name
+ 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
- private const val BIOMETRIC_KEYSTORE = "AndroidKeyStore"
- private const val BIOMETRIC_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
- private const val BIOMETRIC_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
- private const val BIOMETRIC_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
- private const val BIOMETRIC_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
+ private const val REQUEST_DEVICE_CREDENTIAL = 556
@RequiresApi(api = Build.VERSION_CODES.M)
fun canAuthenticate(context: Context): Int {
@@ -337,11 +387,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
}
@RequiresApi(api = Build.VERSION_CODES.M)
- fun allowInitKeyStore(context: Context): Boolean {
- val biometricCanAuthenticate = canAuthenticate(context)
- return ( biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
- || biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
- )
+ fun isDeviceSecure(context: Context): Boolean {
+ val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java)
+ return keyguardManager?.isDeviceSecure ?: false
}
@RequiresApi(api = Build.VERSION_CODES.M)
@@ -365,36 +413,48 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
)
}
- @RequiresApi(api = Build.VERSION_CODES.R)
+ @RequiresApi(api = Build.VERSION_CODES.M)
fun deviceCredentialUnlockSupported(context: Context): Boolean {
- val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
- 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
- )
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
+ 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
+ )
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ ContextCompat.getSystemService(context, KeyguardManager::class.java)?.apply {
+ return isDeviceSecure
+ }
+ }
+ return false
}
/**
* Remove entry key in keystore
*/
@RequiresApi(api = Build.VERSION_CODES.M)
- fun deleteEntryKeyInKeystoreForBiometric(context: FragmentActivity,
- biometricCallback: BiometricUnlockErrorCallback) {
- BiometricUnlockDatabaseHelper(context).apply {
- biometricUnlockCallback = object : BiometricUnlockCallback {
+ 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: String, ivSpec: String) {}
override fun handleDecryptedResult(decryptedValue: String) {}
override fun onInvalidKeyException(e: Exception) {
- biometricCallback.onInvalidKeyException(e)
+ advancedCallback.onInvalidKeyException(e)
}
- override fun onBiometricException(e: Exception) {
- biometricCallback.onBiometricException(e)
+ override fun onGenericException(e: Exception) {
+ advancedCallback.onGenericException(e)
}
}
deleteKeystoreKey()
diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt
deleted file mode 100644
index 1396c6a79..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt
+++ /dev/null
@@ -1,422 +0,0 @@
-/*
- * Copyright 2019 Jeremy Jamet / Kunzisoft.
- *
- * This file is part of KeePassDX.
- *
- * KeePassDX is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * KeePassDX is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with KeePassDX. If not, see .
- *
- */
-package com.kunzisoft.keepass.biometric
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Build
-import android.provider.Settings
-import android.util.Log
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.View
-import android.widget.CompoundButton
-import android.widget.TextView
-import androidx.annotation.RequiresApi
-import androidx.biometric.BiometricManager
-import androidx.biometric.BiometricPrompt
-import androidx.fragment.app.FragmentActivity
-import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.app.database.CipherDatabaseAction
-import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
-import com.kunzisoft.keepass.settings.PreferencesUtil
-import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
-
-@RequiresApi(api = Build.VERSION_CODES.M)
-class AdvancedUnlockedManager(var context: FragmentActivity,
- var databaseFileUri: Uri,
- private var advancedUnlockInfoView: AdvancedUnlockInfoView?,
- private var checkboxPasswordView: CompoundButton?,
- private var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null,
- var passwordView: TextView?,
- private var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit,
- private var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit)
- : BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
-
- private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
- private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
-
- // Only to fix multiple fingerprint menu #332
- private var mAllowAdvancedUnlockMenu = false
- private var mAddBiometricMenuInProgress = false
-
- /**
- * Manage setting to auto open biometric prompt
- */
- private var biometricPromptAutoOpenPreference = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
- var isBiometricPromptAutoOpenEnable: Boolean = false
- get() {
- return field && biometricPromptAutoOpenPreference
- }
-
- // 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 var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
-
- private val cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
- override fun onDatabaseCleared() {
- deleteEncryptedDatabaseKey()
- }
- }
-
- init {
- // Add a check listener to change fingerprint mode
- checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked ->
- checkBiometricAvailability()
- // Add old listener to enable the button, only be call here because of onCheckedChange bug
- onCheckedPasswordChangeListener?.onCheckedChanged(compoundButton, checked)
- }
- cipherDatabaseAction.apply {
- reloadPreferences()
- registerDatabaseListener(cipherDatabaseListener)
- }
- }
-
- /**
- * Check biometric availability and change the current mode depending of device's state
- */
- fun checkBiometricAvailability() {
-
- if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
- advancedUnlockInfoView?.setIconResource(R.drawable.bolt)
- } else if (PreferencesUtil.isBiometricUnlockEnable(context)) {
- advancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
- }
-
- // biometric not supported (by API level or hardware) so keep option hidden
- // or manually disable
- val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(context)
- allowOpenBiometricPrompt = true
-
- 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.BIOMETRIC_NOT_CONFIGURED)
- } else {
- // Check if fingerprint well init (be called the first time the fingerprint is configured
- // and the activity still active)
- if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
- biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context)
- // callback for fingerprint findings
- biometricUnlockDatabaseHelper?.biometricUnlockCallback = this
- biometricUnlockDatabaseHelper?.authenticationCallback = biometricAuthenticationCallback
- }
- // Recheck to change the mode
- if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
- toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
- } else {
- if (checkboxPasswordView?.isChecked == true) {
- // listen for encryption
- toggleMode(Mode.STORE_CREDENTIAL)
- } else {
- cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { 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 {
- // wait for typing
- Mode.WAIT_CREDENTIAL
- })
- }
- }
- }
- }
- }
- }
-
- private fun toggleMode(newBiometricMode: Mode) {
- if (newBiometricMode != biometricMode) {
- biometricMode = newBiometricMode
- initAdvancedUnlockMode()
- }
- }
-
- private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback () {
-
- override fun onAuthenticationError(
- errorCode: Int,
- errString: CharSequence) {
- context.runOnUiThread {
- Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
- setAdvancedUnlockedMessageView(errString.toString())
- }
- }
-
- override fun onAuthenticationFailed() {
- context.runOnUiThread {
- Log.e(TAG, "Biometric authentication failed, biometric not recognized")
- setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
- }
- }
-
- override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
- context.runOnUiThread {
- when (biometricMode) {
- Mode.BIOMETRIC_UNAVAILABLE -> {
- }
- Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
- }
- Mode.BIOMETRIC_NOT_CONFIGURED -> {
- }
- Mode.KEY_MANAGER_UNAVAILABLE -> {
- }
- Mode.WAIT_CREDENTIAL -> {
- }
- Mode.STORE_CREDENTIAL -> {
- // newly store the entered password in encrypted way
- biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
- AdvancedUnlockNotificationService.startServiceForTimeout(context)
- }
- Mode.EXTRACT_CREDENTIAL -> {
- // retrieve the encrypted value from preferences
- cipherDatabaseAction.getCipherDatabase(databaseFileUri) { cipherDatabase ->
- cipherDatabase?.encryptedValue?.let { value ->
- biometricUnlockDatabaseHelper?.decryptData(value)
- } ?: deleteEncryptedDatabaseKey()
- }
- }
- }
- }
- }
- }
-
- private fun initNotAvailable() {
- showFingerPrintViews(false)
-
- advancedUnlockInfoView?.setIconViewClickListener(false, null)
- }
-
- @Suppress("DEPRECATION")
- private fun openBiometricSetting() {
- advancedUnlockInfoView?.setIconViewClickListener(false) {
- // ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
- context.startActivity(Intent(Settings.ACTION_SETTINGS))
- }
- }
-
- private fun initSecurityUpdateRequired() {
- showFingerPrintViews(true)
- setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
-
- openBiometricSetting()
- }
-
- private fun initNotConfigured() {
- showFingerPrintViews(true)
- setAdvancedUnlockedTitleView(R.string.configure_biometric)
- setAdvancedUnlockedMessageView("")
-
- openBiometricSetting()
- }
-
- private fun initKeyManagerNotAvailable() {
- showFingerPrintViews(true)
- setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
-
- openBiometricSetting()
- }
-
- private fun initWaitData() {
- showFingerPrintViews(true)
- setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
- setAdvancedUnlockedMessageView("")
-
- advancedUnlockInfoView?.setIconViewClickListener(false) {
- biometricAuthenticationCallback.onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
- context.getString(R.string.credential_before_click_advanced_unlock_button))
- }
- }
-
- private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
- cryptoObject: BiometricPrompt.CryptoObject?,
- promptInfo: BiometricPrompt.PromptInfo) {
- context.runOnUiThread {
- if (allowOpenBiometricPrompt) {
- if (biometricPrompt != null) {
- if (cryptoObject != null) {
- biometricPrompt.authenticate(promptInfo, cryptoObject)
- } else {
- setAdvancedUnlockedTitleView(R.string.crypto_object_not_initialized)
- }
- } else {
- setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
- }
- }
- }
- }
-
- private fun initEncryptData() {
- showFingerPrintViews(true)
- setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential)
- setAdvancedUnlockedMessageView("")
-
- biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo ->
- // Set listener to open the biometric dialog and save credential
- advancedUnlockInfoView?.setIconViewClickListener { _ ->
- openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
- }
- }
- }
-
- private fun initDecryptData() {
- showFingerPrintViews(true)
- setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database)
- setAdvancedUnlockedMessageView("")
-
- if (biometricUnlockDatabaseHelper != null) {
- cipherDatabaseAction.getCipherDatabase(databaseFileUri) { cipherDatabase ->
- cipherDatabase?.let {
- biometricUnlockDatabaseHelper?.initDecryptData(it.specParameters) { biometricPrompt, cryptoObject, promptInfo ->
-
- // Set listener to open the biometric dialog and check credential
- advancedUnlockInfoView?.setIconViewClickListener { _ ->
- openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
- }
-
- // Auto open the biometric prompt
- if (isBiometricPromptAutoOpenEnable) {
- isBiometricPromptAutoOpenEnable = false
- openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
- }
- }
- } ?: deleteEncryptedDatabaseKey()
- }
- }
- }
-
- @Synchronized
- fun initAdvancedUnlockMode() {
- mAllowAdvancedUnlockMenu = false
- when (biometricMode) {
- Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
- Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
- Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
- Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
- Mode.WAIT_CREDENTIAL -> initWaitData()
- Mode.STORE_CREDENTIAL -> initEncryptData()
- Mode.EXTRACT_CREDENTIAL -> initDecryptData()
- }
-
- invalidateBiometricMenu()
- }
-
- private fun invalidateBiometricMenu() {
- // Show fingerprint key deletion
- if (!mAddBiometricMenuInProgress) {
- mAddBiometricMenuInProgress = true
- cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
- mAllowAdvancedUnlockMenu = containsCipher
- && (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
- && biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
- mAddBiometricMenuInProgress = false
- context.invalidateOptionsMenu()
- }
- }
- }
-
- fun destroy() {
- // Close the biometric prompt
- allowOpenBiometricPrompt = false
- biometricUnlockDatabaseHelper?.closeBiometricPrompt()
- // Restore the checked listener
- checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
- cipherDatabaseAction.unregisterDatabaseListener(cipherDatabaseListener)
- }
-
- fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
- if (mAllowAdvancedUnlockMenu)
- menuInflater.inflate(R.menu.advanced_unlock, menu)
- }
-
- fun deleteEncryptedDatabaseKey() {
- allowOpenBiometricPrompt = false
- advancedUnlockInfoView?.setIconViewClickListener(false, null)
- biometricUnlockDatabaseHelper?.closeBiometricPrompt()
- cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri) {
- checkBiometricAvailability()
- }
- }
-
- override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
- loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec)
- }
-
- override fun handleDecryptedResult(decryptedValue: String) {
- // Load database directly with password retrieve
- loadDatabaseAfterRetrieveCredentials.invoke(decryptedValue)
- }
-
- override fun onInvalidKeyException(e: Exception) {
- setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
- }
-
- override fun onBiometricException(e: Exception) {
- val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
- setAdvancedUnlockedMessageView(errorMessage)
- }
-
- private fun showFingerPrintViews(show: Boolean) {
- context.runOnUiThread {
- advancedUnlockInfoView?.visibility = if (show) View.VISIBLE else View.GONE
- }
- }
-
- private fun setAdvancedUnlockedTitleView(textId: Int) {
- context.runOnUiThread {
- advancedUnlockInfoView?.setTitle(textId)
- }
- }
-
- private fun setAdvancedUnlockedMessageView(textId: Int) {
- context.runOnUiThread {
- advancedUnlockInfoView?.setMessage(textId)
- }
- }
-
- private fun setAdvancedUnlockedMessageView(text: CharSequence) {
- context.runOnUiThread {
- advancedUnlockInfoView?.message = text
- }
- }
-
- enum class Mode {
- BIOMETRIC_UNAVAILABLE,
- BIOMETRIC_SECURITY_UPDATE_REQUIRED,
- BIOMETRIC_NOT_CONFIGURED,
- KEY_MANAGER_UNAVAILABLE,
- WAIT_CREDENTIAL,
- STORE_CREDENTIAL,
- EXTRACT_CREDENTIAL
- }
-
- companion object {
-
- private val TAG = AdvancedUnlockedManager::class.java.name
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt
index 6c9a02beb..553641f20 100644
--- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt
@@ -244,7 +244,8 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
if (entryInfoKey != null) {
currentInputConnection.commitText(entryInfoKey!!.password, 1)
}
- actionGoAutomatically()
+ val otpFieldExists = entryInfoKey?.containsCustomField(OTP_TOKEN_FIELD) ?: false
+ actionGoAutomatically(!otpFieldExists)
}
KEY_OTP -> {
if (entryInfoKey != null) {
@@ -280,10 +281,11 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
currentInputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB))
}
- private fun actionGoAutomatically() {
+ private fun actionGoAutomatically(switchToPreviousKeyboardIfAllowed: Boolean = true) {
if (PreferencesUtil.isAutoGoActionEnable(this)) {
currentInputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
- if (PreferencesUtil.isKeyboardPreviousFillInEnable(this)) {
+ if (switchToPreviousKeyboardIfAllowed
+ && PreferencesUtil.isKeyboardPreviousFillInEnable(this)) {
switchToPreviousKeyboard()
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt
index 29695adab..67d8e410f 100644
--- a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt
@@ -91,6 +91,10 @@ class EntryInfo : Parcelable {
return customFields.any { !it.protectedValue.isProtected }
}
+ fun containsCustomField(label: String): Boolean {
+ return customFields.lastOrNull { it.name == label } != null
+ }
+
fun isAutoGeneratedField(field: Field): Boolean {
return field.name == OTP_TOKEN_FIELD
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/AdvancedUnlockNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/AdvancedUnlockNotificationService.kt
index 60e860c0d..7974e24bb 100644
--- a/app/src/main/java/com/kunzisoft/keepass/notifications/AdvancedUnlockNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/notifications/AdvancedUnlockNotificationService.kt
@@ -62,12 +62,12 @@ class AdvancedUnlockNotificationService : NotificationService() {
action = ACTION_REMOVE_KEYS
}
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
- val deviceCredential = PreferencesUtil.isDeviceCredentialUnlockEnable(this)
+ val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
val notificationBuilder = buildNewNotification().apply {
- setSmallIcon(if (deviceCredential) {
- R.drawable.notification_ic_device_unlock_24dp
- } else {
+ setSmallIcon(if (biometricUnlockEnabled) {
R.drawable.notification_ic_fingerprint_unlock_24dp
+ } else {
+ R.drawable.notification_ic_device_unlock_24dp
})
intent?.let {
setContentTitle(getString(R.string.advanced_unlock))
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt
index 5fe13714e..d3332a162 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt
@@ -41,7 +41,7 @@ import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
-import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
+import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.icons.IconPackChooser
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
@@ -218,7 +218,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
val tempAdvancedUnlockPreference: SwitchPreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- BiometricUnlockDatabaseHelper.biometricUnlockSupported(activity)
+ AdvancedUnlockManager.biometricUnlockSupported(activity)
} else false
biometricUnlockEnablePreference?.apply {
// False if under Marshmallow
@@ -258,15 +258,18 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
}
- val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- BiometricUnlockDatabaseHelper.deviceCredentialUnlockSupported(activity)
+ val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ AdvancedUnlockManager.deviceCredentialUnlockSupported(activity)
} else false
deviceCredentialUnlockEnablePreference?.apply {
+ // Biometric unlock already checked
+ if (biometricUnlockEnablePreference?.isChecked == true)
+ isChecked = false
if (!deviceCredentialUnlockSupported) {
isChecked = false
setOnPreferenceClickListener { preference ->
(preference as SwitchPreference).isChecked = false
- UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.R)
+ UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.M)
.show(parentFragmentManager, "unavailableFeatureDialog")
false
}
@@ -337,9 +340,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
validate?.invoke()
deleteKeysAlertDialog?.setOnDismissListener(null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric(
+ AdvancedUnlockManager.deleteEntryKeyInKeystoreForBiometric(
activity,
- object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback {
+ object : AdvancedUnlockManager.AdvancedUnlockErrorCallback {
fun showException(e: Exception) {
Toast.makeText(context,
getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
@@ -350,7 +353,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
showException(e)
}
- override fun onBiometricException(e: Exception) {
+ override fun onGenericException(e: Exception) {
showException(e)
}
})
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt
index d4baa0d14..f9c2f415d 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt
@@ -26,6 +26,7 @@ import android.net.Uri
import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.timeout.TimeoutHelper
import java.util.*
@@ -240,14 +241,23 @@ object PreferencesUtil {
fun isBiometricUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+ val biometricSupported = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+ AdvancedUnlockManager.biometricUnlockSupported(context)
+ } else {
+ false
+ }
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
+ && biometricSupported
}
fun isDeviceCredentialUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+ // Priority to biometric unlock
+ val biometricAlreadySupported = isBiometricUnlockEnable(context)
return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key),
context.resources.getBoolean(R.bool.device_credential_unlock_enable_default))
+ && !biometricAlreadySupported
}
fun isTempAdvancedUnlockEnable(context: Context): Boolean {
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt
index 2188498a2..4418e86f7 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt
@@ -35,6 +35,7 @@ import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
+import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionError
@@ -81,7 +82,7 @@ open class SettingsActivity
}
// Focus view to reinitialize timeout
- resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
+ coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt
index 63d5cb7dc..38a311fc2 100644
--- a/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt
@@ -53,23 +53,19 @@ object MenuUtil {
fun onDefaultMenuOptionsItemSelected(activity: Activity,
item: MenuItem,
readOnly: Boolean = READ_ONLY_DEFAULT,
- timeoutEnable: Boolean = false): Boolean {
+ timeoutEnable: Boolean = false) {
when (item.itemId) {
R.id.menu_contribute -> {
onContributionItemSelected(activity)
- return true
}
R.id.menu_app_settings -> {
// To avoid flickering when launch settings in a LockingActivity
SettingsActivity.launch(activity, readOnly, timeoutEnable)
- return true
}
R.id.menu_about -> {
val intent = Intent(activity, AboutActivity::class.java)
activity.startActivity(intent)
- return true
}
- else -> return true
}
}
}
diff --git a/app/src/main/res/layout/activity_password.xml b/app/src/main/res/layout/activity_password.xml
index 52e1f3ed2..1955fea2d 100644
--- a/app/src/main/res/layout/activity_password.xml
+++ b/app/src/main/res/layout/activity_password.xml
@@ -68,17 +68,10 @@
android:padding="0dp"
android:contentDescription="@string/about"
android:src="@drawable/ic_launcher_foreground"/>
-
-
-
+ android:layout_height="match_parent" />
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 7c4928090..20464124d 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -504,7 +504,6 @@
Pokuste se uložit sdílené info, když manuálné vybíráte položku
Uložit sdílené info
Oznámení
- Krypto objekt nelze načíst.
Vyžadována aktualizace biometrického zabezpečení.
Žádné přihlašovací ani biometrické údaje nejsou registrovány.
Trvale odstranit všechny položky z koše\?
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 750c897fb..1a1d6abf0 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -491,7 +491,6 @@
Fjerner vedhæftede filer indeholdt i databasen, men ikke knyttet til en post
Fjern ikke-sammenkædede data
Data
- Kryptoobjektet kunne ikke hentes.
Biometrisk sikkerhedsopdatering påkrævet.
Indholdet af nøglefilen bør aldrig ændres og bør i bedste fald indeholde tilfældigt genererede data.
Det anbefales ikke at tilføje en tom nøglefil.
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 67dd25425..b686744e9 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -508,7 +508,6 @@
Automatisches Zurückschalten zur vorherigen Tastatur nach dem Sperren der Datenbank
Datenbank sperren
Benachrichtigung
- Kryptoobjekt kann nicht abgerufen werden.
Biometrisches Sicherheitsupdate erforderlich.
Es sind keine biometrischen oder Geräteanmeldeinformationen registriert.
Registrierungsmodus
@@ -529,4 +528,29 @@
Öffnen des erweiterten Entsperrdialogs zum Entsperren der Datenbank
Löschen des Schlüssels zum erweiterten Entsperren
Fortschrittliche Entsperrerkennung
+ Ihr Passwort verbinden mit Ihrem gescannten biometrischen oder berechtigen Gerät um schnell Ihre Datenbank zu entsperren.
+ Erweiterte Entsperrung der Datenbank
+ Verfallzeit der erweiterten Entsperrung
+ Dauer der erweiterten Entsperrung bevor sein Inhalt gelöscht wird
+ Verfall der erweiterten Entsperrung
+ Keinen verschlüsselten Inhalt speichern, um erweiterte Entsperrung zu benutzen
+ Temporäre erweiterte Entsperrung
+ Erlaubt Ihnn die Geräteanmeldedaten zum Öffnen der Datenbank zu verwenden
+ Drücken um erweiterte Entsperrschlüssel zu löschen
+ Inhalt
+ Öffne Datenbank mit fortgeschriitener Entsperrungs-Erkennung
+ Eingabetaste
+ Rücktaste
+ Wähle Eintrag
+ Zurück zur vorherigen Tastatur
+ Benutzerdefinierte Felder
+ Löschen aller Schlüssel in Zusammenhang mit Erkennung des erweiterterten Entsperrens\?
+ Geräteanmeldedaten entsperren
+ Geräteanmeldedaten
+ Geben sie das Passwort ein, und dann klicken sie den \"Erweitertes Entsperren\" Knopf.
+ Initialisieren des erweitertes Entsperren Dialogs fehlgeschlagen.
+ Erweitertes Entsperren Fehler: %1$s
+ Konnte den Abdruck des erweiterten Entsperrens nicht erkennen
+ Kann den Schlüssel zum erweiterten Entsperren nicht lesen. Bitte löschen sie ihn und wiederholen sie Prozedur zum Erkennen des Entsperrens.
+ Extrahiere Datenbankanmeldedaten mit Daten aus erweitertem Entsperren
\ No newline at end of file
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 51b8005b1..2d7814339 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -503,7 +503,6 @@
Προσπαθήστε να αποθηκεύσετε κοινόχρηστες πληροφορίες όταν κάνετε μια χειροκίνητη επιλογή καταχώρησης
Αποθήκευση κοινόχρηστων πληροφοριών
Ειδοποίηση
- Δεν είναι δυνατή η ανάκτηση κρυπτογραφικού αντικειμένου.
Απαιτείται ενημέρωση βιομετρικής ασφάλειας.
Κανένα πιστοποιητικό βιομετρίας ή συσκευής δεν είναι εγγεγραμμένο.
Να διαγραφούν οριστικά όλοι οι κόμβοι από τον κάδο ανακύκλωσης;
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 9e9d38a21..230fb746d 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -513,7 +513,6 @@
Essayer d’enregistrer les informations partagées lors de la sélection manuelle d’une entrée
Enregistrer les infos partagées
Notification
- Impossible de récupérer l\'objet crypto.
Mise à jour de sécurité biométrique requise.
Supprimer définitivement tous les nœuds de la corbeille \?
Mode enregistrement
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 02470d650..e92b3bfeb 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -490,7 +490,6 @@
Pokušaj spremiti dijeljene informacije prilikom odabira ručnog unosa
Spremi dijeljene informacije
Trajno izbrisati sve čvorove iz smeća\?
- Nije moguće dohvatiti kripto objekt.
Potrebno je aktualizirati biometrijsku zaštitu.
Ne postoji biometrijski ključ niti podaci za prijavu uređaja.
Modus registracije
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index ac642ea27..ec497b56e 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -464,7 +464,6 @@
Eltávolítja azokat a mellékleteket, melyek az adatbázisban szerepelnek, de nem tartoznak bejegyzéshez
Nem összekapcsolt adatok eltávolítása
Adatok
- A titkosítási objektum nem kérhető le.
Biometrikus biztonsági frissítés szükséges.
Nincs biometrikus vagy eszközazonosító beállítva.
A kulcsfájl tartalmának sosem szabad megváltoznia, és a legjobb esetben véletlenszerűen előállított adatokat kellene tartalmaznia.
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 68bcfe3c1..2b6f112de 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -27,11 +27,11 @@
Aggiungi gruppo
Algoritmo di cifratura
Scadenza app
- Tempo di inattività prima del blocco della base di dati
+ Tempo di inattività prima del blocco del database
App
Impostazioni app
Parentesi
- Un file manager che accetta l\'azione Intent. ACTION_CREATE_DOCUMENT e ACTION_OPEN_DOCUMENT sono necessari per creare, aprire e salvare i file del database.
+ Un file manager che accetta intent action ACTION_CREATE_DOCUMENT e ACTION_OPEN_DOCUMENT è necessario creare, aprire e salvare i file del database.
Appunti eliminati
Errore negli appunti
Alcuni dispositivi non permettono alle app di usare gli appunti.
@@ -39,15 +39,15 @@
Scadenza appunti
Tempo prima di eliminare gli appunti (se supportato dal dispositivo)
Copia %1$s negli appunti
- Creazione file chiave base di dati…
- Banca dati
- Decodifica contenuto base di dati…
- Usa come base di dati predefinita
+ Recupero chiave database…
+ Database
+ Decodifica contenuto database…
+ Usa come database predefinito
Numeri
KeePassDX © %1$d Kunzisoft è un programma <strong>open-source</strong> e <strong>senza pubblicità</strong>.
\nViene distribuito sotto le condizioni della licenza <strong>GPL versione 3</strong> o successiva, senza alcuna garanzia.
Note
- Apri una base di dati esistente
+ Apri un database esistente
Ultimo accesso
Annulla
Conferma password
@@ -64,15 +64,15 @@
La codifica a flusso Arcfour non è supportata.
KeePassDX non può gestire questo URI.
Impossibile creare il file
- Impossibile leggere la base di dati.
+ Impossibile leggere il database.
Assicurati che il percorso sia corretto.
Inserisci un nome.
- Memoria insufficiente per caricare l\'intera base di dati.
+ Memoria insufficiente per caricare l\'intero database.
Deve essere selezionato almeno un tipo di generazione password.
Le password non corrispondono.
«Livello» troppo alto. Impostato a 2147483648.
Ogni stringa deve avere un nome.
- Inserisci un numero naturale positivo nel campo «lunghezza».
+ Inserisci un numero intero positivo nel campo \"Lunghezza\".
Nome campo
Valore campo
File non trovato. Prova a riaprirlo dal tuo gestore di file.
@@ -87,24 +87,24 @@
Password
Non è possibile leggere le credenziali.
Algoritmo errato.
- Formato della base di dati non riconosciuto.
+ Formato del database non riconosciuto.
Il file chiave è vuoto.
Lunghezza
Dimensione elenco elementi
Dimensione del testo nell\'elenco del gruppo
- Caricamento della base di dati…
+ Caricamento del database…
Minuscole
Nascondi le password
Maschera le password (***) in modo predefinito
Informazioni
Modifica chiave principale
Impostazioni
- Impostazioni base di dati
+ Impostazioni database
Elimina
Dona
Modifica
Nascondi password
- Blocca la base di dati
+ Blocca database
Apri
Cerca
Mostra password
@@ -115,16 +115,16 @@
Installa un browser web per aprire questo URL.
Non cercare nelle voci di backup
Ometti i gruppi «Backup» e «Cestino» dai risultati di ricerca
- Creazione di una nuova base di dati…
+ Creazione di un nuovo database…
In corso…
Protezione
Sola lettura
- KeePassDX richiede l\'autorizzazione di scrittura per poter modificare la tua base di dati.
+ A seconda del tuo file manager, KeepassDX potrebbe non riuscire a scrivere sulla memoria.
Elimina
Root
Livello cifratura
Livelli di cifratura aggiuntivi forniscono una maggiore protezione contro attacchi di tipo forza bruta, ma può rallentare il caricamento e il salvataggio.
- Salvataggio della base di dati…
+ Salvataggio del database…
Spazio
Cerca
Ordine naturale
@@ -132,16 +132,16 @@
Cerca
Risultati della ricerca
Trattino basso
- Versione della base di dati non supportata.
+ Versione del database non supportata.
Maiuscole
Attenzione
- Evita password con caratteri al di fuori del formato di codifica del testo nel file di base di dati (i caratteri non riconosciuti vengono convertiti nella stessa lettera).
+ Evita password con caratteri al di fuori del formato di codifica del testo nel file del database (i caratteri non riconosciuti vengono convertiti nella stessa lettera).
Versione %1$s
Password criptata salvata
- Questa base di dati non contiene alcuna credenziale.
+ Questo database non contiene alcuna credenziale.
Inserisci la password o il file chiave per sbloccare la base di dati.
\n
-\nEseguire il backup del file di base di dati in un luogo sicuro dopo ogni modifica.
+\nEseguire il backup del file del database in un luogo sicuro dopo ogni modifica.
- 5 secondi
- 10 secondi
@@ -173,7 +173,7 @@
Annulla
Sola lettura
Modificabile
- Algoritmo di cifratura della base di dati usato per tutti i dati.
+ Algoritmo di cifratura del database usato per tutti i dati.
Per generare la chiave per l\'algoritmo di cifratura, la chiave principale viene trasformata usando una funzione di derivazione della chiave (con un sale casuale).
Utilizzo di memoria
Quantità di memoria (in byte) utilizzabili dalla funzione di derivazione della chiave.
@@ -207,19 +207,19 @@
Se l\'eliminazione automatica degli appunti fallisce, cancellali manualmente.
Blocca
Blocco schermo
- Blocca la base di dati quando lo schermo è spento
+ Blocca il database quando lo schermo è spento
Impronta digitale
Scansione di impronte
Consente la scansione biometrica per aprire il database
Elimina chiavi di cifratura
- Elimina tutte le chiavi di cifratura relative al riconoscimento dell\'impronta
+ Elimina tutte le chiavi di cifratura relative allo sblocco avanzato
Impossibile avviare questa funzione.
Il dispositivo usa Android %1$s, ma richiede %2$s o versioni successive.
L\'hardware relativo non è stato trovato.
Nome file
Percorso
Assegna una chiave master
- Crea una nuova base di dati
+ Crea un nuovo database
Uso del Cestino
Sposta i gruppi e le voci nel gruppo «Cestino» prima di eliminarlo
Carattere campi
@@ -227,11 +227,11 @@
Fiducia appunti
Consenti la copia della password e dei campi protetti negli appunti
Attenzione: gli appunti sono condivisi da tutte le app. Se vengono copiati dati sensibili, altri software possono recuperarli.
- Nome della base di dati
- Descrizione della base di dati
- Versione della base di dati
+ Nome del database
+ Descrizione del database
+ Versione del database
Testo
- App
+ Interfaccia
Altro
Tastiera
Magitastiera
@@ -239,20 +239,20 @@
Non consentire nessuna chiave principale
Permetti di toccare il pulsante \"Apri\" se non sono selezionate credenziali
Protetto da scrittura
- Apri la base di dati in sola lettura in modo predefinito
+ Apri il database in sola lettura in modo predefinito
Suggerimenti educativi
Evidenzia gli elementi per imparare come funziona l\'app
Ripristina i suggerimenti educativi
Mostra di nuovo tutte le informazioni educative
Suggerimenti educativi ripristinati
- Crea il tuo file di base di dati
+ Crea il tuo file database
Crea il tuo primo file di gestione password.
- Apri una base di dati esistente
- Apri il tuo file di base di dati precedente dal tuo gestore di file per continuare ad usarlo.
- Aggiungi elementi alla tua base di dati
+ Apri un database esitente
+ Apri il tuo file database usato in precedenza con il file manager per continuare ad usarlo.
+ Aggiungi elementi al tuo database
Gli elementi aiutano a gestire le tue identità digitali.
\n
-\nI gruppi (come cartelle) organizzano gli elementi nella base di dati.
+\nI gruppi (come cartelle) organizzano gli elementi nel database.
Cerca tra gli elementi
Inserisci il titolo, il nome utente o il contenuto di altri campi per recuperare le tue password.
Modifica l\'elemento
@@ -261,18 +261,18 @@
Genera una password robusta da associare all\'elemento, definiscila a seconda dei criteri del modulo e non dimenticare di tenerla al sicuro.
Aggiungi campi personalizzati
Registra un campo aggiuntivo, aggiungi un valore e facoltativamente proteggilo.
- Sblocca la tua base di dati
- Proteggi da scrittura la tua base di dati
+ Sblocca il tuo database
+ Proteggi da scrittura il tuo database
Cambia la modalità di apertura per la sessione.
\n
-\n«Sola lettura» impedisce modifiche accidentali alla base di dati.
+\n«Sola lettura» impedisce modifiche accidentali al databae.
\n«Modificabile» permette di aggiungere, eliminare o modificare tutti gli elementi.
Copia un campo
I campi copiati possono essere incollati ovunque.
\n
\nUsa il metodo di inserimento che preferisci.
- Blocca la base di dati
- Blocca velocemente la base di dati, puoi impostare l\'applicazione per bloccarla dopo un certo periodo e quando lo schermo si spegne.
+ Blocca il database
+ Blocca velocemente il database, puoi impostare l\'applicazione per bloccarsi dopo un certo periodo e quando lo schermo si spegne.
Ordine elementi
Scegli l\'ordine di elementi e gruppi.
Partecipa
@@ -288,7 +288,7 @@
stai incoraggiando gli sviluppatori a creare <strong>nuove funzionalità</strong> e a <strong>correggere errori</strong> in base alle tue osservazioni.
Grazie mille per il tuo contributo.
Stiamo lavorando sodo per rilasciare questa funzione a breve.
- Non dimenticare di tenere aggiornata l\'app installando nuove versioni.
+ Ricorda di tenere aggiornata l\'app installando le nuove versioni.
Scarica
Contribuisci
Rijndael (AES)
@@ -300,7 +300,7 @@
Pacchetto icone
Pacchetto icone usato nell\'app
Modifica elemento
- Caricamento della base di dati fallito.
+ Caricamento del database fallito.
Caricamento della chiave fallito. Prova a diminuire l\'«Utilizzo memoria» del KDF.
Mostra nomi utente
Mostra i nomi utente negli elenchi
@@ -318,7 +318,7 @@
%1$s disponibile nella Magitastiera
%1$s
Pulisci alla chiusura
- Chiudere la base di dati alla chiusura della notifica
+ Chiudere il database alla chiusura della notifica
Aspetto
Tema tastiera
Tasti
@@ -327,14 +327,14 @@
Modalità selezione
Non terminare l\'app…
Premere \'\'Indietro\'\' per bloccare
- Blocca la base di dati quando l\'utente preme il pulsante Indietro nella schermata principale
+ Blocca il database quando l\'utente preme il pulsante Indietro nella schermata principale
Pulisci alla chiusura
- Blocca la base di dati quando scade la durata degli appunti o la notifica viene chiusa dopo che inizi ad usarlo
+ Blocca il database quando scade la durata degli appunti o la notifica viene chiusa dopo che inizi ad usarlo
Cestino
Selezione elemento
Mostra i campi di input nella Magitastiera durante la visualizzazione di un elemento
Elimina password
- Elimina la password immessa dopo un tentativo di connessione alla base di dati
+ Elimina la password immessa dopo un tentativo di connessione al database
Apri il file
Figli del nodo
Aggiungi un nodo
@@ -358,7 +358,7 @@
Sicurezza
Sfondo
Identificativo univoco universale
- Impossibile creare una base di dati con questa password e file chiave.
+ Impossibile creare un database con questa password e file chiave.
Sblocco avanzato
Cronologia
Imposta password usa e getta
@@ -377,16 +377,16 @@
Il periodo deve essere tra %1$d e %2$d secondi.
Il token deve contenere tra %1$d e %2$d cifre.
%1$s con le stesse credenziali univoche %2$s è già esistente.
- Sto creando la base di dati…
+ Sto creando il database…
Impostazioni di sicurezza
Il database contiene Identificativi Univoci Universali (UUID) duplicati.
- Non è possibile salvare la base di dati.
- Salva la base di dati
+ Non è possibile salvare il database.
+ Salva database
Svuota il cestino
Esecuzione del comando…
Vuoi eliminare definitivamente i nodi selezionati\?
Allegati
- Richiedi una ricerca quando una base di dati viene aperta
+ Richiedi una ricerca quando un database viene aperto
Ricerca rapida
Cancella cronologia
Ripristina cronologia
@@ -396,32 +396,32 @@
Impostazioni della chiave principale
Chiave principale
Contributi
- Garantisci il permesso di scrittura per salvare i cambiamenti della base di dati
- Nascondi collegamenti corrotti nella lista delle basi di dati recenti
- Nascondi i collegamenti di basi di dati corrotti
- Mostra le posizioni delle basi di dati recenti
+ Concedi il permesso di scrittura per salvare i cambiamenti del database
+ Nascondi collegamenti corrotti nella lista dei database recenti
+ Nascondi i collegamenti dei database corrotti
+ Mostra le posizioni dei database recenti
Mostra file recenti
Ricorda la posizione dei file chiave
- Ricorda la posizione delle basi di dati
- Ricorda la posizione delle basi di dati
+ Ricorda la posizione dei database
+ Ricorda la posizione dei database
Per continuare, risolvi il problema generando nuovi UUID per i duplicati\?
- Impossibile creare il file della base di dati.
+ Impossibile creare il file del database.
Aggiungi allegato
Scarta
Scartare i cambiamenti\?
Convalida
Dimensione massima
Numero massimo
- Apri automaticamente prompt biometrico
+ Apri automaticamente la richiesta
Limita la dimensione (in byte) della cronologia per voce
Limita il numero di elementi della cronologia per voce
Gruppo cestino
- La compressione dei dati riduce le dimensioni della base di dati
+ La compressione dei dati riduce le dimensioni del database
Compressione dati
- Proponi l\'autenticazione biometrica quando la base di dati è configurata per usarla
- Utilizza lo sblocco avanzato per aprire la base di dati più facilmente
+ Richiedi automaticamente lo sblocco avanzato se il database è impostato per usarlo
+ Utilizza lo sblocco avanzato per aprire il database più facilmente
Copia i campi di immissione utilizzando gli appunti del tuo dispositivo
- Base di dati aperta
+ Database aperto
Biometrico
Forza rinnovo
È consigliato di cambiare la chiave principale (giorni)
@@ -435,7 +435,7 @@
Inizializzazione…
Scaricamento %1$s
Imposta One-Time Password (OTP)
- Salva la base di dati dopo ogni azione importante (in modalità «Modificabile»)
+ Salva il database dopo ogni azione importante (in modalità «Modificabile»)
Salvataggio automatico del database
Suggerisci automaticamente risultati dal dominio web o ID dell\'applicazione
Ricerca automatica
@@ -445,7 +445,7 @@
Gzip
Nessuna
Compressione
- Colore personalizzato della base di dati
+ Colore personalizzato del database
Nome utente predefinito
Disabilita
Abilita
@@ -456,7 +456,7 @@
Mostra il bottone di blocco
Impostazioni dell\'autocompletamento
Accesso al file revocato dal file manager
- Ricorda la posizione dei file chiave dei basi di dati
+ Ricorda la posizione dei file chiave
Questa etichetta esiste già.
Riavvia l\'app contenente il campo per attivare il blocco.
Blocca riempimento automatico
@@ -473,24 +473,24 @@
Aggiungi elemento
Torna automaticamente alla tastiera precedente quando si esegue l\'azione del tasto automatico
Azione tasto automatico
- Torna automaticamente alla tastiera precedente nella schermata delle credenziali della base di dati
- Schermata credenziali della base di dati
+ Torna automaticamente alla tastiera precedente nella schermata credenziali del database
+ Schermata credenziali del database
Cambia tastiera
Carica %1$s
Carica un allegato alla voce per salvare dati esterni importanti.
Aggiungi allegato
- Rimuovi gli allegati contenuti nella base di dati ma non collegati ad una voce
+ Rimuovi gli allegati contenuti nel database ma non collegati ad una voce
Rimuovi i dati scollegati
Dati
Il contenuto del file chiave non deve mai essere modificato e, nel migliore dei casi, dovrebbe contenere dati generati casualmente.
Non è consigliabile aggiungere un file chiave vuoto.
Rimuovere questi dati comunque\?
- La rimozione di dati scollegati può ridurre le dimensioni della base di dati, ma può anche eliminare i dati utilizzati per i plugin KeePass.
+ La rimozione di dati scollegati può ridurre le dimensioni del database, ma può anche eliminare i dati utilizzati per i plugin KeePass.
Aggiungi comunque il file\?
Caricare questo file sostituirà quello esistente.
- Una base di dati KeePass dovrebbe contenere solo piccoli file di utilità (come i file di chiavi PGP).
+ Un database KeePass dovrebbe contenere solo piccoli file di utilità (come i file di chiavi PGP).
\n
-\nLa base di dati può diventare molto grande e ridurre le prestazioni con questo caricamento.
+\nIl tuo database può diventare molto grande e ridurre le prestazioni con questo caricamento.
Info credenziali
Visualizza l\'UUID collegato a una voce
Mostra UUID
@@ -499,20 +499,49 @@
Chiedi di salvare i dati
Prova a salvare le informazioni di ricerca quando effettui una selezione di immissione manuale
Salva le informazioni di ricerca
- Chiudi la base di dati dopo una selezione di compilazione automatica
- Chiudi la base di dati
- Torna automaticamente alla tastiera precedente dopo aver bloccato la base di dati
- Blocca la base di dati
+ Chiudi il database dopo aver usato l\'autocompletamento
+ Chiudi database
+ Torna automaticamente alla tastiera precedente dopo aver bloccato il database
+ Blocca il database
Prova a salvare le informazioni condivise quando effettui una selezione di immissione manuale
Salva le informazioni condivise
Notifica
- Impossibile recuperare l\'oggetto crittografico.
È necessario un aggiornamento della sicurezza biometrica.
Eliminare definitivamente tutti i nodi dal cestino\?
Modalità registrazione
Modalità salvataggio
Modalità ricerca
Il nome del campo esiste già.
- Il salvataggio di un nuovo elemento non è consentito in una base di dati di sola lettura
+ Il salvataggio di un nuovo elemento non è consentito in un database di sola lettura
Nessuna credenziale biometrica o del dispositivo è registrata.
+ Collega la password alla tua autenticazione biometrica (o del dispositivo) per sbloccare velocemente il database.
+ Sblocco avanzato del database
+ Invio
+ Backspace
+ Seleziona voce
+ Torna alla tasitera precedente
+ Campi personalizzati
+ Vuoi eliminare le chiavi di cifratura relative allo sblocco avanzato\?
+ Scadenza sblocco avanzato
+ Non salvare alcun contenuto criptato per usare lo sblocco avanzato
+ Validità dello sblocco avanzato prima di eliminarne il contenuto
+ Scadenza sblocco avanzato
+ Sblocco avanzato temporaneo
+ Utilizza le credenziali del dispositivo per sbloccare il database
+ Sblocco con credenziali dispositivo
+ Tocca per eliminare le chiavi di sblocco avanzato
+ Contenuto
+ Non è possibile inizializzare lo sblocco avanzato.
+ Non è possibile riconoscere lo sblocco avanzato
+ Non è possibile leggere la chiave di sblocco avanzato. Eliminala e ripeti la prodecura dello sblocco.
+ Estrai le credenziali del database con i dati dallo sblocco avanzato
+ Attenzione: dovrai sempre ricordare la password principale anche se usi lo sblocco avanzato.
+ Riconoscimento con sblocco avanzato
+ Credenziali del dispositivo
+ Inserisci la password, quindi clicca sull\'icona \"Sblocco avanzato\".
+ Errore sblocco avanzato: %1$s
+ Apri il database autenticando con lo sblocco avanzato
+ Autentica con lo sblocco avanzato per sbloccare il database
+ Autentica con lo sblocco avanzato per salvare le credenziali
+ Elimina chiave di sblocco avanzato
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index e8e2887f9..e18b94511 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -273,7 +273,6 @@
キーストアが正しく初期化されていません。
保存された暗号化済みパスワード
データベースの保存済み認証情報はありません。
- crypto オブジェクトを取得できません。
履歴
デザイン
生体認証
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml
index 62e60c599..f3ff7a099 100644
--- a/app/src/main/res/values-nb/strings.xml
+++ b/app/src/main/res/values-nb/strings.xml
@@ -417,7 +417,7 @@
Skjul ødelagte lenker i listen over nylige databaser
Skjul ødelagte databaselenker
Spør om lagring av data
- Avansert opplåsningsfeil:
+ Avansert opplåsningsfeil: %1$s
Det anbefales ikke å legge til en tom nøkkelfil.
Legg til filen uansett\?
Registreringsmodus
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 30abad69c..635a55661 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -34,7 +34,7 @@
Bestandsbeheer dat de Intent-actie ACTION_CREATE_DOCUMENT en ACTION_OPEN_DOCUMENT accepteert, is nodig om databasebestanden aan te maken, te openen en op te slaan.
Klembord gewist
Klembordtime-out
- Tijd van opslag op het klembord (indien ondersteund op jouw apparaat)
+ Tijd van opslag op het klembord (voor zover ondersteund op dit apparaat)
Selecteer om %1$s naar klembord te kopiëren
Databasesleutel ophalen…
Database
@@ -67,7 +67,7 @@
Onvoldoende vrij geheugen om de gehele database te laden.
Je moet minimaal één soort wachtwoordgenerering kiezen.
De wachtwoorden komen niet overeen.
- \"Cycli-waarde\" te groot. Wordt ingesteld op 2147483648.
+ \"Cycli-waarde\" te groot. Deze wordt ingesteld op 2147483648.
Voer een positief geheel getal in in het veld \"Lengte\".
Bestandsbeheer
Wachtwoord genereren
@@ -81,16 +81,16 @@
Kan referenties niet lezen.
Databaseformaat kan niet worden herkend.
Lengte
- Lengte van lijst met items
- Tekstgrootte in de lijst
+ Lijstgrootte
+ Tekstgrootte in de itemslijst
Database laden…
Kleine letters
Wachtwoorden verbergen
- Wachtwoorden standaard verbergen (***)
+ Wachtwoorden standaard maskeren (***)
Over
Hoofdsleutel wijzigen
Instellingen
- Instellingen database
+ Database-instellingen
Verwijderen
Doneren
Bewerken
@@ -205,15 +205,15 @@
Auto-aanvullen
KeePassDX auto-aanvullendienst
Inloggen met KeePassDX
- Dienst voor automatisch aanvullen
+ Dienst automatisch aanvullen
Schakel de dienst in om formulieren in andere apps in te vullen
Gegenereerde wachtwoordlengte
- Standaardlengte van gegenereerd wachtwoord instellen
+ Stel de standaardlengte van gegenereerd wachtwoord in
Wachtwoordtekens
Toegestane wachtwoordtekens instellen
Klembord
Klembordmeldingen
- Toon klembordmeldingen om velden te kopiëren bij het bekijken van een item
+ Schakel klembordmeldingen in om velden te kopiëren bij het bekijken van een item
Als automatisch wissen van het klembord mislukt, doe dit dan handmatig.
Vergrendelen
Schermvergrendeling
@@ -222,9 +222,9 @@
Ontgrendelen met biometrie
Gebruik biometrische herkenning om de database te openen
Coderingssleutels verwijderen
- Alle sleutels voor biometrische herkenning verwijderen
+ Alle coderingssleutels met betrekking tot geavanceerde ontgrendelingsherkenning verwijderen
Kan deze functie niet starten.
- Het apparaat draait op Android %1$s, maar %2$s of hoger is vereist.
+ Dit apparaat draait op Android %1$s, maar %2$s of hoger is vereist.
De bijbehorende hardware werd niet gevonden.
Bestandsnaam
Pad
@@ -241,19 +241,19 @@
Databaseomschrijving
Databaseversie
Tekst
- App
+ Gebruikersomgeving
Overig
Toetsenbord
Magikeyboard
- Aangepast toetsenbord met je wachtwoorden en alle identiteitsvelden activeren
+ Activeer een aangepast toetsenbord dat je wachtwoorden en identiteitsvelden vult
Geen hoofdwachtwoord toestaan
- Maakt het mogelijk op de knop \"Openen\" te tikken als er geen inloggegevens zijn geselecteerd
+ Schakel de knop \"Openen\" in als er geen referenties zijn geselecteerd
Alleen-lezen
Open de database standaard alleen-lezen
Informatieve tips
Markeer elementen om te leren hoe de app werkt
Informatieve tips opnieuw instellen
- Informatieve tips opnieuw weergeven
+ Informatieve tips opnieuw tonen
Informatieve tips opnieuw ingesteld
Maak je databasebestand aan
Maak je eerste wachtwoordbeheerbestand aan.
@@ -273,10 +273,10 @@
Registreer een extra veld, voeg een waarde toe en bescherm dit desgewenst.
Ontgrendel je database
Database alleen-lezen
- Wijzig de opstartmodus van de sessie.
-\n
-\nIn alleen-lezenmodus kunnen geen onbedoelde wijzigingen worden gemaakt.
-\nIn schrijfmodus kun je elementen toevoegen, verwijderen of aanpassen.
+ Wijzig de opstartmodus van de sessie.
+\n
+\nIn alleen-lezenmodus kunnen geen onbedoelde wijzigingen worden gemaakt.
+\nIn schrijfmodus kun je naar wens elementen toevoegen, verwijderen of aanpassen.
Veld kopiëren toestaan
Kopieer een veld en plak de inhoud waar je maar wilt.
\n
@@ -297,15 +297,15 @@
motiveer je ontwikkelaars om <strong>nieuwe functies</strong> te creëren en <strong>problemen op te lossen</strong>.
Hartelijk bedankt voor je bijdrage.
We zijn druk bezig om deze functie snel beschikbaar te stellen.
- Vergeet niet om je app up-to-date te houden door nieuwe versies te installeren.
+ Vergeet niet je app up-to-date te houden door nieuwe versies te installeren.
Downloaden
Bijdragen
ChaCha20
AES
App-thema
Thema gebruikt in de app
- Pictogrammenverzameling
- Pictogrammenverzameling in gebruik
+ Icon pack
+ Gebruikt Icon Pack
Build %1$s
Magikeyboard
Magikeyboard (KeePassDX)
@@ -328,14 +328,14 @@
Selectiemodus
App niet afsluiten…
Druk \'Terug\' om te vergrendelen
- Vergrendel de database wanneer de gebruiker op de knop Terug in het hoofdscherm klikt
+ Vergrendel de database wanneer de gebruiker in het hoofdscherm op de knop Terug klikt
Wissen bij afsluiten
Vergrendel de database wanneer de duur van het klembord verloopt of de melding wordt gesloten nadat u deze bent gaan gebruiken
Prullenmand
Itemselectie
Invoervelden in Magikeyboard tonen bij het bekijken van een item
Wachtwoord wissen
- Wis het ingevoerde wachtwoord na een poging met een database te verbinden
+ Wis het ingevoerde wachtwoord na een verbindingspoging met een database
Bestand openen
Onderliggende items
Knooppunt toevoegen
@@ -353,7 +353,7 @@
UUID
Je kan hier geen item plaatsen.
Je kan hier geen item kopiëren.
- Toon het aantal items
+ Aantal items tonen
Toon het aantal items in een groep
Achtergrond
Update
@@ -361,8 +361,8 @@
Kan geen database aanmaken met dit wachtwoord en sleutelbestand.
Geavanceerd ontgrendelen
Biometrie
- Automatisch om biometrie vragen
- Automatisch om biometrie vragen als een database hiervoor is ingesteld
+ Auto-open suggestie
+ Automatisch om geavanceerde ontgrendeling vragen als een database hiervoor is ingesteld
Inschakelen
Uitschakelen
Hoofdsleutel
@@ -390,7 +390,7 @@
De database bevat dubbele UUID\'s.
Probleem oplossen door nieuwe UUID\'s te genereren voor de duplicaten\?
Database geopend
- Kopieer invoervelden met behulp van het klembord van uw apparaat
+ Kopieer invoervelden met behulp van het klembord van dit apparaat
Geavanceerde ontgrendeling gebruiken om een database gemakkelijker te openen
Gegevenscompressie
Gegevenscompressie verkleint de omvang van de database
@@ -413,20 +413,20 @@
Sla de database op na elke belangrijke actie (in \"Schrijf\" modus)
OTP instellen
Locatie van sleutelbestanden opslaan
- Databaselocaties onthouden
+ Databaselocaties opslaan
Verlopen items worden niet getoond
- Verberg verlopen items
- Klaar!
+ Verlopen items verbergen
+ Voltooid!
Voltooien…
Voortgang: %1$d%%
Initialiseren…
Download %1$s
- Stel eenmalig wachtwoordbeheer (HOTP / TOTP) in om een token te genereren dat wordt gevraagd voor tweefactorauthenticatie (2FA).
+ Stel eenmalig wachtwoordbeheer (HOTP / TOTP) in om een token te genereren voor tweefactorauthenticatie (2FA).
Automatisch opslaan
Automatisch zoekresultaten voorstellen vanuit het webdomein of de toepassings-ID
Automatisch zoeken
Prullenbak
- Geeft de vergrendelknop weer in de gebruikersinterface
+ Geef de vergrendelknop weer in de gebruikersinterface
Vergrendelknop weergeven
Instellingen voor automatisch aanvullen
De sleutelopslag is niet correct geïnitialiseerd.
@@ -434,10 +434,10 @@
Toegang tot het bestand ingetrokken door bestandsbeheer
Bestandstoegang verlenen om databasewijzigingen op te slaan
Opdracht uitvoeren…
- Verberg gebroken links in de lijst met recente databases
- Verberg corrupte databasekoppelingen
- Onthoudt waar de databasesleutelbestanden zijn opgeslagen
- Onthoudt waar de databases zijn opgeslagen
+ Gebroken links in de lijst met recente databases verbergen
+ Corrupte databasekoppelingen verbergen
+ Onthoud de locatie van databasesleutelbestanden
+ Onthoud de locatie van databases
Zoekopdracht aanmaken bij het openen van een database
Snel zoeken
Geschiedenis wissen
@@ -470,19 +470,19 @@
Item toevoegen
\"Gaan\"-toetsactie na het indrukken van een \"Veld\"-toets
Automatische toetsactie
- Schakel automatisch terug naar het vorige toetsenbord na het uitvoeren van de Automatische toetsactie
+ Schakel automatisch terug naar het vorige toetsenbord na het uitvoeren van de \"Automatische toetsactie\"
Automatische toetsactie
Schakel automatisch terug naar het vorige toetsenbord op het databasereferentiescherm
Scherm Databasereferenties
Van toetsenbord wisselen
- %1$s uploaden
- Upload een bijlage bij dit item om belangrijke externe gegevens op te slaan.
+ Upload %1$s
+ Voeg een bijlage toe aan dit item om belangrijke externe gegevens op te slaan.
Bijlage toevoegen
- Toch het bestand toevoegen\?
- Een KeePass-database mag alleen kleine hulpprogramma-bestanden bevatten (zoals PGP-sleutelbestanden).
+ Het bestand toch toevoegen\?
+ Een KeePass database is bedoeld om alleen kleine gebruiksbestanden te bevatten (zoals PGP sleutelbestanden).
\n
-\nDe database kan erg groot worden en de prestaties kunnen verminderen bij deze upload.
- Als je dit bestand uploadt, wordt het bestaande vervangen.
+\nMet deze upload kan de database erg groot worden en kunnen de prestaties verminderen.
+ Uploaden van dit bestand zal het bestaande bestand vervangen.
Inloggegevens
Het verwijderen van niet-gekoppelde gegevens kan de omvang van uw database verkleinen, maar kan ook gegevens verwijderen die voor KeePass-plug-ins worden gebruikt.
Deze gegevens toch verwijderen\?
@@ -505,7 +505,6 @@
Probeer gedeelde informatie op te slaan bij een handmatige invoerselectie
Gedeelde info opslaan
Melding
- Kan crypto-object niet ophalen.
Biometrische beveiligingsupdate vereist.
Geen biometrische gegevens of apparaatgegevens geregistreerd.
Alles definitief uit de prullenbak verwijderen\?
@@ -513,4 +512,35 @@
Veilige modus
Zoekmodus
Het opslaan van een nieuw item is niet toegestaan in een alleen-lezen database
+ Koppel je wachtwoord aan je gescande biometrische gegevens of apparaatreferentie om je database snel te ontgrendelen.
+ Geavanceerde database-ontgrendeling
+ Enter
+ Backspace
+ Item selecteren
+ Terug naar vorig toetsenbord
+ Aangepaste velden
+ Alle coderingssleutels met betrekking tot geavanceerde ontgrendelingsherkenning verwijderen\?
+ Time-out voor geavanceerd ontgrendelen
+ Duur van geavanceerd ontgrendelingsgebruik voordat de inhoud wordt verwijderd
+ Vervaltijd voor geavanceerde ontgrendeling
+ Sla geen versleutelde inhoud op om geavanceerde ontgrendeling te gebruiken
+ Tijdelijke geavanceerde ontgrendeling
+ Hiermee kan je de referentie van je apparaat gebruiken om de database te openen
+ Ontgrendeling met apparaatreferenties
+ Tik om geavanceerde ontgrendelingstoetsen te verwijderen
+ Inhoud
+ Apparaatreferentie
+ Typ het wachtwoord en klik vervolgens op de knop \"Geavanceerd ontgrendelen\".
+ Kan geavanceerde ontgrendelingsprompt niet initialiseren.
+ Geavanceerde ontgrendelingsfout: %1$s
+ Kan geavanceerde ontgrendelingsafdruk niet herkennen
+ Kan de geavanceerde ontgrendelingssleutel niet lezen. Verwijder deze en herhaal de herkenningsprocedure voor het ontgrendelen.
+ Databasegegevens uitpakken met geavanceerde ontgrendelingsgegevens
+ Open database met geavanceerde ontgrendelingsherkenning
+ Waarschuwing: je moet nog steeds je hoofdwachtwoord onthouden als je geavanceerde ontgrendelingsherkenning gebruikt.
+ Geavanceerde ontgrendelingsherkenning
+ Open de geavanceerde ontgrendelingsprompt om inloggegevens op te slaan
+ Open de geavanceerde ontgrendelingsprompt om de database te ontgrendelen
+ Geavanceerde ontgrendelingssleutel verwijderen
+ De veldnaam bestaat al.
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 7b6969195..c9b233a3b 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -228,7 +228,7 @@
Utwórz nową bazę danych
Wykorzystaj kosz
Przenosi grupy i wpisy do grupy \"Kosz\" przed usunięciem
- Krój pisma pola
+ Czcionka pola
Zmień czcionkę użytą w polach, aby poprawić widoczność postaci
Zaufanie do schowka
Zezwalanie na kopiowanie hasła wejściowego i chronionych pól do schowka
@@ -503,7 +503,6 @@
Spróbuj zapisać udostępnione informacje podczas ręcznego wybierania pozycji
Zapisz udostępnione informacje
Powiadomienia
- Nie można pobrać obiektu kryptograficznego.
Wymagana aktualizacja zabezpieczeń biometrycznych.
Nie zarejestrowano żadnych danych biometrycznych ani danych urządzenia.
Trwale usunąć wszystkie węzły z kosza\?
@@ -528,4 +527,12 @@
Ostrzeżenie: Jeśli używasz zaawansowanego rozpoznawania odblokowania, nadal musisz zapamiętać hasło główne.
Zaawansowane rozpoznawanie odblokowania
Usuń zaawansowany klucz odblokowujący
+ Połącz swoje hasło ze zeskanowanymi danymi biometrycznymi lub danymi logowania urządzenia, aby szybko odblokować bazę danych.
+ Zaawansowane odblokowywanie bazy danych
+ Zaawansowany limit czasu odblokowania
+ Czas trwania zaawansowanego odblokowywania przed usunięciem jego zawartości
+ Wygaśnięcie zaawansowanego odblokowania
+ Nie przechowuj żadnych zaszyfrowanych treści, aby korzystać z zaawansowanego odblokowywania
+ Naciśnij, aby usunąć zaawansowane klucze odblokowujące
+ Zawartość
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 762eeb9de..46079eb05 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -215,12 +215,12 @@
Блокировка экрана
Блокировать базу при отключении экрана
Расширенная разблокировка
- Сканирование биометрического ключа
+ Биометрическая разблокировка
Включить разблокировку базы при помощи биометрического ключа
Удалить ключи шифрования
Удалить все ключи шифрования, связанные с распознаванием расширенной разблокировки
- Невозможно запустить эту функцию.
- Ваша версия Android %1$s, но требуется %2$s.
+ Невозможно использовать эту функцию.
+ Ваша версия Android %1$s, требуется %2$s.
Соответствующее оборудование не найдено.
Имя файла
Путь
@@ -228,8 +228,8 @@
Создать новую базу
Использовать \"корзину\"
Перемещать группу или запись в \"корзину\" вместо удаления
- Шрифт полей
- Использовать в полях особый шрифт для лучшей читаемости
+ Особый шрифт полей
+ Использовать в полях специальный шрифт для лучшей читаемости
Доверять буферу обмена
Разрешить копирование пароля и защищённых полей в буфер обмена
Внимание: буфер обмена доступен всем приложениям. Если копируются чувствительные данные, другие программы могут их перехватить.
@@ -494,7 +494,6 @@
Сохранять данные поиска
Сохранять общую информацию при ручном выборе записи
Сохранять общие данные
- Невозможно получить доступ к зашифрованному объекту.
Блокировка базы
Автоматически переключаться на предыдущую клавиатуру после блокировки базы
Показывать UUID, связанный с записью
@@ -513,7 +512,7 @@
Сохранение новых записей невозможно, т.к. база открыта только для чтения
Поле с таким именем уже существует.
Позволяет использовать учётные данные вашего устройства для открытия базы
- Разблокировка учётных данных устройства
+ Разблокировка учётными данными устройства
Учётные данные устройства
Введите пароль и нажмите кнопку \"Расширенная разблокировка\".
Невозможно инициализировать запрос расширенной разблокировки.
@@ -534,10 +533,10 @@
Backspace
Выберите запись
Расширенная разблокировка базы
- Ожидание расширенной разблокировки
+ Срок действия расширенной разблокировки
Свяжите пароль с отсканированными биометрическими данными или учётными данными устройства, чтобы быстро разблокировать базу.
Продолжительность использования содержимого расширенной разблокировки до его удаления
- Срок действия расширенной разблокировки
+ Время действия
Временная расширенная разблокировка
Не сохранять зашифрованное содержимое для использования расширенной разблокировки
Нажмите, чтобы удалить ключи расширенной разблокировки
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 1632f88bf..aae3a6f80 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -486,7 +486,6 @@
Elle girdi seçimi yaparken paylaşılan bilgileri kaydetmeyi dene
Paylaşılan bilgileri kaydet
Bildirim
- Şifreleme nesnesi alınamıyor.
Biyometrik güvenlik güncellemesi gerekli.
Biyometrik bilgiler veya aygıt kimlik doğrulama bilgileri kaydedilmedi.
Geri dönüşüm kutusundaki tüm düğümler kalıcı olarak silinsin mi\?
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index b6adc824c..51ba1f330 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -503,7 +503,6 @@
Намагатися зберегти спільні відомості під час вибору запису власноруч
Збереження спільних відомостей
Сповіщення
- Не вдалося отримати крипто-об\'єкт.
Необхідно оновити біометричний захист.
Біометричних чи облікових даних пристрою не зареєстровано.
Видалити всі вузли з кошика остаточно\?
diff --git a/app/src/main/res/values-v23/donottranslate.xml b/app/src/main/res/values-v23/donottranslate.xml
index e761117a7..e1e50e325 100644
--- a/app/src/main/res/values-v23/donottranslate.xml
+++ b/app/src/main/res/values-v23/donottranslate.xml
@@ -19,4 +19,5 @@
-->
true
+ true
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 54567e6d2..7f0f0221f 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -503,7 +503,6 @@
手动选择条目时,尝试保存共享信息
保存分享的信息
通知
- 无法检索加密对象。
需要生物识别安全更新。
未登记生物识别或设备凭证。
从回收站永久删除所有节点?
@@ -533,4 +532,13 @@
选择条目
回到先前的键盘
自定义字段
+ 将您的密码连接到您扫描的生物特征或设备凭据,以快速解锁您的数据库。
+ 高级数据库解锁
+ 高级解锁超时
+ 删除内容之前高级解锁使用的持续时间
+ 高级解锁过期
+ 不要存储任何加密内容来使用高级解锁
+ 临时性高级解锁
+ 点击删除高级解锁密钥
+ 内容
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ff0e89dee..80f574349 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -287,7 +287,6 @@
Advanced unlock error: %1$s
This database does not have stored credential yet.
Unable to initialize advanced unlock prompt.
- Unable to retrieve crypto object.
Type in the password, and then click the \"Advanced unlock\" button.
History
Appearance
diff --git a/fastlane/metadata/android/en-US/changelogs/49.txt b/fastlane/metadata/android/en-US/changelogs/49.txt
new file mode 100644
index 000000000..af4f00858
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/49.txt
@@ -0,0 +1,3 @@
+ * Unlock database by device credentials (PIN/Password/Pattern) with Android M+ #102 #152 #811
+ * Prevent auto switch back to previous keyboard if otp field exists #814
+ * Fix timeout reset #817
\ No newline at end of file
diff --git a/fastlane/metadata/android/fr-FR/changelogs/49.txt b/fastlane/metadata/android/fr-FR/changelogs/49.txt
new file mode 100644
index 000000000..da28437c5
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/49.txt
@@ -0,0 +1,3 @@
+ * Déverouillage de base de données avec identifiants de l'appareil (PIN/Password/Pattern) pour Android M+ #102 #152 #811
+ * Empêcher le retour automatique au clavier précédent si le champ otp existe #814
+ * Correction de la réinitialisation du timer #817
\ No newline at end of file