From 844b1dfc799b5c9a3a1f5c8f24eee5befc2f2645 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 27 Nov 2025 20:00:20 +0100 Subject: [PATCH] fix: Add main credential check method --- .../keepass/activities/GroupActivity.kt | 3 +- .../activities/legacy/DatabaseLockActivity.kt | 8 + .../credentialprovider/UserVerification.kt | 142 ---------------- .../UserVerificationHelper.kt | 157 ++++++++++++++++++ .../activity/PasskeyLauncherActivity.kt | 57 ++++--- .../viewmodel/PasskeyLauncherViewModel.kt | 7 +- .../keepass/database/DatabaseTaskProvider.kt | 12 +- .../action/CheckCredentialDatabaseRunnable.kt | 65 ++++++++ .../DatabaseTaskNotificationService.kt | 38 ++++- .../keepass/viewmodels/DatabaseViewModel.kt | 7 + .../viewmodels/MainCredentialViewModel.kt | 4 +- .../viewmodels/UserVerificationViewModel.kt | 34 ++++ .../keepass/database/element/Database.kt | 48 ++++++ .../database/element/MasterCredential.kt | 13 +- 14 files changed, 418 insertions(+), 177 deletions(-) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerification.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationHelper.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/database/action/CheckCredentialDatabaseRunnable.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/viewmodels/UserVerificationViewModel.kt diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index 5adbdcd31..66b55253e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -157,7 +157,6 @@ class GroupActivity : DatabaseLockActivity(), private val mGroupViewModel: GroupViewModel by viewModels() private val mGroupEditViewModel: GroupEditViewModel by viewModels() - private val mMainCredentialViewModel: MainCredentialViewModel by viewModels() private val mGroupActivityEducation = GroupActivityEducation(this) @@ -557,7 +556,7 @@ class GroupActivity : DatabaseLockActivity(), mMainCredentialViewModel.uiState.collect { uiState -> when (uiState) { is MainCredentialViewModel.UIState.Loading -> {} - is MainCredentialViewModel.UIState.OnMainCredentialValidated -> { + is MainCredentialViewModel.UIState.OnMainCredentialEntered -> { mergeDatabaseFrom(uiState.databaseUri, uiState.mainCredential) } is MainCredentialViewModel.UIState.OnMainCredentialCanceled -> { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt index c8926abb2..7b29e1568 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt @@ -181,6 +181,14 @@ 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) } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerification.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerification.kt deleted file mode 100644 index 1461feef6..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerification.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.kunzisoft.keepass.credentialprovider - -import android.content.Context -import android.content.Intent -import android.util.Log -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK -import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL -import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS -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.MainCredentialDialogFragment -import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement -import com.kunzisoft.keepass.database.ContextualDatabase -import com.kunzisoft.keepass.utils.getEnumExtra -import com.kunzisoft.keepass.utils.putEnumExtra -import com.kunzisoft.keepass.view.toastError - -class UserVerification { - - companion object { - - private const val EXTRA_USER_VERIFICATION = "com.kunzisoft.keepass.extra.userVerification" - private const val EXTRA_USER_VERIFIED_WITH_AUTH = "com.kunzisoft.keepass.extra.userVerifiedWithAuth" - - /** - * Allowed authenticators for the User Verification - */ - const val ALLOWED_AUTHENTICATORS = BIOMETRIC_WEAK or DEVICE_CREDENTIAL - - /** - * Check if the device supports the biometric prompt for User Verification - */ - fun Context.isAuthenticatorsAllowed(): Boolean { - return BiometricManager.from(this) - .canAuthenticate(ALLOWED_AUTHENTICATORS) == BIOMETRIC_SUCCESS - } - - /** - * Add the User Verification to the intent - */ - fun Intent.addUserVerification( - userVerification: UserVerificationRequirement, - userVerifiedWithAuth: Boolean - ) { - putEnumExtra(EXTRA_USER_VERIFICATION, userVerification) - putExtra(EXTRA_USER_VERIFIED_WITH_AUTH, userVerifiedWithAuth) - } - - /** - * Define if the User is verified with authentification from the intent - */ - fun Intent.getUserVerifiedWithAuth(): Boolean { - return getBooleanExtra(EXTRA_USER_VERIFIED_WITH_AUTH, true) - } - - /** - * Remove the User Verification from the intent - */ - fun Intent.removeUserVerification() { - removeExtra(EXTRA_USER_VERIFICATION) - } - - /** - * Remove the User verified with auth from the intent - */ - fun Intent.removeUserVerifiedWithAuth() { - removeExtra(EXTRA_USER_VERIFIED_WITH_AUTH) - } - - /** - * Get the User Verification from the intent - */ - fun Intent.getUserVerificationCondition(): Boolean { - return (getEnumExtra(EXTRA_USER_VERIFICATION) - ?: UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED - } - - /** - * Ask the user for verification - * Ask for the biometric if defined on the device - * Ask for the database credential otherwise - */ - fun FragmentActivity.askUserVerification( - database: ContextualDatabase, - onVerificationSucceeded: () -> Unit, - onVerificationFailed: () -> Unit - ) { - if (this.intent.getUserVerificationCondition()) { - if (isAuthenticatorsAllowed()) { - BiometricPrompt( - this, ContextCompat.getMainExecutor(this), - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError( - errorCode: Int, - errString: CharSequence - ) { - super.onAuthenticationError(errorCode, errString) - when (errorCode) { - BiometricPrompt.ERROR_CANCELED, - BiometricPrompt.ERROR_NEGATIVE_BUTTON, - BiometricPrompt.ERROR_USER_CANCELED -> { - // No operation - Log.i("UserVerification", "$errString") - } - else -> { - toastError(SecurityException("Authentication error: $errString")) - } - } - onVerificationFailed() - } - override fun onAuthenticationSucceeded( - result: BiometricPrompt.AuthenticationResult - ) { - super.onAuthenticationSucceeded(result) - onVerificationSucceeded() - } - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - toastError(SecurityException(getString(R.string.device_unlock_not_recognized))) - onVerificationFailed() - } - }).authenticate( - BiometricPrompt.PromptInfo.Builder() - .setTitle(getString(R.string.user_verification_required)) - .setAllowedAuthenticators(ALLOWED_AUTHENTICATORS) - .setConfirmationRequired(false) - .build() - ) - } else { - MainCredentialDialogFragment.getInstance(database.fileUri) - .show( - supportFragmentManager, - MainCredentialDialogFragment.TAG_ASK_MAIN_CREDENTIAL - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationHelper.kt new file mode 100644 index 000000000..cb391a87e --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationHelper.kt @@ -0,0 +1,157 @@ +package com.kunzisoft.keepass.credentialprovider + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS +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.MainCredentialDialogFragment +import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment.Companion.TAG_ASK_MAIN_CREDENTIAL +import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement +import com.kunzisoft.keepass.database.ContextualDatabase +import com.kunzisoft.keepass.utils.getEnumExtra +import com.kunzisoft.keepass.utils.putEnumExtra +import com.kunzisoft.keepass.view.toastError +import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel + +class UserVerificationHelper { + + companion object { + + private const val EXTRA_USER_VERIFICATION = "com.kunzisoft.keepass.extra.userVerification" + private const val EXTRA_USER_VERIFIED_WITH_AUTH = "com.kunzisoft.keepass.extra.userVerifiedWithAuth" + + /** + * Allowed authenticators for the User Verification + */ + const val ALLOWED_AUTHENTICATORS = BIOMETRIC_WEAK or DEVICE_CREDENTIAL + + /** + * Check if the device supports the biometric prompt for User Verification + */ + fun Context.isAuthenticatorsAllowed(): Boolean { + return BiometricManager.from(this) + .canAuthenticate(ALLOWED_AUTHENTICATORS) == BIOMETRIC_SUCCESS + } + + /** + * Add the User Verification to the intent + */ + fun Intent.addUserVerification( + userVerification: UserVerificationRequirement, + userVerifiedWithAuth: Boolean + ) { + putEnumExtra(EXTRA_USER_VERIFICATION, userVerification) + putExtra(EXTRA_USER_VERIFIED_WITH_AUTH, userVerifiedWithAuth) + } + + /** + * Define if the User is verified with authentification from the intent + */ + fun Intent.getUserVerifiedWithAuth(): Boolean { + return getBooleanExtra(EXTRA_USER_VERIFIED_WITH_AUTH, true) + } + + /** + * Remove the User Verification from the intent + */ + fun Intent.removeUserVerification() { + removeExtra(EXTRA_USER_VERIFICATION) + } + + /** + * Remove the User verified with auth from the intent + */ + fun Intent.removeUserVerifiedWithAuth() { + removeExtra(EXTRA_USER_VERIFIED_WITH_AUTH) + } + + /** + * Get the User Verification from the intent + */ + fun Intent.getUserVerificationCondition(): Boolean { + return (getEnumExtra(EXTRA_USER_VERIFICATION) + ?: UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED + } + + /** + * Ask the user for verification + * Ask for the biometric if defined on the device + * Ask for the database credential otherwise + */ + fun FragmentActivity.askUserVerification( + database: ContextualDatabase?, + userVerificationViewModel: UserVerificationViewModel + ) { + if (this.intent.getUserVerificationCondition()) { + // Important to check the nullable database here + database?.let { + if (isAuthenticatorsAllowed()) { + BiometricPrompt( + this, ContextCompat.getMainExecutor(this), + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence + ) { + super.onAuthenticationError(errorCode, errString) + when (errorCode) { + BiometricPrompt.ERROR_CANCELED, + BiometricPrompt.ERROR_NEGATIVE_BUTTON, + BiometricPrompt.ERROR_USER_CANCELED -> { + // No operation + Log.i("UserVerification", "$errString") + } + + else -> { + toastError(SecurityException("Authentication error: $errString")) + } + } + userVerificationViewModel.onUserVerificationFailed(database) + } + + override fun onAuthenticationSucceeded( + result: BiometricPrompt.AuthenticationResult + ) { + super.onAuthenticationSucceeded(result) + userVerificationViewModel.onUserVerificationSucceeded(database) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + toastError(SecurityException(getString(R.string.device_unlock_not_recognized))) + userVerificationViewModel.onUserVerificationFailed(database) + } + }).authenticate( + BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.user_verification_required)) + .setAllowedAuthenticators(ALLOWED_AUTHENTICATORS) + .setConfirmationRequired(false) + .build() + ) + } else { + // TODO Check fragment + var mainCredentialDialogFragment = supportFragmentManager + .findFragmentByTag(TAG_ASK_MAIN_CREDENTIAL) as? MainCredentialDialogFragment? + if (mainCredentialDialogFragment == null) { + mainCredentialDialogFragment = MainCredentialDialogFragment + .getInstance(database.fileUri) + mainCredentialDialogFragment.show( + supportFragmentManager, + TAG_ASK_MAIN_CREDENTIAL + ) + } + } + } + } else { + userVerificationViewModel.onUserVerificationSucceeded(database) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt index e539f5a51..d179c1f45 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt @@ -43,9 +43,9 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.TypeMode -import com.kunzisoft.keepass.credentialprovider.UserVerification.Companion.addUserVerification -import com.kunzisoft.keepass.credentialprovider.UserVerification.Companion.askUserVerification -import com.kunzisoft.keepass.credentialprovider.UserVerification.Companion.getUserVerifiedWithAuth +import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.addUserVerification +import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification +import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.getUserVerifiedWithAuth import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin @@ -55,11 +55,13 @@ 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.tasks.ActionRunnable import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode import com.kunzisoft.keepass.view.toastError import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel +import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel import kotlinx.coroutines.launch import java.util.UUID @@ -68,6 +70,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels() private val mainCredentialViewModel: MainCredentialViewModel by viewModels() + private val userVerificationViewModel: UserVerificationViewModel by viewModels() private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher? = this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { @@ -172,11 +175,28 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { mainCredentialViewModel.uiState.collect { uiState -> when (uiState) { is MainCredentialViewModel.UIState.Loading -> {} - is MainCredentialViewModel.UIState.OnMainCredentialValidated -> { - // TODO Pass through UserVerification View Model - passkeyLauncherViewModel.launchAction(userVerified = true, intent, mSpecialMode) + is MainCredentialViewModel.UIState.OnMainCredentialEntered -> { + checkMainCredential(uiState.mainCredential) } is MainCredentialViewModel.UIState.OnMainCredentialCanceled -> { + userVerificationViewModel.onUserVerificationFailed() + } + } + } + } + lifecycleScope.launch { + userVerificationViewModel.uiState.collect { uiState -> + when (uiState) { + is UserVerificationViewModel.UIState.Loading -> {} + is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> { + passkeyLauncherViewModel.launchActionIfNeeded( + userVerified = true, + intent = intent, + specialMode = mSpecialMode, + database = uiState.database + ) + } + is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> { passkeyLauncherViewModel.cancelResult() } } @@ -186,21 +206,11 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) { super.onUnknownDatabaseRetrieved(database) - // To manage https://github.com/Kunzisoft/KeePassDX/issues/2283 - database?.let { - askUserVerification( - database = it, - onVerificationSucceeded = { - passkeyLauncherViewModel.launchAction(userVerified = true, intent, mSpecialMode) - }, - onVerificationFailed = { - passkeyLauncherViewModel.cancelResult() - } - ) - } - - passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database) + askUserVerification( + database = database, + userVerificationViewModel = userVerificationViewModel + ) } override fun onDatabaseActionFinished( @@ -214,6 +224,13 @@ 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(database) + } else { + userVerificationViewModel.onUserVerificationFailed(database) + } + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt index 86196970d..7eb2f18fb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt @@ -18,7 +18,7 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNod import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.TypeMode -import com.kunzisoft.keepass.credentialprovider.UserVerification.Companion.getUserVerificationCondition +import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.getUserVerificationCondition import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters @@ -152,13 +152,14 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView } } - fun launchAction( + fun launchActionIfNeeded( userVerified: Boolean, intent: Intent, specialMode: SpecialMode, + database: ContextualDatabase? ) { this.mUserVerified = userVerified - super.launchActionIfNeeded(intent, specialMode, mDatabase) + super.launchActionIfNeeded(intent, specialMode, database) } override fun launchActionIfNeeded( diff --git a/app/src/main/java/com/kunzisoft/keepass/database/DatabaseTaskProvider.kt b/app/src/main/java/com/kunzisoft/keepass/database/DatabaseTaskProvider.kt index f4e8414ff..7ea3239a2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/DatabaseTaskProvider.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/DatabaseTaskProvider.kt @@ -49,6 +49,7 @@ 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 @@ -319,13 +320,22 @@ class DatabaseTaskProvider( databaseUri: Uri, mainCredential: MainCredential ) { - start(Bundle().apply { putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) }, 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 diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/CheckCredentialDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/CheckCredentialDatabaseRunnable.kt new file mode 100644 index 000000000..f696307f2 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/CheckCredentialDatabaseRunnable.kt @@ -0,0 +1,65 @@ +/* + * 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 . + * + */ +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) + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt index 82e69845a..649b3f21f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -33,9 +33,11 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction +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 @@ -61,7 +63,6 @@ import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.hardware.HardwareKey -import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.settings.PreferencesUtil @@ -348,6 +349,7 @@ 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) @@ -917,7 +919,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress private fun buildDatabaseAssignCredentialActionTask( intent: Intent, - database: ContextualDatabase, + database: ContextualDatabase ): ActionRunnable? { return if (intent.hasExtra(DATABASE_URI_KEY) && intent.hasExtra(MAIN_CREDENTIAL_KEY) @@ -942,6 +944,37 @@ 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) @@ -1330,6 +1363,7 @@ 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" diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt index 3b9b5a97c..691340b60 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt @@ -133,6 +133,13 @@ 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) } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/MainCredentialViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/MainCredentialViewModel.kt index 5f8824f84..97f51afdb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/MainCredentialViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/MainCredentialViewModel.kt @@ -19,7 +19,7 @@ class MainCredentialViewModel: ViewModel() { databaseUri: Uri, mainCredential: MainCredential ) { - mUiState.value = UIState.OnMainCredentialValidated(databaseUri, mainCredential) + mUiState.value = UIState.OnMainCredentialEntered(databaseUri, mainCredential) } fun cancelMainCredential( @@ -30,7 +30,7 @@ class MainCredentialViewModel: ViewModel() { sealed class UIState { object Loading: UIState() - data class OnMainCredentialValidated( + data class OnMainCredentialEntered( val databaseUri: Uri, val mainCredential: MainCredential ): UIState() diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/UserVerificationViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/UserVerificationViewModel.kt new file mode 100644 index 000000000..3b3da6a00 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/UserVerificationViewModel.kt @@ -0,0 +1,34 @@ +package com.kunzisoft.keepass.viewmodels + +import androidx.lifecycle.ViewModel +import com.kunzisoft.keepass.database.ContextualDatabase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * ViewModel for the User Verification + */ +class UserVerificationViewModel: ViewModel() { + + private val mUiState = MutableStateFlow(UIState.Loading) + val uiState: StateFlow = mUiState + + fun onUserVerificationSucceeded(database: ContextualDatabase?) { + mUiState.value = UIState.OnUserVerificationSucceeded(database) + } + + fun onUserVerificationFailed(database: ContextualDatabase? = null) { + mUiState.value = UIState.OnUserVerificationCanceled(database) + } + + sealed class UIState { + object Loading: UIState() + data class OnUserVerificationSucceeded( + val database: ContextualDatabase? + ): UIState() + data class OnUserVerificationCanceled( + val database: ContextualDatabase? + ): UIState() + } + +} \ No newline at end of file diff --git a/database/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/database/src/main/java/com/kunzisoft/keepass/database/element/Database.kt index 23a75212c..ed003fdfe 100644 --- a/database/src/main/java/com/kunzisoft/keepass/database/element/Database.kt +++ b/database/src/main/java/com/kunzisoft/keepass/database/element/Database.kt @@ -42,6 +42,7 @@ 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 @@ -618,6 +619,53 @@ 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) + } + } + fun isMergeDataAllowed(): Boolean { return mDatabaseKDBX != null } diff --git a/database/src/main/java/com/kunzisoft/keepass/database/element/MasterCredential.kt b/database/src/main/java/com/kunzisoft/keepass/database/element/MasterCredential.kt index 8e3f395cf..aa8ca4b24 100644 --- a/database/src/main/java/com/kunzisoft/keepass/database/element/MasterCredential.kt +++ b/database/src/main/java/com/kunzisoft/keepass/database/element/MasterCredential.kt @@ -42,9 +42,11 @@ import javax.xml.XMLConstants import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.ParserConfigurationException -data class MasterCredential(var password: String? = null, - var keyFileData: ByteArray? = null, - var hardwareKey: HardwareKey? = null): Parcelable { +data class MasterCredential( + var password: String? = null, + var keyFileData: ByteArray? = null, + var hardwareKey: HardwareKey? = null +): Parcelable { constructor(parcel: Parcel) : this() { password = parcel.readString() @@ -94,8 +96,9 @@ data class MasterCredential(var password: String? = null, private val TAG = MasterCredential::class.java.simpleName @Throws(IOException::class) - fun retrievePasswordKey(key: String, - encoding: Charset + fun retrievePasswordKey( + key: String, + encoding: Charset ): ByteArray { val bKey: ByteArray = try { key.toByteArray(encoding)