From ed095ad0a752f3508a12aea71784e090a0b1fd9a Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 25 Nov 2025 13:06:19 +0100 Subject: [PATCH] fix: Passwordless for multiple CredentialIds --- .../keepass/activities/GroupActivity.kt | 4 +-- .../passkey/PasskeyProviderService.kt | 20 ++++++++------- .../passkey/util/PasskeyHelper.kt | 4 +-- .../keepass/database/helper/SearchHelper.kt | 2 +- app/src/main/res/values/strings.xml | 2 +- .../keepass/database/search/SearchHelper.kt | 25 +++++++++++-------- .../database/search/SearchParameters.kt | 2 +- .../com/kunzisoft/keepass/model/SearchInfo.kt | 21 ++++++++-------- 8 files changed, 43 insertions(+), 37 deletions(-) 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 1c3b30e32..2d90b50f1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -950,14 +950,14 @@ class GroupActivity : DatabaseLockActivity(), } private fun errorIfNeededForPasskeySelection(searchInfo: SearchInfo?) { - if (mTypeMode == TypeMode.PASSKEY && searchInfo?.credentialId != null) { + if (mTypeMode == TypeMode.PASSKEY && searchInfo?.credentialIds.isNullOrEmpty().not()) { removeSearch() // Build response with the entry selected if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { buildPasskeyErrorAndSetResult( resources = resources, relyingPartyId = searchInfo.relyingParty, - credentialId = searchInfo.credentialId + credentialIds = searchInfo.credentialIds ) } onValidateSpecialMode() 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 64ba48c75..2f1d11fc9 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 @@ -91,12 +91,13 @@ class PasskeyProviderService : CredentialProviderService() { super.onDestroy() } - private fun buildPasskeySearchInfo(relyingParty: String, credentialId: String? = null): SearchInfo { + private fun buildPasskeySearchInfo( + relyingParty: String, + credentialIds: List = listOf() + ): SearchInfo { return SearchInfo().apply { - credentialId?.let { - this.credentialId = it - } this.relyingParty = relyingParty + this.credentialIds = credentialIds } } @@ -145,9 +146,10 @@ class PasskeyProviderService : CredentialProviderService() { val publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions(option.requestJson) val relyingPartyId = publicKeyCredentialRequestOptions.rpId - val credentialId = publicKeyCredentialRequestOptions.allowCredentials.firstOrNull()?.id?.let { b64Encode(it) } - val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialId) - Log.d(TAG, "Build passkey search for relying party $relyingPartyId, credentialId $credentialId") + val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials + .map { b64Encode(it.id) } + val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList) + Log.d(TAG, "Build passkey search for relying party $relyingPartyId, credentialIds $credentialIdList") SearchHelper.checkAutoSearchInfo( context = this, database = mDatabase, @@ -181,7 +183,7 @@ class PasskeyProviderService : CredentialProviderService() { }, onItemNotFound = { _ -> Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId") - if (credentialId == null) { + if (credentialIdList.isEmpty()) { Log.d(TAG, "Add pending intent for passkey selection in opened database") PasskeyLauncherActivity.getPendingIntent( context = applicationContext, @@ -207,7 +209,7 @@ class PasskeyProviderService : CredentialProviderService() { getString( R.string.error_passkey_credential_id, relyingPartyId, - credentialId + credentialIdList ) ) } 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 3d2081cbf..0598d40e5 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 @@ -207,12 +207,12 @@ object PasskeyHelper { fun Activity.buildPasskeyErrorAndSetResult( resources: Resources, relyingPartyId: String?, - credentialId: String? + credentialIds: List ) { val error = resources.getString( R.string.error_passkey_credential_id, relyingPartyId, - credentialId + credentialIds ) Log.e(javaClass.name, error) Toast.makeText( diff --git a/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt b/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt index be1b288ac..ba727dc0b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt @@ -94,7 +94,7 @@ object SearchHelper { callback.invoke( SearchParameters().apply { searchQuery = query - searchOption = optionString() + searchOptions = optionsString() allowEmptyQuery = false searchInTitles = false searchInUsernames = false diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e802030f6..c13825b11 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -775,5 +775,5 @@ Passkey Backup Eligibility Passkey Backup State Unable to return the passkey - No passkey found with relying party %1$s and credentialId %2$s + No passkey found with relying party %1$s and credentialIds %2$s \ No newline at end of file diff --git a/database/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt b/database/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt index 07c9a10ab..f2763c897 100644 --- a/database/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt +++ b/database/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt @@ -27,9 +27,9 @@ import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.model.AppOriginEntryField.isAppId import com.kunzisoft.keepass.model.AppOriginEntryField.isAppIdSignature import com.kunzisoft.keepass.model.AppOriginEntryField.isWebDomain +import com.kunzisoft.keepass.model.PasskeyEntryFields.isCredentialId import com.kunzisoft.keepass.model.PasskeyEntryFields.isPasskey import com.kunzisoft.keepass.model.PasskeyEntryFields.isRelyingParty -import com.kunzisoft.keepass.model.PasskeyEntryFields.isCredentialId import com.kunzisoft.keepass.otp.OtpEntryFields.isOTP import com.kunzisoft.keepass.otp.OtpEntryFields.isOTPURIField import com.kunzisoft.keepass.utils.UUIDUtils.asHexString @@ -178,7 +178,7 @@ class SearchHelper { } if (searchParameters.searchInRelyingParty) { val relyingParty = searchParameters.searchQuery - val credentialId = searchParameters.searchOption + val credentialIds = searchParameters.searchOptions val containsRelyingParty = entry.getExtraFields().any { field -> field.isRelyingParty() && checkSearchQuery( @@ -191,17 +191,20 @@ class SearchHelper { } ) } - val containsCredentialId = if(credentialId == null) true + // Check empty to allow any credential if not defined + val containsCredentialId = if(credentialIds.isEmpty()) true else entry.getExtraFields().any { field -> field.isCredentialId() - && checkSearchQuery( - stringToCheck = field.protectedValue.stringValue, - searchParameters = SearchParameters().apply { - searchQuery = credentialId - caseSensitive = false - isRegex = false - } - ) + && credentialIds.any { credentialId -> + checkSearchQuery( + stringToCheck = field.protectedValue.stringValue, + searchParameters = SearchParameters().apply { + searchQuery = credentialId + caseSensitive = false + isRegex = false + } + ) + } } return containsRelyingParty && containsCredentialId } diff --git a/database/src/main/java/com/kunzisoft/keepass/database/search/SearchParameters.kt b/database/src/main/java/com/kunzisoft/keepass/database/search/SearchParameters.kt index 6516bdbf6..5b152e2f3 100644 --- a/database/src/main/java/com/kunzisoft/keepass/database/search/SearchParameters.kt +++ b/database/src/main/java/com/kunzisoft/keepass/database/search/SearchParameters.kt @@ -28,7 +28,7 @@ import android.os.Parcelable class SearchParameters() : Parcelable{ var searchQuery: String = "" // Add an optional string to search with the main search query - var searchOption: String? = null + var searchOptions: List = listOf() var allowEmptyQuery = true var caseSensitive = false var isRegex = false diff --git a/database/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt b/database/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt index b73a456d2..b6fb9e607 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt @@ -35,7 +35,7 @@ class SearchInfo : ObjectNameResource, Parcelable { return if (webDomain == null) null else field } var relyingParty: String? = null - var credentialId: String? = null + var credentialIds: List = listOf() var otpString: String? = null constructor() @@ -47,7 +47,7 @@ class SearchInfo : ObjectNameResource, Parcelable { webDomain = toCopy?.webDomain webScheme = toCopy?.webScheme relyingParty = toCopy?.relyingParty - credentialId = toCopy?.credentialId + credentialIds = toCopy?.credentialIds ?: listOf() otpString = toCopy?.otpString } @@ -63,8 +63,9 @@ class SearchInfo : ObjectNameResource, Parcelable { webScheme = if (readScheme.isNullOrEmpty()) null else readScheme val readRelyingParty = parcel.readString() relyingParty = if (readRelyingParty.isNullOrEmpty()) null else readRelyingParty - val readCredentialId = parcel.readString() - credentialId = if (readCredentialId.isNullOrEmpty()) null else readCredentialId + val readCredentialIdList = mutableListOf() + parcel.readStringList(readCredentialIdList) + credentialIds = readCredentialIdList.toList() val readOtp = parcel.readString() otpString = if (readOtp.isNullOrEmpty()) null else readOtp } @@ -80,7 +81,7 @@ class SearchInfo : ObjectNameResource, Parcelable { parcel.writeString(webDomain ?: "") parcel.writeString(webScheme ?: "") parcel.writeString(relyingParty ?: "") - parcel.writeString(credentialId ?: "") + parcel.writeStringList(credentialIds) parcel.writeString(otpString ?: "") } @@ -99,7 +100,7 @@ class SearchInfo : ObjectNameResource, Parcelable { && webDomain == null && webScheme == null && relyingParty == null - && credentialId == null + && credentialIds.isEmpty() && otpString == null } @@ -133,7 +134,7 @@ class SearchInfo : ObjectNameResource, Parcelable { if (webDomain != other.webDomain) return false if (webScheme != other.webScheme) return false if (relyingParty != other.relyingParty) return false - if (credentialId != other.credentialId) return false + if (credentialIds != other.credentialIds) return false if (otpString != other.otpString) return false return true @@ -146,7 +147,7 @@ class SearchInfo : ObjectNameResource, Parcelable { result = 31 * result + (webDomain?.hashCode() ?: 0) result = 31 * result + (webScheme?.hashCode() ?: 0) result = 31 * result + (relyingParty?.hashCode() ?: 0) - result = 31 * result + (credentialId?.hashCode() ?: 0) + result = 31 * result + (credentialIds.hashCode()) result = 31 * result + (otpString?.hashCode() ?: 0) return result } @@ -155,8 +156,8 @@ class SearchInfo : ObjectNameResource, Parcelable { return otpString ?: webDomain ?: applicationId ?: relyingParty ?: tag ?: "" } - fun optionString(): String? { - return if (isPasskeySearch && credentialId != null) credentialId else null + fun optionsString(): List { + return if (isPasskeySearch && credentialIds.isNotEmpty()) credentialIds else listOf() } fun toRegisterInfo(): RegisterInfo {