fix: Add main credential check method

This commit is contained in:
J-Jamet
2025-11-27 20:00:20 +01:00
parent d087fcc930
commit 844b1dfc79
14 changed files with 418 additions and 177 deletions

View File

@@ -157,7 +157,6 @@ class GroupActivity : DatabaseLockActivity(),
private val mGroupViewModel: GroupViewModel by viewModels() private val mGroupViewModel: GroupViewModel by viewModels()
private val mGroupEditViewModel: GroupEditViewModel by viewModels() private val mGroupEditViewModel: GroupEditViewModel by viewModels()
private val mMainCredentialViewModel: MainCredentialViewModel by viewModels() private val mMainCredentialViewModel: MainCredentialViewModel by viewModels()
private val mGroupActivityEducation = GroupActivityEducation(this) private val mGroupActivityEducation = GroupActivityEducation(this)
@@ -557,7 +556,7 @@ class GroupActivity : DatabaseLockActivity(),
mMainCredentialViewModel.uiState.collect { uiState -> mMainCredentialViewModel.uiState.collect { uiState ->
when (uiState) { when (uiState) {
is MainCredentialViewModel.UIState.Loading -> {} is MainCredentialViewModel.UIState.Loading -> {}
is MainCredentialViewModel.UIState.OnMainCredentialValidated -> { is MainCredentialViewModel.UIState.OnMainCredentialEntered -> {
mergeDatabaseFrom(uiState.databaseUri, uiState.mainCredential) mergeDatabaseFrom(uiState.databaseUri, uiState.mainCredential)
} }
is MainCredentialViewModel.UIState.OnMainCredentialCanceled -> { is MainCredentialViewModel.UIState.OnMainCredentialCanceled -> {

View File

@@ -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() { fun saveDatabase() {
mDatabaseViewModel.saveDatabase(save = true) mDatabaseViewModel.saveDatabase(save = true)
} }

View File

@@ -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<UserVerificationRequirement>(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
)
}
}
}
}
}

View File

@@ -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<UserVerificationRequirement>(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)
}
}
}
}

View File

@@ -43,9 +43,9 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.UserVerification.Companion.addUserVerification import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.addUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerification.Companion.askUserVerification import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerification.Companion.getUserVerifiedWithAuth import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.getUserVerifiedWithAuth
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin 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.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.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.view.toastError import com.kunzisoft.keepass.view.toastError
import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
@@ -68,6 +70,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels() private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels()
private val mainCredentialViewModel: MainCredentialViewModel by viewModels() private val mainCredentialViewModel: MainCredentialViewModel by viewModels()
private val userVerificationViewModel: UserVerificationViewModel by viewModels()
private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? = private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@@ -172,11 +175,28 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
mainCredentialViewModel.uiState.collect { uiState -> mainCredentialViewModel.uiState.collect { uiState ->
when (uiState) { when (uiState) {
is MainCredentialViewModel.UIState.Loading -> {} is MainCredentialViewModel.UIState.Loading -> {}
is MainCredentialViewModel.UIState.OnMainCredentialValidated -> { is MainCredentialViewModel.UIState.OnMainCredentialEntered -> {
// TODO Pass through UserVerification View Model checkMainCredential(uiState.mainCredential)
passkeyLauncherViewModel.launchAction(userVerified = true, intent, mSpecialMode)
} }
is MainCredentialViewModel.UIState.OnMainCredentialCanceled -> { 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() passkeyLauncherViewModel.cancelResult()
} }
} }
@@ -186,21 +206,11 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) { override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database) super.onUnknownDatabaseRetrieved(database)
// To manage https://github.com/Kunzisoft/KeePassDX/issues/2283 // To manage https://github.com/Kunzisoft/KeePassDX/issues/2283
database?.let { askUserVerification(
askUserVerification( database = database,
database = it, userVerificationViewModel = userVerificationViewModel
onVerificationSucceeded = { )
passkeyLauncherViewModel.launchAction(userVerified = true, intent, mSpecialMode)
},
onVerificationFailed = {
passkeyLauncherViewModel.cancelResult()
}
)
}
passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -214,6 +224,13 @@ 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(database)
} else {
userVerificationViewModel.onUserVerificationFailed(database)
}
}
} }
} }

View File

@@ -18,7 +18,7 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNod
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode 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.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
@@ -152,13 +152,14 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
} }
} }
fun launchAction( fun launchActionIfNeeded(
userVerified: Boolean, userVerified: Boolean,
intent: Intent, intent: Intent,
specialMode: SpecialMode, specialMode: SpecialMode,
database: ContextualDatabase?
) { ) {
this.mUserVerified = userVerified this.mUserVerified = userVerified
super.launchActionIfNeeded(intent, specialMode, mDatabase) super.launchActionIfNeeded(intent, specialMode, database)
} }
override fun launchActionIfNeeded( override fun launchActionIfNeeded(

View File

@@ -49,6 +49,7 @@ 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
@@ -319,13 +320,22 @@ class DatabaseTaskProvider(
databaseUri: Uri, databaseUri: Uri,
mainCredential: MainCredential mainCredential: MainCredential
) { ) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}, 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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.exception.DatabaseInputException
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.getUriInputStream
class CheckCredentialDatabaseRunnable(
private val context: Context,
private val mDatabase: ContextualDatabase,
private val mDatabaseUri: Uri,
private val mMainCredential: MainCredential,
private val mChallengeResponseRetriever: (hardwareKey: HardwareKey, seed: ByteArray?) -> ByteArray,
private val progressTaskUpdater: ProgressTaskUpdater?
) : ActionRunnable() {
var afterCheckCredential : ((Result) -> Unit)? = null
override fun onStartRun() {}
override fun onActionRun() {
try {
val contentResolver = context.contentResolver
mDatabase.fileUri = mDatabaseUri
mDatabase.checkMasterKey(
databaseStream = contentResolver.getUriInputStream(mDatabaseUri)
?: throw UnknownDatabaseLocationException(),
masterCredential = mMainCredential.toMasterCredential(contentResolver),
challengeResponseRetriever = mChallengeResponseRetriever,
progressTaskUpdater = progressTaskUpdater
)
} catch (e: DatabaseInputException) {
setError(e)
}
}
override fun onFinishRun() {
afterCheckCredential?.invoke(result)
}
}

View File

@@ -33,9 +33,11 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction 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.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
@@ -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.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -348,6 +349,7 @@ 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)
@@ -917,7 +919,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private fun buildDatabaseAssignCredentialActionTask( private fun buildDatabaseAssignCredentialActionTask(
intent: Intent, intent: Intent,
database: ContextualDatabase, database: ContextualDatabase
): ActionRunnable? { ): ActionRunnable? {
return if (intent.hasExtra(DATABASE_URI_KEY) return if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MAIN_CREDENTIAL_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) { private fun eraseCredentials(databaseUri: Uri) {
// Erase the biometric // Erase the biometric
CipherDatabaseAction.getInstance(this) 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_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"

View File

@@ -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) { fun saveDatabase(save: Boolean, saveToUri: Uri? = null) {
mDatabaseTaskProvider.startDatabaseSave(save, saveToUri) mDatabaseTaskProvider.startDatabaseSave(save, saveToUri)
} }

View File

@@ -19,7 +19,7 @@ class MainCredentialViewModel: ViewModel() {
databaseUri: Uri, databaseUri: Uri,
mainCredential: MainCredential mainCredential: MainCredential
) { ) {
mUiState.value = UIState.OnMainCredentialValidated(databaseUri, mainCredential) mUiState.value = UIState.OnMainCredentialEntered(databaseUri, mainCredential)
} }
fun cancelMainCredential( fun cancelMainCredential(
@@ -30,7 +30,7 @@ class MainCredentialViewModel: ViewModel() {
sealed class UIState { sealed class UIState {
object Loading: UIState() object Loading: UIState()
data class OnMainCredentialValidated( data class OnMainCredentialEntered(
val databaseUri: Uri, val databaseUri: Uri,
val mainCredential: MainCredential val mainCredential: MainCredential
): UIState() ): UIState()

View File

@@ -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>(UIState.Loading)
val uiState: StateFlow<UIState> = 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()
}
}

View File

@@ -42,6 +42,7 @@ 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
@@ -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 { fun isMergeDataAllowed(): Boolean {
return mDatabaseKDBX != null return mDatabaseKDBX != null
} }

View File

@@ -42,9 +42,11 @@ import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException import javax.xml.parsers.ParserConfigurationException
data class MasterCredential(var password: String? = null, data class MasterCredential(
var keyFileData: ByteArray? = null, var password: String? = null,
var hardwareKey: HardwareKey? = null): Parcelable { var keyFileData: ByteArray? = null,
var hardwareKey: HardwareKey? = null
): Parcelable {
constructor(parcel: Parcel) : this() { constructor(parcel: Parcel) : this() {
password = parcel.readString() password = parcel.readString()
@@ -94,8 +96,9 @@ data class MasterCredential(var password: String? = null,
private val TAG = MasterCredential::class.java.simpleName private val TAG = MasterCredential::class.java.simpleName
@Throws(IOException::class) @Throws(IOException::class)
fun retrievePasswordKey(key: String, fun retrievePasswordKey(
encoding: Charset key: String,
encoding: Charset
): ByteArray { ): ByteArray {
val bKey: ByteArray = try { val bKey: ByteArray = try {
key.toByteArray(encoding) key.toByteArray(encoding)