From d90d175bd8f942146c559aa078e8ae6d8ac46d6d Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 25 Nov 2025 19:15:37 +0100 Subject: [PATCH] fix: User Verified response flag --- .../activity/PasskeyLauncherActivity.kt | 24 ++++++---- .../passkey/PasskeyProviderService.kt | 26 +++++----- .../passkey/util/PasskeyHelper.kt | 47 ++++++++++++++----- .../viewmodel/PasskeyLauncherViewModel.kt | 12 +++-- 4 files changed, 74 insertions(+), 35 deletions(-) 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 4cc44281f..c2881d2f8 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 @@ -46,13 +46,15 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivity import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp +import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.ALLOWED_AUTHENTICATORS import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode -import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addUserVerificationRequired +import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addUserVerification +import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getUserVerificationCondition +import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getUserVerifiedWithAuth import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isAuthenticatorsAllowed -import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isUserVerificationRequired -import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeUserVerificationRequired +import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeUserVerification import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel import com.kunzisoft.keepass.database.ContextualDatabase @@ -91,16 +93,17 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { override fun onCreate(savedInstanceState: Bundle?) { // To manage https://github.com/Kunzisoft/KeePassDX/issues/2283 - if (intent.isUserVerificationRequired()) { + val userVerificationCondition = intent.getUserVerificationCondition() + if (userVerificationCondition) { if (isAuthenticatorsAllowed().not()) { - intent.removeUserVerificationRequired() + intent.removeUserVerification() sendBroadcast(Intent(LOCK_ACTION)) } } // super.onCreate must be after UserVerification to allow database lock super.onCreate(savedInstanceState) // Biometric must be after super.onCreate - if (intent.isUserVerificationRequired()) { + if (userVerificationCondition) { if (isAuthenticatorsAllowed()) { BiometricPrompt( this, ContextCompat.getMainExecutor(this), @@ -127,7 +130,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { result: BiometricPrompt.AuthenticationResult ) { super.onAuthenticationSucceeded(result) - passkeyLauncherViewModel.launchAction(intent, mSpecialMode) + passkeyLauncherViewModel.launchAction(userVerified = true, intent, mSpecialMode) } override fun onAuthenticationFailed() { super.onAuthenticationFailed() @@ -146,7 +149,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { lifecycleScope.launch { // Initialize the parameters - passkeyLauncherViewModel.initialize() + passkeyLauncherViewModel.initialize(userVerified = intent.getUserVerifiedWithAuth()) // Retrieve the UI passkeyLauncherViewModel.uiState.collect { uiState -> when (uiState) { @@ -340,7 +343,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { searchInfo: SearchInfo? = null, appOrigin: AppOrigin? = null, nodeId: UUID? = null, - userVerificationRequired: Boolean = false + userVerification: UserVerificationRequirement = UserVerificationRequirement.PREFERRED, + userVerifiedWithAuth: Boolean = true ): PendingIntent? { return PendingIntent.getActivity( context, @@ -352,7 +356,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { addAppOrigin(appOrigin) addNodeId(nodeId) addAuthCode(nodeId) - addUserVerificationRequired(userVerificationRequired) + addUserVerification(userVerification, userVerifiedWithAuth) }, PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) 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 22cff0fbb..4dc9ffd99 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 @@ -157,9 +157,9 @@ class PasskeyProviderService : CredentialProviderService() { val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials .map { b64Encode(it.id) } val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList) - val userVerificationRequired = publicKeyCredentialRequestOptions - .userVerification == UserVerificationRequirement.REQUIRED - Log.d(TAG, "Build passkey search for UV $userVerificationRequired, " + + //val userVerification = publicKeyCredentialRequestOptions.userVerification + val userVerification = UserVerificationRequirement.REQUIRED + Log.d(TAG, "Build passkey search for UV $userVerification, " + "RP $relyingPartyId and Credential IDs $credentialIdList") SearchHelper.checkAutoSearchInfo( context = this, @@ -170,7 +170,7 @@ class PasskeyProviderService : CredentialProviderService() { passkeyEntries = passkeyEntries, searchInfo = searchInfo, option = option, - userVerificationRequired = userVerificationRequired + userVerification = userVerification ) { Log.d(TAG, "Add pending intent for passkey selection with found items") for (passkeyEntry in items) { @@ -179,7 +179,8 @@ class PasskeyProviderService : CredentialProviderService() { specialMode = SpecialMode.SELECTION, nodeId = passkeyEntry.id, appOrigin = passkeyEntry.appOrigin, - userVerificationRequired = userVerificationRequired + userVerification = userVerification, + userVerifiedWithAuth = false )?.let { usagePendingIntent -> val passkey = passkeyEntry.passkey passkeyEntries.add( @@ -208,7 +209,7 @@ class PasskeyProviderService : CredentialProviderService() { passkeyEntries = passkeyEntries, searchInfo = searchInfo, option = option, - userVerificationRequired = userVerificationRequired + userVerification = userVerification, ) { Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId") if (credentialIdList.isEmpty()) { @@ -217,7 +218,8 @@ class PasskeyProviderService : CredentialProviderService() { context = applicationContext, specialMode = SpecialMode.SELECTION, searchInfo = searchInfo, - userVerificationRequired = userVerificationRequired + userVerification = userVerification, + userVerifiedWithAuth = false )?.let { pendingIntent -> passkeyEntries.add( PublicKeyCredentialEntry( @@ -250,7 +252,8 @@ class PasskeyProviderService : CredentialProviderService() { PasskeyLauncherActivity.getPendingIntent( context = applicationContext, specialMode = SpecialMode.SELECTION, - searchInfo = searchInfo + searchInfo = searchInfo, + userVerifiedWithAuth = true )?.let { pendingIntent -> passkeyEntries.add( PublicKeyCredentialEntry( @@ -277,15 +280,16 @@ class PasskeyProviderService : CredentialProviderService() { passkeyEntries: MutableList, searchInfo: SearchInfo, option: BeginGetPublicKeyCredentialOption, - userVerificationRequired: Boolean, + userVerification: UserVerificationRequirement, standardAction: () -> Unit ) { - if (userVerificationRequired && isAuthenticatorsAllowed().not()) { + if (userVerification == UserVerificationRequirement.REQUIRED && isAuthenticatorsAllowed().not()) { PasskeyLauncherActivity.getPendingIntent( context = applicationContext, specialMode = SpecialMode.SELECTION, searchInfo = searchInfo, - userVerificationRequired = true + userVerification = userVerification, + userVerifiedWithAuth = true )?.let { pendingIntent -> passkeyEntries.add( PublicKeyCredentialEntry( diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt index b31d0bcb0..d3c322d7e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt @@ -60,6 +60,7 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredential import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters +import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.getOriginFromPrivilegedAllowLists import com.kunzisoft.keepass.model.AndroidOrigin import com.kunzisoft.keepass.model.AppOrigin @@ -67,7 +68,9 @@ import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.utils.AppUtil import com.kunzisoft.keepass.utils.StringUtil.toHexString +import com.kunzisoft.keepass.utils.getEnumExtra import com.kunzisoft.keepass.utils.getParcelableExtraCompat +import com.kunzisoft.keepass.utils.putEnumExtra import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.IOException @@ -95,7 +98,8 @@ object PasskeyHelper { private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin" private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp" private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode" - private const val EXTRA_UV_REQUIRED = "com.kunzisoft.keepass.extra.userVerification" + private const val EXTRA_USER_VERIFICATION = "com.kunzisoft.keepass.extra.userVerification" + private const val EXTRA_USER_VERIFIED_WITH_AUTH = "com.kunzisoft.keepass.extra.userVerifiedWithAuth" private const val SEPARATOR = "_" @@ -113,26 +117,46 @@ object PasskeyHelper { private val internalSecureRandom: SecureRandom = SecureRandom() /** - * Add the user verification to the intent + * Add the User Verification to the intent */ - fun Intent.addUserVerificationRequired(userVerification: Boolean) { - putExtra(EXTRA_UV_REQUIRED, userVerification) + fun Intent.addUserVerification( + userVerification: UserVerificationRequirement, + userVerifiedWithAuth: Boolean + ) { + putEnumExtra(EXTRA_USER_VERIFICATION, userVerification) + putExtra(EXTRA_USER_VERIFIED_WITH_AUTH, userVerifiedWithAuth) } /** - * Check if the user verification is required + * Get the User Verification from the intent */ - fun Intent.isUserVerificationRequired(): Boolean { - return getBooleanExtra(EXTRA_UV_REQUIRED, false) + fun Intent.getUserVerificationCondition(): Boolean { + return (getEnumExtra(EXTRA_USER_VERIFICATION) + ?: UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED } /** - * Remove the user verification from the intent + * Define if the User is verified with authentification from the intent */ - fun Intent.removeUserVerificationRequired() { - removeExtra(EXTRA_UV_REQUIRED) + 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) + } + + /** * Allowed authenticators for the User Verification */ @@ -622,6 +646,7 @@ object PasskeyHelper { requestOptions: PublicKeyCredentialRequestOptions, clientDataResponse: ClientDataResponse, passkey: Passkey, + userVerified: Boolean, defaultBackupEligibility: Boolean, defaultBackupState: Boolean ): PublicKeyCredential { @@ -630,7 +655,7 @@ object PasskeyHelper { response = AuthenticatorAssertionResponse( requestOptions = requestOptions, userPresent = true, - userVerified = true, + userVerified = userVerified, backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility, backupState = passkey.backupState ?: defaultBackupState, userHandle = passkey.userHandle, 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 fd8eee838..825823b90 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 @@ -24,8 +24,8 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredential import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity +import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getUserVerificationCondition import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedGETClientDataResponse -import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isUserVerificationRequired import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin @@ -65,14 +65,16 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView private var mPasskey: Passkey? = null private var mLockDatabaseAfterSelection: Boolean = false + private var mUserVerified: Boolean = true private var mBackupEligibility: Boolean = true private var mBackupState: Boolean = false private val mUiState = MutableStateFlow(UIState.Loading) val uiState: StateFlow = mUiState - fun initialize() { + fun initialize(userVerified: Boolean) { mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication()) + mUserVerified = userVerified mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication()) mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication()) } @@ -151,9 +153,11 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView } fun launchAction( + userVerified: Boolean, intent: Intent, specialMode: SpecialMode, ) { + this.mUserVerified = userVerified super.launchActionIfNeeded(intent, specialMode, mDatabase) } @@ -162,7 +166,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView specialMode: SpecialMode, database: ContextualDatabase? ) { - if (intent.isUserVerificationRequired()) { + if (intent.getUserVerificationCondition()) { if (database != null) { onDatabaseRetrieved(database) } @@ -321,6 +325,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView appOrigin = appOrigin ), passkey = passkey, + userVerified = mUserVerified, defaultBackupEligibility = mBackupEligibility, defaultBackupState = mBackupState ) @@ -377,6 +382,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView appOrigin = appOrigin ), passkey = passkey, + userVerified = mUserVerified, defaultBackupEligibility = mBackupEligibility, defaultBackupState = mBackupState )