Added a dialog to verify the password #2283

This commit is contained in:
J-Jamet
2025-12-01 18:39:40 +01:00
parent 9c6241afc9
commit c754b6a049
21 changed files with 292 additions and 263 deletions

View File

@@ -84,6 +84,7 @@ import com.kunzisoft.keepass.view.changeTitleColor
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.showError
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
import kotlinx.coroutines.launch
@@ -214,7 +215,7 @@ class EntryActivity : DatabaseLockActivity() {
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
}
} catch (e: ClassCastException) {
} catch (_: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
@@ -328,6 +329,7 @@ class EntryActivity : DatabaseLockActivity() {
when (uIState) {
is UserVerificationViewModel.UIState.Loading -> {}
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
coordinatorLayout?.showError(uIState.error)
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {

View File

@@ -122,6 +122,7 @@ import com.kunzisoft.keepass.view.applyWindowInsets
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.showError
import com.kunzisoft.keepass.view.toastError
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
@@ -581,6 +582,7 @@ class GroupActivity : DatabaseLockActivity(),
when (uIState) {
is UserVerificationViewModel.UIState.Loading -> {}
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
coordinatorLayout?.showError(uIState.error)
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
@@ -711,14 +713,6 @@ class GroupActivity : DatabaseLockActivity(),
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
var entry: Entry? = null
try {
entry = result.data?.getNewEntry(database)
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve entry action for selection", e)
}
when (actionTask) {
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
if (result.isSuccess) {
@@ -731,6 +725,12 @@ class GroupActivity : DatabaseLockActivity(),
// Search not used
},
selectionAction = { intentSenderMode, typeMode, searchInfo ->
var entry: Entry? = null
try {
entry = result.data?.getNewEntry(database)
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve entry action for selection", e)
}
when (typeMode) {
TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD -> entry?.let {
@@ -749,15 +749,13 @@ class GroupActivity : DatabaseLockActivity(),
}
)
}
}
}
coordinatorError?.showActionErrorIfNeeded(result)
// Reload the group
loadGroup()
finishNodeAction()
}
}
}
private fun manageIntent(intent: Intent?) {
intent?.let {

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
class CheckDatabaseCredentialDialogFragment : DatabaseDialogFragment() {
private val userVerificationViewModel: UserVerificationViewModel by activityViewModels()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
val rootView = inflater.inflate(R.layout.fragment_check_database_credential, null)
builder.setView(rootView)
.setPositiveButton(R.string.check) { _, _ ->
userVerificationViewModel.checkMainCredential(
rootView
.findViewById<TextView>(R.id.setup_check_password_edit_text)
.text.toString(),
)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
userVerificationViewModel.onUserVerificationFailed()
dismiss()
}
rootView.findViewById<View>(R.id.user_verification_information)?.setOnClickListener {
activity.openUrl(R.string.user_verification_explanation_url)
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
companion object {
fun getInstance(): CheckDatabaseCredentialDialogFragment {
val fragment = CheckDatabaseCredentialDialogFragment()
val args = Bundle()
fragment.arguments = args
return fragment
}
}
}

View File

@@ -181,14 +181,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
}
fun checkMainCredential(mainCredential: MainCredential) {
mDatabase?.let { database ->
database.fileUri?.let { databaseUri ->
mDatabaseViewModel.checkMainCredential(databaseUri, mainCredential)
}
}
}
fun saveDatabase() {
mDatabaseViewModel.saveDatabase(save = true)
}

View File

@@ -2,9 +2,7 @@ package com.kunzisoft.keepass.credentialprovider
import android.content.Context
import android.content.Intent
import android.provider.Settings
import android.util.Log
import androidx.appcompat.app.AlertDialog
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
@@ -13,6 +11,7 @@ import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.CheckDatabaseCredentialDialogFragment
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.utils.getEnumExtra
@@ -99,9 +98,7 @@ class UserVerificationHelper {
if (isAuthenticatorsAllowed()) {
showUserVerificationDeviceCredential(userVerificationViewModel, dataToVerify)
} else {
showUserVerificationMessage {
userVerificationViewModel.onUserVerificationFailed()
}
showUserVerificationDatabaseCredential(userVerificationViewModel, dataToVerify)
}
}
@@ -145,33 +142,22 @@ class UserVerificationHelper {
}
}).authenticate(
BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.user_verification_required))
.setTitle(getString(R.string.user_verification_required_title))
.setSubtitle(getString(R.string.user_verification_required_description))
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
.setConfirmationRequired(false)
.build()
)
}
fun FragmentActivity.showUserVerificationMessage(
onActionPerformed: () -> Unit
fun FragmentActivity.showUserVerificationDatabaseCredential(
userVerificationViewModel: UserVerificationViewModel,
dataToVerify: UserVerificationData
) {
AlertDialog.Builder(this)
.setTitle(getString(R.string.set_up_user_verification_passkeys_title))
.setMessage(getString(R.string.set_up_user_verification_passkeys_description))
.setPositiveButton(resources.getString(R.string.set_up_user_verification)
) { _, _ ->
startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
onActionPerformed()
}
.setNegativeButton(resources.getString(android.R.string.cancel)
) { _, _ ->
onActionPerformed()
}
.setOnDismissListener {
onActionPerformed()
}
.create()
.show()
userVerificationViewModel.dataToVerify = dataToVerify
CheckDatabaseCredentialDialogFragment
.getInstance()
.show(this.supportFragmentManager, "checkDatabaseCredentialDialog")
}
}
}

View File

@@ -59,7 +59,6 @@ import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewMod
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CHECK_CREDENTIAL_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.settings.PreferencesUtil.isPasskeyUserVerificationPreferred
import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -189,6 +188,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
userVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
toastError(uiState.error)
passkeyLauncherViewModel.cancelResult()
userVerificationViewModel.onUserVerificationReceived()
}
@@ -229,17 +229,6 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
// TODO When auto save is enabled, WARNING filter by the calling activity
// passkeyLauncherViewModel.autoSelectPasskey(result, database)
}
ACTION_DATABASE_CHECK_CREDENTIAL_TASK -> {
if (result.isSuccess) {
userVerificationViewModel.onUserVerificationSucceeded(
UserVerificationData(database)
)
} else {
userVerificationViewModel.onUserVerificationFailed(
UserVerificationData(database)
)
}
}
}
}

View File

@@ -49,7 +49,6 @@ import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CHECK_CREDENTIAL_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
@@ -239,7 +238,7 @@ class DatabaseTaskProvider(
try {
context.unregisterReceiver(databaseTaskBroadcastReceiver)
} catch (e: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
// If receiver not register, do nothing
}
}
@@ -326,16 +325,6 @@ class DatabaseTaskProvider(
}, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
}
fun startDatabaseCheckCredential(
databaseUri: Uri,
mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}, ACTION_DATABASE_CHECK_CREDENTIAL_TASK)
}
/*
----
Nodes Actions

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.exception.DatabaseInputException
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.getUriInputStream
class CheckCredentialDatabaseRunnable(
private val context: Context,
private val mDatabase: ContextualDatabase,
private val mDatabaseUri: Uri,
private val mMainCredential: MainCredential,
private val mChallengeResponseRetriever: (hardwareKey: HardwareKey, seed: ByteArray?) -> ByteArray,
private val progressTaskUpdater: ProgressTaskUpdater?
) : ActionRunnable() {
var afterCheckCredential : ((Result) -> Unit)? = null
override fun onStartRun() {}
override fun onActionRun() {
try {
val contentResolver = context.contentResolver
mDatabase.fileUri = mDatabaseUri
mDatabase.checkMasterKey(
databaseStream = contentResolver.getUriInputStream(mDatabaseUri)
?: throw UnknownDatabaseLocationException(),
masterCredential = mMainCredential.toMasterCredential(contentResolver),
challengeResponseRetriever = mChallengeResponseRetriever,
progressTaskUpdater = progressTaskUpdater
)
} catch (e: DatabaseInputException) {
setError(e)
}
}
override fun onFinishRun() {
afterCheckCredential?.invoke(result)
}
}

View File

@@ -37,7 +37,6 @@ import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.ProgressMessage
import com.kunzisoft.keepass.database.action.CheckCredentialDatabaseRunnable
import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable
import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
import com.kunzisoft.keepass.database.action.MergeDatabaseRunnable
@@ -349,7 +348,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
ACTION_DATABASE_MERGE_TASK -> buildDatabaseMergeActionTask(intent, database)
ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask(database)
ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK -> buildDatabaseAssignCredentialActionTask(intent, database)
ACTION_DATABASE_CHECK_CREDENTIAL_TASK -> buildDatabaseCheckCredentialActionTask(intent, database)
ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent, database)
ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent, database)
ACTION_DATABASE_CREATE_ENTRY_TASK -> buildDatabaseCreateEntryActionTask(intent, database)
@@ -944,37 +942,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
}
private fun buildDatabaseCheckCredentialActionTask(
intent: Intent,
database: ContextualDatabase
): ActionRunnable? {
return if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MAIN_CREDENTIAL_KEY)
) {
val databaseUri: Uri = intent.getParcelableExtraCompat(DATABASE_URI_KEY) ?: return null
val mainCredential: MainCredential =
intent.getParcelableExtraCompat(MAIN_CREDENTIAL_KEY) ?: MainCredential()
CheckCredentialDatabaseRunnable(
context = this,
mDatabase = database,
mDatabaseUri = databaseUri,
mMainCredential = mainCredential,
mChallengeResponseRetriever = { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
},
progressTaskUpdater = this
).apply {
afterCheckCredential = {
result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
}
}
}
} else {
null
}
}
private fun eraseCredentials(databaseUri: Uri) {
// Erase the biometric
CipherDatabaseAction.getInstance(this)
@@ -1363,7 +1330,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val ACTION_DATABASE_MERGE_TASK = "ACTION_DATABASE_MERGE_TASK"
const val ACTION_DATABASE_RELOAD_TASK = "ACTION_DATABASE_RELOAD_TASK"
const val ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK = "ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK"
const val ACTION_DATABASE_CHECK_CREDENTIAL_TASK = "ACTION_DATABASE_CHECK_CREDENTIAL_TASK"
const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK"
const val ACTION_DATABASE_UPDATE_GROUP_TASK = "ACTION_DATABASE_UPDATE_GROUP_TASK"
const val ACTION_DATABASE_CREATE_ENTRY_TASK = "ACTION_DATABASE_CREATE_ENTRY_TASK"

View File

@@ -238,15 +238,13 @@ fun View.updateLockPaddingStart() {
}
}
fun Context.toastError(e: Throwable) {
Toast.makeText(
applicationContext,
if (e is LocalizedException)
fun Context.toastError(e: Throwable?) {
val message = if (e is LocalizedException)
e.getLocalizedMessage(resources)
else
e.localizedMessage,
Toast.LENGTH_LONG
).show()
else e?.localizedMessage
message?.let {
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
}
}
fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
@@ -259,6 +257,15 @@ fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
}
}
fun CoordinatorLayout.showError(error: Throwable?) {
val message = if (error is LocalizedException) {
error.getLocalizedMessage(resources) ?: error.message
} else error?.message
message?.let {
Snackbar.make(this, message, Snackbar.LENGTH_LONG).asError().show()
}
}
fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) {
if (!result.isSuccess) {
result.exception?.getLocalizedMessage(resources)?.let { errorMessage ->

View File

@@ -133,13 +133,6 @@ class DatabaseViewModel(application: Application): AndroidViewModel(application)
}
}
fun checkMainCredential(
databaseUri: Uri,
mainCredential: MainCredential
) {
mDatabaseTaskProvider.startDatabaseCheckCredential(databaseUri, mainCredential)
}
fun saveDatabase(save: Boolean, saveToUri: Uri? = null) {
mDatabaseTaskProvider.startDatabaseSave(save, saveToUri)
}

View File

@@ -2,6 +2,8 @@ package com.kunzisoft.keepass.viewmodels
import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.database.element.MasterCredential.CREATOR.getCheckKey
import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -13,12 +15,28 @@ class UserVerificationViewModel: ViewModel() {
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
var dataToVerify: UserVerificationData = UserVerificationData()
fun checkMainCredential(checkString: String) {
// Check the password part
if (dataToVerify.database?.checkKey(getCheckKey(checkString)) == true)
onUserVerificationSucceeded(dataToVerify)
else {
onUserVerificationFailed(dataToVerify, InvalidCredentialsDatabaseException())
}
dataToVerify = UserVerificationData()
}
fun onUserVerificationSucceeded(dataToVerify: UserVerificationData) {
mUiState.value = UIState.OnUserVerificationSucceeded(dataToVerify)
}
fun onUserVerificationFailed(dataToVerify: UserVerificationData = UserVerificationData()) {
mUiState.value = UIState.OnUserVerificationCanceled(dataToVerify)
fun onUserVerificationFailed(
dataToVerify: UserVerificationData = UserVerificationData(),
error: Throwable? = null
) {
this.dataToVerify = dataToVerify
mUiState.value = UIState.OnUserVerificationCanceled(dataToVerify, error)
}
fun onUserVerificationReceived() {
@@ -28,7 +46,10 @@ class UserVerificationViewModel: ViewModel() {
sealed class UIState {
object Loading: UIState()
data class OnUserVerificationSucceeded(val dataToVerify: UserVerificationData): UIState()
data class OnUserVerificationCanceled(val dataToVerify: UserVerificationData): UIState()
data class OnUserVerificationCanceled(
val dataToVerify: UserVerificationData,
val error: Throwable?
): UIState()
}
}

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2025 Jeremy Jamet / Kunzisoft.
This file is part of KeePassDX.
KeePassDX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
KeePassDX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:padding="@dimen/default_margin"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/user_verification_information"
android:text="@string/user_verification_required_title"
style="@style/KeepassDXStyle.Title"/>
<ImageView
android:id="@+id/user_verification_information"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/ic_info_white_24dp"
style="@style/KeepassDXStyle.ImageButton.Simple"
android:contentDescription="@string/content_description_user_verification_information"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/user_verification_required_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/card_view_margin_horizontal"
android:layout_marginLeft="@dimen/card_view_margin_horizontal"
android:layout_marginEnd="@dimen/card_view_margin_horizontal"
android:layout_marginRight="@dimen/card_view_margin_horizontal"
android:text="@string/user_verification_database_credential"
android:textColor="?attr/colorSecondary"/>
<!-- Password -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/setup_check_password_input_layout"
android:layout_margin="@dimen/card_view_margin_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:endIconMode="password_toggle"
app:endIconTint="?attr/colorSecondary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/setup_check_password_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusedByDefault="true"
android:maxLength="4"
android:inputType="textPassword"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:hint="@string/first_chars" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>

View File

@@ -45,6 +45,7 @@
<string name="magic_keyboard_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/Magikeyboard</string>
<string name="clipboard_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/Clipboard</string>
<string name="passkeys_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/Passkeys</string>
<string name="user_verification_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/Passkeys#UserVerification</string>
<string name="autofill_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/Autofill</string>
<string name="file_manager_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/File-Manager-and-Sync</string>
<string name="html_rose">--,--`--,{@</string>

View File

@@ -775,8 +775,10 @@
<string name="passkey_backup_state">Passkey Backup State</string>
<string name="error_passkey_result">Unable to return the passkey</string>
<string name="error_passkey_credential_id">No passkey found with relying party %1$s and credentialIds %2$s</string>
<string name="user_verification_required">User verification required</string>
<string name="set_up_user_verification">Set up user verification</string>
<string name="set_up_user_verification_passkeys_title">Set up user verification to use passkeys</string>
<string name="set_up_user_verification_passkeys_description">To use passkeys, make sure you have a device screen lock set up</string>
<string name="content_description_user_verification_information">User verification info</string>
<string name="user_verification_required_title">User Verification</string>
<string name="user_verification_required_description">User verification is required to use and edit passkeys</string>
<string name="user_verification_database_credential">Enter the first four characters of your database password</string>
<string name="first_chars">First chars</string>
<string name="check">Check</string>
</resources>

View File

@@ -288,8 +288,7 @@
<!-- Dialog -->
<style name="KeepassDXStyle.Light.Dialog" parent="ThemeOverlay.Material3.Dialog.Alert">
<item name="android:windowBackground">?attr/dialogBackgroundColor</item>
<item name="background">?attr/dialogBackgroundColor</item>
<item name="android:colorBackground">?attr/dialogBackgroundColor</item>
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="dialogCornerRadius">@dimen/dialog_radius</item>
<item name="buttonBarNegativeButtonStyle">@style/KeepassDXStyle.Light.Dialog.NegativeButtonStyle</item>
@@ -303,8 +302,7 @@
</style>
<style name="KeepassDXStyle.Night.Dialog" parent="ThemeOverlay.Material3.Dialog.Alert">
<item name="android:windowBackground">?attr/dialogBackgroundColor</item>
<item name="background">?attr/dialogBackgroundColor</item>
<item name="android:colorBackground">?attr/dialogBackgroundColor</item>
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="dialogCornerRadius">@dimen/dialog_radius</item>
<item name="buttonBarNegativeButtonStyle">@style/KeepassDXStyle.Night.Dialog.NegativeButtonStyle</item>

View File

@@ -42,7 +42,6 @@ import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.database.exception.DatabaseInputException
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
import com.kunzisoft.keepass.database.exception.MergeDatabaseKDBException
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
@@ -391,6 +390,9 @@ open class Database {
val transformSeed: ByteArray?
get() = mDatabaseKDB?.transformSeed ?: mDatabaseKDBX?.transformSeed
private val checkKey: ByteArray
get() = mDatabaseKDB?.checkKey ?: mDatabaseKDBX?.checkKey ?: ByteArray(32)
var rootGroup: Group?
get() {
mDatabaseKDB?.rootGroup?.let {
@@ -619,51 +621,11 @@ open class Database {
}
}
fun checkMasterKey(
databaseStream: InputStream,
masterCredential: MasterCredential,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
progressTaskUpdater: ProgressTaskUpdater?
) {
try {
var masterKey = byteArrayOf()
// Read database stream for the first time
readDatabaseStream(databaseStream,
{ databaseInputStream ->
val databaseKDB = DatabaseKDB()
DatabaseInputKDB(databaseKDB)
.openDatabase(databaseInputStream,
progressTaskUpdater
) {
databaseKDB.deriveMasterKey(
masterCredential
)
}
masterKey = databaseKDB.masterKey
},
{ databaseInputStream ->
val databaseKDBX = DatabaseKDBX()
DatabaseInputKDBX(databaseKDBX).apply {
openDatabase(databaseInputStream,
progressTaskUpdater) {
databaseKDBX.deriveMasterKey(
masterCredential,
challengeResponseRetriever
)
}
}
masterKey = databaseKDBX.masterKey
}
)
if (!this.masterKey.contentEquals(masterKey)) {
throw InvalidCredentialsDatabaseException()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to check the main credential")
if (e is DatabaseInputException)
throw e
throw DatabaseInputException(e)
}
/**
* Check if the key is valid
*/
fun checkKey(key: ByteArray): Boolean {
return checkKey.contentEquals(key)
}
fun isMergeDataAllowed(): Boolean {
@@ -1268,7 +1230,7 @@ open class Database {
}
fun undoRecycle(entry: Entry, parent: Group) {
recycleBin?.let { it ->
recycleBin?.let {
removeEntryFrom(entry, it)
}
addEntryTo(entry, parent)

View File

@@ -64,6 +64,10 @@ data class MasterCredential(
return 0
}
fun getCheckKey(): ByteArray {
return getCheckKey(password)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -95,6 +99,15 @@ data class MasterCredential(
private val TAG = MasterCredential::class.java.simpleName
fun getCheckKey(password: String?): ByteArray {
return retrievePasswordKey(
try {
password?.substring(0, 3) ?: ""
} catch (_: Exception) { "" },
Charsets.UTF_8
)
}
@Throws(IOException::class)
fun retrievePasswordKey(
key: String,
@@ -102,7 +115,7 @@ data class MasterCredential(
): ByteArray {
val bKey: ByteArray = try {
key.toByteArray(encoding)
} catch (e: UnsupportedEncodingException) {
} catch (_: UnsupportedEncodingException) {
key.toByteArray()
}
return HashManager.hashSha256(bKey)
@@ -128,7 +141,7 @@ data class MasterCredential(
32 -> return keyFileData
64 -> try {
return Hex.decodeHex(String(keyFileData).toCharArray())
} catch (ignoredException: Exception) {
} catch (_: Exception) {
// Key is not base 64, treat it as binary data
}
}
@@ -151,7 +164,7 @@ data class MasterCredential(
// Disable certain unsecure XML-Parsing DocumentBuilderFactory features
try {
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
} catch (e : ParserConfigurationException) {
} catch (_ : ParserConfigurationException) {
Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)")
}
@@ -187,7 +200,7 @@ data class MasterCredential(
xmlKeyFileVersion = versionText.toFloat()
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
} catch (e: Exception) {
Log.e(TAG, "XML Keyfile version cannot be read : $versionText")
Log.e(TAG, "XML Keyfile version cannot be read : $versionText", e)
}
}
}
@@ -235,7 +248,7 @@ data class MasterCredential(
}
}
}
} catch (e: Exception) {
} catch (_: Exception) {
return null
}
return null

View File

@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.database.exception.EmptyKeyDatabaseException
import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
import java.io.IOException
import java.nio.charset.Charset
import java.util.*
import java.util.UUID
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
@@ -158,6 +158,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
} else {
this.masterKey = passwordBytes ?: keyFileBytes ?: byteArrayOf(0)
}
// Build check key
this.checkKey = masterCredential.getCheckKey()
}
override fun createGroup(): GroupKDB {

View File

@@ -19,7 +19,6 @@
*/
package com.kunzisoft.keepass.database.element.database
import android.util.Base64
import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
@@ -28,7 +27,12 @@ import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.CompositeKey
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.MasterCredential
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
@@ -36,7 +40,11 @@ import com.kunzisoft.keepass.database.element.entry.FieldReferencesEngine
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.*
import com.kunzisoft.keepass.database.element.node.NodeHandler
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
@@ -51,7 +59,8 @@ import java.io.IOException
import java.nio.charset.Charset
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.*
import java.util.Arrays
import java.util.UUID
import javax.crypto.Mac
import kotlin.math.min
@@ -253,6 +262,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
keyFileBytes,
hardwareKeyBytes
)
// Build check key
this.checkKey = masterCredential.getCheckKey()
}
@Throws(DatabaseOutputException::class)
@@ -635,7 +647,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
messageDigest = MessageDigest.getInstance("SHA-512")
cmpKey[64] = 1
hmacKey = messageDigest.digest(cmpKey)
} catch (e: NoSuchAlgorithmException) {
} catch (_: NoSuchAlgorithmException) {
throw IOException("No SHA-512 implementation")
} finally {
Arrays.fill(cmpKey, 0.toByte())

View File

@@ -34,7 +34,7 @@ import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import java.util.*
import java.util.UUID
abstract class DatabaseVersioned<
GroupId,
@@ -58,6 +58,8 @@ abstract class DatabaseVersioned<
protected set
var transformSeed: ByteArray? = null
var checkKey = ByteArray(32)
abstract val version: String
abstract val defaultFileExtension: String
@@ -89,10 +91,6 @@ abstract class DatabaseVersioned<
return getGroupIndexes().filter { it != rootGroup }
}
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
return null
}
open fun isValidCredential(password: String?, containsKeyFile: Boolean): Boolean {
if (password == null && !containsKeyFile)
return false
@@ -105,14 +103,14 @@ abstract class DatabaseVersioned<
val bKey: ByteArray
try {
bKey = password.toByteArray(encoding)
} catch (e: UnsupportedEncodingException) {
} catch (_: UnsupportedEncodingException) {
return false
}
val reEncoded: String
try {
reEncoded = String(bKey, encoding)
} catch (e: UnsupportedEncodingException) {
} catch (_: UnsupportedEncodingException) {
return false
}
return password == reEncoded
@@ -121,6 +119,7 @@ abstract class DatabaseVersioned<
fun copyMasterKeyFrom(databaseVersioned: DatabaseVersioned<GroupId, EntryId, Group, Entry>) {
this.masterKey = databaseVersioned.masterKey
this.transformSeed = databaseVersioned.transformSeed
this.checkKey = databaseVersioned.checkKey
}
/*