diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index 7c5694c48..eeddbbabe 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -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( - userVerificationViewModel = mUserVerificationViewModel, - userVerificationCondition = mEntryViewModel.entryInfo - ?.isUserVerificationNeeded() == true, - dataToVerify = UserVerificationData(mDatabase, mEntryViewModel.mainEntryId) - ) + if (mEntryViewModel.entryInfo?.isUserVerificationNeeded() == true) { + checkUserVerification( + userVerificationViewModel = mUserVerificationViewModel, + dataToVerify = UserVerificationData(mDatabase, mEntryViewModel.mainEntryId) + ) + } else { + editEntry(mDatabase, mEntryViewModel.mainEntryId) + } return true } R.id.menu_restore_entry_history -> { 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 483a0f162..05caa6a95 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -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( - userVerificationViewModel = mUserVerificationViewModel, - userVerificationCondition = (node as Entry).getEntryInfo(database) - .isUserVerificationNeeded(), - dataToVerify = UserVerificationData(database,node.nodeId) - ) + if ((node as Entry).getEntryInfo(database).isUserVerificationNeeded()) { + checkUserVerification( + userVerificationViewModel = mUserVerificationViewModel, + dataToVerify = UserVerificationData(database, node.nodeId) + ) + } else { + editEntry(database, node.nodeId) + } } } return true diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationHelper.kt index a7062fe29..03ca1e77d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationHelper.kt @@ -75,9 +75,14 @@ class UserVerificationHelper { /** * Get the User Verification from the intent */ - fun Intent.getUserVerificationCondition(): Boolean { - return (getEnumExtra(EXTRA_USER_VERIFICATION) - ?: UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED + fun Intent.isUserVerificationNeeded(userVerificationPreferred: Boolean): Boolean { + val userVerification: UserVerificationRequirement = + getEnumExtra(EXTRA_USER_VERIFICATION) + ?: UserVerificationRequirement.PREFERRED + return (userVerification == UserVerificationRequirement.REQUIRED + || (userVerificationPreferred + && userVerification == UserVerificationRequirement.PREFERRED) + ) } /** @@ -87,67 +92,66 @@ 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, - 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() { - 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(dataToVerify) - } - - override fun onAuthenticationSucceeded( - result: BiometricPrompt.AuthenticationResult - ) { - super.onAuthenticationSucceeded(result) - userVerificationViewModel.onUserVerificationSucceeded(dataToVerify) - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - toastError(SecurityException(getString(R.string.device_unlock_not_recognized))) - userVerificationViewModel.onUserVerificationFailed(dataToVerify) - } - }).authenticate( - BiometricPrompt.PromptInfo.Builder() - .setTitle(getString(R.string.user_verification_required)) - .setAllowedAuthenticators(ALLOWED_AUTHENTICATORS) - .setConfirmationRequired(false) - .build() - ) - } + if (isAuthenticatorsAllowed()) { + showUserVerificationDeviceCredential(userVerificationViewModel, dataToVerify) } else { - userVerificationViewModel.onUserVerificationSucceeded(dataToVerify) + showUserVerificationMessage { + userVerificationViewModel.onUserVerificationFailed() + } } } + fun FragmentActivity.showUserVerificationDeviceCredential( + userVerificationViewModel: UserVerificationViewModel, + dataToVerify: UserVerificationData + ) { + 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(dataToVerify) + } + + override fun onAuthenticationSucceeded( + result: BiometricPrompt.AuthenticationResult + ) { + super.onAuthenticationSucceeded(result) + userVerificationViewModel.onUserVerificationSucceeded(dataToVerify) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + toastError(SecurityException(getString(R.string.device_unlock_not_recognized))) + userVerificationViewModel.onUserVerificationFailed(dataToVerify) + } + }).authenticate( + BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.user_verification_required)) + .setAllowedAuthenticators(ALLOWED_AUTHENTICATORS) + .setConfirmationRequired(false) + .build() + ) + } + fun FragmentActivity.showUserVerificationMessage( onActionPerformed: () -> Unit ) { 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 a701809b2..0ddd0d489 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 @@ -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 + ) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt index 5346fd013..8e0cce839 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt @@ -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( 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 7eb2f18fb..f54a117d0 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,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,15 +166,9 @@ 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) - } + // Launch with database when a nodeId is present + if ((database != null && database.loaded) || intent.retrieveNodeId() == null) { + super.launchActionIfNeeded(intent, specialMode, database) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt index 1495f6ee4..ea19872ea 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt @@ -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), diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 928a4fde3..0aa161c1d 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -139,6 +139,8 @@ passkeys_privileged_apps_key passkeys_auto_select_key true + passkeys_user_verification_preferred_key + false passkeys_backup_eligibility_key true passkeys_backup_state_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1bab61e52..090ecae1e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -434,6 +434,8 @@ Add app signature to passkey entry? Auto select Auto select if only one entry and the database is open, only if the requesting app is compatible + Preferred User Verification + Perform a user verification to access sensitive data when the relying party requests \"preferred\". Backup Eligibility Determine at creation time whether the public key credential source is allowed to be backed up Backup State diff --git a/app/src/main/res/xml/preferences_passkeys.xml b/app/src/main/res/xml/preferences_passkeys.xml index 2ae65c6f9..b586c3bf6 100644 --- a/app/src/main/res/xml/preferences_passkeys.xml +++ b/app/src/main/res/xml/preferences_passkeys.xml @@ -37,6 +37,11 @@ +