fix: Behavior when ask device credential #2283

This commit is contained in:
J-Jamet
2025-12-01 13:20:06 +01:00
parent f6774b6d51
commit 9c6241afc9
10 changed files with 142 additions and 116 deletions

View File

@@ -57,7 +57,7 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TagsAdapter
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase
@@ -331,17 +331,7 @@ class EntryActivity : DatabaseLockActivity() {
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
uIState.dataToVerify.database?.let { database ->
uIState.dataToVerify.entryId?.let { entryId ->
EntryEditActivity.launch(
activity = this@EntryActivity,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
editEntry(uIState.dataToVerify.database, uIState.dataToVerify.entryId)
mUserVerificationViewModel.onUserVerificationReceived()
}
}
@@ -500,15 +490,31 @@ class EntryActivity : DatabaseLockActivity() {
}
}
private fun editEntry(database: ContextualDatabase?, entryId: NodeId<*>?) {
database?.let { database ->
entryId?.let { entryId ->
EntryEditActivity.launch(
activity = this@EntryActivity,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_edit -> {
askUserVerification(
if (mEntryViewModel.entryInfo?.isUserVerificationNeeded() == true) {
checkUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
userVerificationCondition = mEntryViewModel.entryInfo
?.isUserVerificationNeeded() == true,
dataToVerify = UserVerificationData(mDatabase, mEntryViewModel.mainEntryId)
)
} else {
editEntry(mDatabase, mEntryViewModel.mainEntryId)
}
return true
}
R.id.menu_restore_entry_history -> {

View File

@@ -76,7 +76,7 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSea
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
@@ -584,17 +584,7 @@ class GroupActivity : DatabaseLockActivity(),
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
uIState.dataToVerify.database?.let { database ->
uIState.dataToVerify.entryId?.let { entryId ->
EntryEditActivity.launch(
activity = this@GroupActivity,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
editEntry(uIState.dataToVerify.database, uIState.dataToVerify.entryId)
mUserVerificationViewModel.onUserVerificationReceived()
}
}
@@ -1047,6 +1037,20 @@ class GroupActivity : DatabaseLockActivity(),
).containsSearchInfo(searchInfo)
}
private fun editEntry(database: ContextualDatabase?, entryId: NodeId<*>?) {
database?.let {
entryId?.let {
EntryEditActivity.launch(
activity = this@GroupActivity,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
}
private fun finishNodeAction() {
actionNodeMode?.finish()
}
@@ -1096,12 +1100,14 @@ class GroupActivity : DatabaseLockActivity(),
launchDialogForGroupUpdate(node as Group)
}
Type.ENTRY -> {
askUserVerification(
if ((node as Entry).getEntryInfo(database).isUserVerificationNeeded()) {
checkUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
userVerificationCondition = (node as Entry).getEntryInfo(database)
.isUserVerificationNeeded(),
dataToVerify = UserVerificationData(database,node.nodeId)
dataToVerify = UserVerificationData(database, node.nodeId)
)
} else {
editEntry(database, node.nodeId)
}
}
}
return true

View File

@@ -75,9 +75,14 @@ class UserVerificationHelper {
/**
* Get the User Verification from the intent
*/
fun Intent.getUserVerificationCondition(): Boolean {
return (getEnumExtra<UserVerificationRequirement>(EXTRA_USER_VERIFICATION)
?: UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED
fun Intent.isUserVerificationNeeded(userVerificationPreferred: Boolean): Boolean {
val userVerification: UserVerificationRequirement =
getEnumExtra<UserVerificationRequirement>(EXTRA_USER_VERIFICATION)
?: UserVerificationRequirement.PREFERRED
return (userVerification == UserVerificationRequirement.REQUIRED
|| (userVerificationPreferred
&& userVerification == UserVerificationRequirement.PREFERRED)
)
}
/**
@@ -87,19 +92,23 @@ class UserVerificationHelper {
return this.passkey != null
}
/**
* Ask the user for verification
* Ask for the biometric if defined on the device
* Ask for the database credential otherwise
*/
fun FragmentActivity.askUserVerification(
fun FragmentActivity.checkUserVerification(
userVerificationViewModel: UserVerificationViewModel,
dataToVerify: UserVerificationData
) {
if (isAuthenticatorsAllowed()) {
showUserVerificationDeviceCredential(userVerificationViewModel, dataToVerify)
} else {
showUserVerificationMessage {
userVerificationViewModel.onUserVerificationFailed()
}
}
}
fun FragmentActivity.showUserVerificationDeviceCredential(
userVerificationViewModel: UserVerificationViewModel,
userVerificationCondition: Boolean,
dataToVerify: UserVerificationData
) {
if (isAuthenticatorsAllowed() && userVerificationCondition) {
// Important to check the nullable database here
dataToVerify.database?.let {
BiometricPrompt(
this, ContextCompat.getMainExecutor(this),
object : BiometricPrompt.AuthenticationCallback() {
@@ -115,7 +124,6 @@ class UserVerificationHelper {
// No operation
Log.i("UserVerification", "$errString")
}
else -> {
toastError(SecurityException("Authentication error: $errString"))
}
@@ -143,10 +151,6 @@ class UserVerificationHelper {
.build()
)
}
} else {
userVerificationViewModel.onUserVerificationSucceeded(dataToVerify)
}
}
fun FragmentActivity.showUserVerificationMessage(
onActionPerformed: () -> Unit

View File

@@ -47,11 +47,9 @@ import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.addUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.getUserVerificationCondition
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.getUserVerifiedWithAuth
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isAuthenticatorsAllowed
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.showUserVerificationMessage
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
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
@@ -63,6 +61,7 @@ import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CHECK_CREDENTIAL_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.settings.PreferencesUtil.isPasskeyUserVerificationPreferred
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.view.toastError
@@ -202,16 +201,20 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
// To manage https://github.com/Kunzisoft/KeePassDX/issues/2283
if (isAuthenticatorsAllowed()) {
askUserVerification(
val userVerificationNeeded = intent.isUserVerificationNeeded(
userVerificationPreferred = isPasskeyUserVerificationPreferred(this)
) && intent.getUserVerifiedWithAuth().not()
if (userVerificationNeeded) {
checkUserVerification(
userVerificationViewModel = userVerificationViewModel,
userVerificationCondition = intent.getUserVerificationCondition(),
dataToVerify = UserVerificationData(database)
)
} else {
showUserVerificationMessage {
userVerificationViewModel.onUserVerificationFailed()
}
passkeyLauncherViewModel.launchActionIfNeeded(
intent = intent,
specialMode = mSpecialMode,
database = database
)
}
}

View File

@@ -150,8 +150,7 @@ class PasskeyProviderService : CredentialProviderService() {
val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials
.map { b64Encode(it.id) }
val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList)
// TODO remove
val userVerification = UserVerificationRequirement.REQUIRED//publicKeyCredentialRequestOptions.userVerification
val userVerification = publicKeyCredentialRequestOptions.userVerification
Log.d(TAG, "Build passkey search for UV $userVerification, " +
"RP $relyingPartyId and Credential IDs $credentialIdList")
SearchHelper.checkAutoSearchInfo(

View File

@@ -18,7 +18,6 @@ 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.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
@@ -159,7 +158,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
database: ContextualDatabase?
) {
this.mUserVerified = userVerified
super.launchActionIfNeeded(intent, specialMode, database)
launchActionIfNeeded(intent, specialMode, database)
}
override fun launchActionIfNeeded(
@@ -167,17 +166,11 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
specialMode: SpecialMode,
database: ContextualDatabase?
) {
if (intent.getUserVerificationCondition()) {
if (database != null) {
onDatabaseRetrieved(database)
}
} else {
// Launch with database when a nodeId is present
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
super.launchActionIfNeeded(intent, specialMode, database)
}
}
}
override suspend fun launchAction(
intent: Intent,

View File

@@ -690,6 +690,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.passkeys_close_database_default))
}
fun isPasskeyUserVerificationPreferred(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.passkeys_user_verification_preferred_key),
context.resources.getBoolean(R.bool.passkeys_user_verification_preferred_default))
}
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),

View File

@@ -139,6 +139,8 @@
<string name="passkeys_privileged_apps_key" translatable="false">passkeys_privileged_apps_key</string>
<string name="passkeys_auto_select_key" translatable="false">passkeys_auto_select_key</string>
<bool name="passkeys_auto_select_default" translatable="false">true</bool>
<string name="passkeys_user_verification_preferred_key" translatable="false">passkeys_user_verification_preferred_key</string>
<bool name="passkeys_user_verification_preferred_default" translatable="false">false</bool>
<string name="passkeys_backup_eligibility_key" translatable="false">passkeys_backup_eligibility_key</string>
<bool name="passkeys_backup_eligibility_default" translatable="false">true</bool>
<string name="passkeys_backup_state_key" translatable="false">passkeys_backup_state_key</string>

View File

@@ -434,6 +434,8 @@
<string name="passkeys_missing_signature_app_ask_question">Add app signature to passkey entry?</string>
<string name="passkeys_auto_select_title">Auto select</string>
<string name="passkeys_auto_select_summary">Auto select if only one entry and the database is open, only if the requesting app is compatible</string>
<string name="passkeys_user_verification_preferred_title">Preferred User Verification</string>
<string name="passkeys_user_verification_preferred_summary">Perform a user verification to access sensitive data when the relying party requests \"preferred\".</string>
<string name="passkeys_backup_eligibility_title">Backup Eligibility</string>
<string name="passkeys_backup_eligibility_summary">Determine at creation time whether the public key credential source is allowed to be backed up</string>
<string name="passkeys_backup_state_title">Backup State</string>

View File

@@ -37,6 +37,11 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/database">
<SwitchPreferenceCompat
android:key="@string/passkeys_user_verification_preferred_key"
android:title="@string/passkeys_user_verification_preferred_title"
android:summary="@string/passkeys_user_verification_preferred_summary"
android:defaultValue="@bool/passkeys_user_verification_preferred_default"/>
<SwitchPreferenceCompat
android:key="@string/passkeys_backup_eligibility_key"
android:title="@string/passkeys_backup_eligibility_title"