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