mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Added a dialog to verify the password #2283
This commit is contained in:
@@ -84,6 +84,7 @@ import com.kunzisoft.keepass.view.changeTitleColor
|
|||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import com.kunzisoft.keepass.view.showError
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
|
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -214,7 +215,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
|
|
||||||
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
|
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
|
||||||
}
|
}
|
||||||
} catch (e: ClassCastException) {
|
} catch (_: ClassCastException) {
|
||||||
Log.e(TAG, "Unable to retrieve the entry key")
|
Log.e(TAG, "Unable to retrieve the entry key")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,6 +329,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
when (uIState) {
|
when (uIState) {
|
||||||
is UserVerificationViewModel.UIState.Loading -> {}
|
is UserVerificationViewModel.UIState.Loading -> {}
|
||||||
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
|
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
|
||||||
|
coordinatorLayout?.showError(uIState.error)
|
||||||
mUserVerificationViewModel.onUserVerificationReceived()
|
mUserVerificationViewModel.onUserVerificationReceived()
|
||||||
}
|
}
|
||||||
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
|
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ import com.kunzisoft.keepass.view.applyWindowInsets
|
|||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import com.kunzisoft.keepass.view.showError
|
||||||
import com.kunzisoft.keepass.view.toastError
|
import com.kunzisoft.keepass.view.toastError
|
||||||
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||||
@@ -581,6 +582,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
when (uIState) {
|
when (uIState) {
|
||||||
is UserVerificationViewModel.UIState.Loading -> {}
|
is UserVerificationViewModel.UIState.Loading -> {}
|
||||||
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
|
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
|
||||||
|
coordinatorLayout?.showError(uIState.error)
|
||||||
mUserVerificationViewModel.onUserVerificationReceived()
|
mUserVerificationViewModel.onUserVerificationReceived()
|
||||||
}
|
}
|
||||||
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
|
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
|
||||||
@@ -711,14 +713,6 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, 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) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
@@ -731,6 +725,12 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
// Search not used
|
// Search not used
|
||||||
},
|
},
|
||||||
selectionAction = { intentSenderMode, typeMode, searchInfo ->
|
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) {
|
when (typeMode) {
|
||||||
TypeMode.DEFAULT -> {}
|
TypeMode.DEFAULT -> {}
|
||||||
TypeMode.MAGIKEYBOARD -> entry?.let {
|
TypeMode.MAGIKEYBOARD -> entry?.let {
|
||||||
@@ -749,15 +749,13 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coordinatorError?.showActionErrorIfNeeded(result)
|
coordinatorError?.showActionErrorIfNeeded(result)
|
||||||
|
|
||||||
// Reload the group
|
// Reload the group
|
||||||
loadGroup()
|
loadGroup()
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun manageIntent(intent: Intent?) {
|
private fun manageIntent(intent: Intent?) {
|
||||||
intent?.let {
|
intent?.let {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
fun saveDatabase() {
|
||||||
mDatabaseViewModel.saveDatabase(save = true)
|
mDatabaseViewModel.saveDatabase(save = true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package com.kunzisoft.keepass.credentialprovider
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.Settings
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||||
@@ -13,6 +11,7 @@ import androidx.biometric.BiometricPrompt
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.CheckDatabaseCredentialDialogFragment
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.utils.getEnumExtra
|
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||||
@@ -99,9 +98,7 @@ class UserVerificationHelper {
|
|||||||
if (isAuthenticatorsAllowed()) {
|
if (isAuthenticatorsAllowed()) {
|
||||||
showUserVerificationDeviceCredential(userVerificationViewModel, dataToVerify)
|
showUserVerificationDeviceCredential(userVerificationViewModel, dataToVerify)
|
||||||
} else {
|
} else {
|
||||||
showUserVerificationMessage {
|
showUserVerificationDatabaseCredential(userVerificationViewModel, dataToVerify)
|
||||||
userVerificationViewModel.onUserVerificationFailed()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,33 +142,22 @@ class UserVerificationHelper {
|
|||||||
}
|
}
|
||||||
}).authenticate(
|
}).authenticate(
|
||||||
BiometricPrompt.PromptInfo.Builder()
|
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)
|
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
|
||||||
.setConfirmationRequired(false)
|
.setConfirmationRequired(false)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun FragmentActivity.showUserVerificationMessage(
|
fun FragmentActivity.showUserVerificationDatabaseCredential(
|
||||||
onActionPerformed: () -> Unit
|
userVerificationViewModel: UserVerificationViewModel,
|
||||||
|
dataToVerify: UserVerificationData
|
||||||
) {
|
) {
|
||||||
AlertDialog.Builder(this)
|
userVerificationViewModel.dataToVerify = dataToVerify
|
||||||
.setTitle(getString(R.string.set_up_user_verification_passkeys_title))
|
CheckDatabaseCredentialDialogFragment
|
||||||
.setMessage(getString(R.string.set_up_user_verification_passkeys_description))
|
.getInstance()
|
||||||
.setPositiveButton(resources.getString(R.string.set_up_user_verification)
|
.show(this.supportFragmentManager, "checkDatabaseCredentialDialog")
|
||||||
) { _, _ ->
|
|
||||||
startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
|
||||||
onActionPerformed()
|
|
||||||
}
|
|
||||||
.setNegativeButton(resources.getString(android.R.string.cancel)
|
|
||||||
) { _, _ ->
|
|
||||||
onActionPerformed()
|
|
||||||
}
|
|
||||||
.setOnDismissListener {
|
|
||||||
onActionPerformed()
|
|
||||||
}
|
|
||||||
.create()
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewMod
|
|||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.model.AppOrigin
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
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.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil.isPasskeyUserVerificationPreferred
|
import com.kunzisoft.keepass.settings.PreferencesUtil.isPasskeyUserVerificationPreferred
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
@@ -189,6 +188,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
userVerificationViewModel.onUserVerificationReceived()
|
userVerificationViewModel.onUserVerificationReceived()
|
||||||
}
|
}
|
||||||
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
|
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
|
||||||
|
toastError(uiState.error)
|
||||||
passkeyLauncherViewModel.cancelResult()
|
passkeyLauncherViewModel.cancelResult()
|
||||||
userVerificationViewModel.onUserVerificationReceived()
|
userVerificationViewModel.onUserVerificationReceived()
|
||||||
}
|
}
|
||||||
@@ -229,17 +229,6 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
// TODO When auto save is enabled, WARNING filter by the calling activity
|
// TODO When auto save is enabled, WARNING filter by the calling activity
|
||||||
// passkeyLauncherViewModel.autoSelectPasskey(result, database)
|
// passkeyLauncherViewModel.autoSelectPasskey(result, database)
|
||||||
}
|
}
|
||||||
ACTION_DATABASE_CHECK_CREDENTIAL_TASK -> {
|
|
||||||
if (result.isSuccess) {
|
|
||||||
userVerificationViewModel.onUserVerificationSucceeded(
|
|
||||||
UserVerificationData(database)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
userVerificationViewModel.onUserVerificationFailed(
|
|
||||||
UserVerificationData(database)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
|
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_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_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_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||||
@@ -239,7 +238,7 @@ class DatabaseTaskProvider(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
context.unregisterReceiver(databaseTaskBroadcastReceiver)
|
context.unregisterReceiver(databaseTaskBroadcastReceiver)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
// If receiver not register, do nothing
|
// If receiver not register, do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,16 +325,6 @@ class DatabaseTaskProvider(
|
|||||||
}, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
|
}, 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
|
Nodes Actions
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,7 +37,6 @@ import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
|
|||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.ProgressMessage
|
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.CreateDatabaseRunnable
|
||||||
import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
|
import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
|
||||||
import com.kunzisoft.keepass.database.action.MergeDatabaseRunnable
|
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_MERGE_TASK -> buildDatabaseMergeActionTask(intent, database)
|
||||||
ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask(database)
|
ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask(database)
|
||||||
ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK -> buildDatabaseAssignCredentialActionTask(intent, 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_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent, database)
|
||||||
ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent, database)
|
ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent, database)
|
||||||
ACTION_DATABASE_CREATE_ENTRY_TASK -> buildDatabaseCreateEntryActionTask(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) {
|
private fun eraseCredentials(databaseUri: Uri) {
|
||||||
// Erase the biometric
|
// Erase the biometric
|
||||||
CipherDatabaseAction.getInstance(this)
|
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_MERGE_TASK = "ACTION_DATABASE_MERGE_TASK"
|
||||||
const val ACTION_DATABASE_RELOAD_TASK = "ACTION_DATABASE_RELOAD_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_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_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK"
|
||||||
const val ACTION_DATABASE_UPDATE_GROUP_TASK = "ACTION_DATABASE_UPDATE_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"
|
const val ACTION_DATABASE_CREATE_ENTRY_TASK = "ACTION_DATABASE_CREATE_ENTRY_TASK"
|
||||||
|
|||||||
@@ -238,15 +238,13 @@ fun View.updateLockPaddingStart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.toastError(e: Throwable) {
|
fun Context.toastError(e: Throwable?) {
|
||||||
Toast.makeText(
|
val message = if (e is LocalizedException)
|
||||||
applicationContext,
|
|
||||||
if (e is LocalizedException)
|
|
||||||
e.getLocalizedMessage(resources)
|
e.getLocalizedMessage(resources)
|
||||||
else
|
else e?.localizedMessage
|
||||||
e.localizedMessage,
|
message?.let {
|
||||||
Toast.LENGTH_LONG
|
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
|
||||||
).show()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
|
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) {
|
fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) {
|
||||||
if (!result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
result.exception?.getLocalizedMessage(resources)?.let { errorMessage ->
|
result.exception?.getLocalizedMessage(resources)?.let { errorMessage ->
|
||||||
|
|||||||
@@ -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) {
|
fun saveDatabase(save: Boolean, saveToUri: Uri? = null) {
|
||||||
mDatabaseTaskProvider.startDatabaseSave(save, saveToUri)
|
mDatabaseTaskProvider.startDatabaseSave(save, saveToUri)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.kunzisoft.keepass.viewmodels
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
@@ -13,12 +15,28 @@ class UserVerificationViewModel: ViewModel() {
|
|||||||
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||||
val uiState: StateFlow<UIState> = mUiState
|
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) {
|
fun onUserVerificationSucceeded(dataToVerify: UserVerificationData) {
|
||||||
mUiState.value = UIState.OnUserVerificationSucceeded(dataToVerify)
|
mUiState.value = UIState.OnUserVerificationSucceeded(dataToVerify)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onUserVerificationFailed(dataToVerify: UserVerificationData = UserVerificationData()) {
|
fun onUserVerificationFailed(
|
||||||
mUiState.value = UIState.OnUserVerificationCanceled(dataToVerify)
|
dataToVerify: UserVerificationData = UserVerificationData(),
|
||||||
|
error: Throwable? = null
|
||||||
|
) {
|
||||||
|
this.dataToVerify = dataToVerify
|
||||||
|
mUiState.value = UIState.OnUserVerificationCanceled(dataToVerify, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onUserVerificationReceived() {
|
fun onUserVerificationReceived() {
|
||||||
@@ -28,7 +46,10 @@ class UserVerificationViewModel: ViewModel() {
|
|||||||
sealed class UIState {
|
sealed class UIState {
|
||||||
object Loading: UIState()
|
object Loading: UIState()
|
||||||
data class OnUserVerificationSucceeded(val dataToVerify: UserVerificationData): 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
<string name="magic_keyboard_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/Magikeyboard</string>
|
<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="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="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="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="file_manager_explanation_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/wiki/File-Manager-and-Sync</string>
|
||||||
<string name="html_rose">--,--`--,{@</string>
|
<string name="html_rose">--,--`--,{@</string>
|
||||||
|
|||||||
@@ -775,8 +775,10 @@
|
|||||||
<string name="passkey_backup_state">Passkey Backup State</string>
|
<string name="passkey_backup_state">Passkey Backup State</string>
|
||||||
<string name="error_passkey_result">Unable to return the passkey</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="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="content_description_user_verification_information">User verification info</string>
|
||||||
<string name="set_up_user_verification">Set up user verification</string>
|
<string name="user_verification_required_title">User Verification</string>
|
||||||
<string name="set_up_user_verification_passkeys_title">Set up user verification to use passkeys</string>
|
<string name="user_verification_required_description">User verification is required to use and edit 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="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>
|
</resources>
|
||||||
@@ -288,8 +288,7 @@
|
|||||||
|
|
||||||
<!-- Dialog -->
|
<!-- Dialog -->
|
||||||
<style name="KeepassDXStyle.Light.Dialog" parent="ThemeOverlay.Material3.Dialog.Alert">
|
<style name="KeepassDXStyle.Light.Dialog" parent="ThemeOverlay.Material3.Dialog.Alert">
|
||||||
<item name="android:windowBackground">?attr/dialogBackgroundColor</item>
|
<item name="android:colorBackground">?attr/dialogBackgroundColor</item>
|
||||||
<item name="background">?attr/dialogBackgroundColor</item>
|
|
||||||
<item name="android:windowSoftInputMode">adjustResize</item>
|
<item name="android:windowSoftInputMode">adjustResize</item>
|
||||||
<item name="dialogCornerRadius">@dimen/dialog_radius</item>
|
<item name="dialogCornerRadius">@dimen/dialog_radius</item>
|
||||||
<item name="buttonBarNegativeButtonStyle">@style/KeepassDXStyle.Light.Dialog.NegativeButtonStyle</item>
|
<item name="buttonBarNegativeButtonStyle">@style/KeepassDXStyle.Light.Dialog.NegativeButtonStyle</item>
|
||||||
@@ -303,8 +302,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="KeepassDXStyle.Night.Dialog" parent="ThemeOverlay.Material3.Dialog.Alert">
|
<style name="KeepassDXStyle.Night.Dialog" parent="ThemeOverlay.Material3.Dialog.Alert">
|
||||||
<item name="android:windowBackground">?attr/dialogBackgroundColor</item>
|
<item name="android:colorBackground">?attr/dialogBackgroundColor</item>
|
||||||
<item name="background">?attr/dialogBackgroundColor</item>
|
|
||||||
<item name="android:windowSoftInputMode">adjustResize</item>
|
<item name="android:windowSoftInputMode">adjustResize</item>
|
||||||
<item name="dialogCornerRadius">@dimen/dialog_radius</item>
|
<item name="dialogCornerRadius">@dimen/dialog_radius</item>
|
||||||
<item name="buttonBarNegativeButtonStyle">@style/KeepassDXStyle.Night.Dialog.NegativeButtonStyle</item>
|
<item name="buttonBarNegativeButtonStyle">@style/KeepassDXStyle.Night.Dialog.NegativeButtonStyle</item>
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import com.kunzisoft.keepass.database.exception.DatabaseException
|
|||||||
import com.kunzisoft.keepass.database.exception.DatabaseInputException
|
import com.kunzisoft.keepass.database.exception.DatabaseInputException
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
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.MergeDatabaseKDBException
|
||||||
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
|
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
@@ -391,6 +390,9 @@ open class Database {
|
|||||||
val transformSeed: ByteArray?
|
val transformSeed: ByteArray?
|
||||||
get() = mDatabaseKDB?.transformSeed ?: mDatabaseKDBX?.transformSeed
|
get() = mDatabaseKDB?.transformSeed ?: mDatabaseKDBX?.transformSeed
|
||||||
|
|
||||||
|
private val checkKey: ByteArray
|
||||||
|
get() = mDatabaseKDB?.checkKey ?: mDatabaseKDBX?.checkKey ?: ByteArray(32)
|
||||||
|
|
||||||
var rootGroup: Group?
|
var rootGroup: Group?
|
||||||
get() {
|
get() {
|
||||||
mDatabaseKDB?.rootGroup?.let {
|
mDatabaseKDB?.rootGroup?.let {
|
||||||
@@ -619,51 +621,11 @@ open class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkMasterKey(
|
/**
|
||||||
databaseStream: InputStream,
|
* Check if the key is valid
|
||||||
masterCredential: MasterCredential,
|
*/
|
||||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
fun checkKey(key: ByteArray): Boolean {
|
||||||
progressTaskUpdater: ProgressTaskUpdater?
|
return checkKey.contentEquals(key)
|
||||||
) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isMergeDataAllowed(): Boolean {
|
fun isMergeDataAllowed(): Boolean {
|
||||||
@@ -1268,7 +1230,7 @@ open class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun undoRecycle(entry: Entry, parent: Group) {
|
fun undoRecycle(entry: Entry, parent: Group) {
|
||||||
recycleBin?.let { it ->
|
recycleBin?.let {
|
||||||
removeEntryFrom(entry, it)
|
removeEntryFrom(entry, it)
|
||||||
}
|
}
|
||||||
addEntryTo(entry, parent)
|
addEntryTo(entry, parent)
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ data class MasterCredential(
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCheckKey(): ByteArray {
|
||||||
|
return getCheckKey(password)
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
@@ -95,6 +99,15 @@ data class MasterCredential(
|
|||||||
|
|
||||||
private val TAG = MasterCredential::class.java.simpleName
|
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)
|
@Throws(IOException::class)
|
||||||
fun retrievePasswordKey(
|
fun retrievePasswordKey(
|
||||||
key: String,
|
key: String,
|
||||||
@@ -102,7 +115,7 @@ data class MasterCredential(
|
|||||||
): ByteArray {
|
): ByteArray {
|
||||||
val bKey: ByteArray = try {
|
val bKey: ByteArray = try {
|
||||||
key.toByteArray(encoding)
|
key.toByteArray(encoding)
|
||||||
} catch (e: UnsupportedEncodingException) {
|
} catch (_: UnsupportedEncodingException) {
|
||||||
key.toByteArray()
|
key.toByteArray()
|
||||||
}
|
}
|
||||||
return HashManager.hashSha256(bKey)
|
return HashManager.hashSha256(bKey)
|
||||||
@@ -128,7 +141,7 @@ data class MasterCredential(
|
|||||||
32 -> return keyFileData
|
32 -> return keyFileData
|
||||||
64 -> try {
|
64 -> try {
|
||||||
return Hex.decodeHex(String(keyFileData).toCharArray())
|
return Hex.decodeHex(String(keyFileData).toCharArray())
|
||||||
} catch (ignoredException: Exception) {
|
} catch (_: Exception) {
|
||||||
// Key is not base 64, treat it as binary data
|
// Key is not base 64, treat it as binary data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +164,7 @@ data class MasterCredential(
|
|||||||
// Disable certain unsecure XML-Parsing DocumentBuilderFactory features
|
// Disable certain unsecure XML-Parsing DocumentBuilderFactory features
|
||||||
try {
|
try {
|
||||||
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
|
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)")
|
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()
|
xmlKeyFileVersion = versionText.toFloat()
|
||||||
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
|
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
|
||||||
} catch (e: Exception) {
|
} 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
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.database.exception.EmptyKeyDatabaseException
|
|||||||
import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
|
import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||||
|
|
||||||
@@ -158,6 +158,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
} else {
|
} else {
|
||||||
this.masterKey = passwordBytes ?: keyFileBytes ?: byteArrayOf(0)
|
this.masterKey = passwordBytes ?: keyFileBytes ?: byteArrayOf(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build check key
|
||||||
|
this.checkKey = masterCredential.getCheckKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createGroup(): GroupKDB {
|
override fun createGroup(): GroupKDB {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
import android.util.Base64
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.encrypt.HashManager
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
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.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
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.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
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.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
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.security.MemoryProtectionConfig
|
||||||
import com.kunzisoft.keepass.database.element.template.Template
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
|
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
|
||||||
@@ -51,7 +59,8 @@ import java.io.IOException
|
|||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.util.*
|
import java.util.Arrays
|
||||||
|
import java.util.UUID
|
||||||
import javax.crypto.Mac
|
import javax.crypto.Mac
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -253,6 +262,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
keyFileBytes,
|
keyFileBytes,
|
||||||
hardwareKeyBytes
|
hardwareKeyBytes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Build check key
|
||||||
|
this.checkKey = masterCredential.getCheckKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
@@ -635,7 +647,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
messageDigest = MessageDigest.getInstance("SHA-512")
|
messageDigest = MessageDigest.getInstance("SHA-512")
|
||||||
cmpKey[64] = 1
|
cmpKey[64] = 1
|
||||||
hmacKey = messageDigest.digest(cmpKey)
|
hmacKey = messageDigest.digest(cmpKey)
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
} catch (_: NoSuchAlgorithmException) {
|
||||||
throw IOException("No SHA-512 implementation")
|
throw IOException("No SHA-512 implementation")
|
||||||
} finally {
|
} finally {
|
||||||
Arrays.fill(cmpKey, 0.toByte())
|
Arrays.fill(cmpKey, 0.toByte())
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.UnsupportedEncodingException
|
import java.io.UnsupportedEncodingException
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
abstract class DatabaseVersioned<
|
abstract class DatabaseVersioned<
|
||||||
GroupId,
|
GroupId,
|
||||||
@@ -58,6 +58,8 @@ abstract class DatabaseVersioned<
|
|||||||
protected set
|
protected set
|
||||||
var transformSeed: ByteArray? = null
|
var transformSeed: ByteArray? = null
|
||||||
|
|
||||||
|
var checkKey = ByteArray(32)
|
||||||
|
|
||||||
abstract val version: String
|
abstract val version: String
|
||||||
abstract val defaultFileExtension: String
|
abstract val defaultFileExtension: String
|
||||||
|
|
||||||
@@ -89,10 +91,6 @@ abstract class DatabaseVersioned<
|
|||||||
return getGroupIndexes().filter { it != rootGroup }
|
return getGroupIndexes().filter { it != rootGroup }
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun isValidCredential(password: String?, containsKeyFile: Boolean): Boolean {
|
open fun isValidCredential(password: String?, containsKeyFile: Boolean): Boolean {
|
||||||
if (password == null && !containsKeyFile)
|
if (password == null && !containsKeyFile)
|
||||||
return false
|
return false
|
||||||
@@ -105,14 +103,14 @@ abstract class DatabaseVersioned<
|
|||||||
val bKey: ByteArray
|
val bKey: ByteArray
|
||||||
try {
|
try {
|
||||||
bKey = password.toByteArray(encoding)
|
bKey = password.toByteArray(encoding)
|
||||||
} catch (e: UnsupportedEncodingException) {
|
} catch (_: UnsupportedEncodingException) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val reEncoded: String
|
val reEncoded: String
|
||||||
try {
|
try {
|
||||||
reEncoded = String(bKey, encoding)
|
reEncoded = String(bKey, encoding)
|
||||||
} catch (e: UnsupportedEncodingException) {
|
} catch (_: UnsupportedEncodingException) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return password == reEncoded
|
return password == reEncoded
|
||||||
@@ -121,6 +119,7 @@ abstract class DatabaseVersioned<
|
|||||||
fun copyMasterKeyFrom(databaseVersioned: DatabaseVersioned<GroupId, EntryId, Group, Entry>) {
|
fun copyMasterKeyFrom(databaseVersioned: DatabaseVersioned<GroupId, EntryId, Group, Entry>) {
|
||||||
this.masterKey = databaseVersioned.masterKey
|
this.masterKey = databaseVersioned.masterKey
|
||||||
this.transformSeed = databaseVersioned.transformSeed
|
this.transformSeed = databaseVersioned.transformSeed
|
||||||
|
this.checkKey = databaseVersioned.checkKey
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
Reference in New Issue
Block a user