From bc854c63f7f260ea7fa81f932e07ae601c41546e Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Mon, 24 Nov 2025 15:25:16 +0100 Subject: [PATCH] fix: Select passkey in passwordless mode #2282 --- .../passkey/PasskeyProviderService.kt | 24 +++++++++----- .../PublicKeyCredentialCreationOptions.kt | 3 +- .../data/PublicKeyCredentialRequestOptions.kt | 25 ++++++++++++++- .../keepass/database/helper/SearchHelper.kt | 1 + .../keepass/database/search/SearchHelper.kt | 31 ++++++++++++++++--- .../database/search/SearchParameters.kt | 2 ++ .../keepass/model/PasskeyEntryFields.kt | 7 +++++ .../com/kunzisoft/keepass/model/SearchInfo.kt | 12 +++++++ 8 files changed, 91 insertions(+), 14 deletions(-) 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 37ddd7e28..86f4317b9 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 @@ -42,6 +42,7 @@ import androidx.credentials.provider.CredentialEntry import androidx.credentials.provider.CredentialProviderService import androidx.credentials.provider.ProviderClearCredentialStateRequest import androidx.credentials.provider.PublicKeyCredentialEntry +import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode import com.kunzisoft.keepass.R import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon import com.kunzisoft.keepass.credentialprovider.SpecialMode @@ -90,8 +91,11 @@ class PasskeyProviderService : CredentialProviderService() { super.onDestroy() } - private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo { + private fun buildPasskeySearchInfo(relyingParty: String, credentialId: String? = null): SearchInfo { return SearchInfo().apply { + credentialId?.let { + this.credentialId = it + } this.relyingParty = relyingParty } } @@ -108,6 +112,10 @@ class PasskeyProviderService : CredentialProviderService() { } } catch (e: Exception) { Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e) + when (e) { + is IOException -> toastError(e) + else -> {} + } callback.onError(GetCredentialUnknownException()) } } @@ -136,12 +144,13 @@ class PasskeyProviderService : CredentialProviderService() { option: BeginGetPublicKeyCredentialOption, callback: (List) -> Unit ) { - val passkeyEntries: MutableList = mutableListOf() - val relyingPartyId = PublicKeyCredentialRequestOptions(option.requestJson).rpId - val searchInfo = buildPasskeySearchInfo(relyingPartyId) - Log.d(TAG, "Build passkey search for relying party $relyingPartyId") + 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") SearchHelper.checkAutoSearchInfo( context = this, database = mDatabase, @@ -287,10 +296,11 @@ class PasskeyProviderService : CredentialProviderService() { getString(R.string.passkey_database_username) else databaseName val createEntries: MutableList = mutableListOf() - val relyingPartyId = PublicKeyCredentialCreationOptions( + val publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions( requestJson = request.requestJson, clientDataHash = request.clientDataHash - ).relyingPartyEntity.id + ) + val relyingPartyId = publicKeyCredentialCreationOptions.relyingPartyEntity.id val searchInfo = buildPasskeySearchInfo(relyingPartyId) Log.d(TAG, "Build passkey search for relying party $relyingPartyId") SearchHelper.checkAutoSearchInfo( diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt index b64634f63..75c19b626 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt @@ -42,11 +42,10 @@ class PublicKeyCredentialCreationOptions( val rpJson = json.getJSONObject("rp") relyingPartyEntity = PublicKeyCredentialRpEntity(rpJson.getString("name"), rpJson.getString("id")) val rpUser = json.getJSONObject("user") - val userId = Base64Helper.b64Decode(rpUser.getString("id")) userEntity = PublicKeyCredentialUserEntity( rpUser.getString("name"), - userId, + Base64Helper.b64Decode(rpUser.getString("id")), rpUser.getString("displayName") ) challenge = Base64Helper.b64Decode(json.getString("challenge")) diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt index 42b0d75de..99a31caea 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt @@ -23,9 +23,32 @@ import com.kunzisoft.encrypt.Base64Helper import org.json.JSONObject class PublicKeyCredentialRequestOptions(requestJson: String) { - val json: JSONObject = JSONObject(requestJson) + private val json: JSONObject = JSONObject(requestJson) val challenge: ByteArray = Base64Helper.b64Decode(json.getString("challenge")) val timeout: Long = json.optLong("timeout", 0) val rpId: String = json.optString("rpId", "") + val allowCredentials: List val userVerification: String = json.optString("userVerification", "preferred") + + init { + val allowCredentialsJson = json.getJSONArray("allowCredentials") + val allowCredentialsTmp: MutableList = mutableListOf() + for (i in 0 until allowCredentialsJson.length()) { + val allowCredentialJson = allowCredentialsJson.getJSONObject(i) + + val transports: MutableList = mutableListOf() + val transportsJson = allowCredentialJson.getJSONArray("transports") + for (j in 0 until transportsJson.length()) { + transports.add(transportsJson.getString(j)) + } + allowCredentialsTmp.add( + PublicKeyCredentialDescriptor( + type = allowCredentialJson.getString("type"), + id = Base64Helper.b64Decode(allowCredentialJson.getString("id")), + transports = transports + ) + ) + } + allowCredentials = allowCredentialsTmp.toList() + } } \ No newline at end of file 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 ade5ff023..be1b288ac 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,6 +94,7 @@ object SearchHelper { callback.invoke( SearchParameters().apply { searchQuery = query + searchOption = optionString() allowEmptyQuery = false searchInTitles = false searchInUsernames = false 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 bb2447924..07c9a10ab 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 @@ -29,6 +29,7 @@ import com.kunzisoft.keepass.model.AppOriginEntryField.isAppIdSignature import com.kunzisoft.keepass.model.AppOriginEntryField.isWebDomain 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 @@ -176,11 +177,33 @@ class SearchHelper { } } if (searchParameters.searchInRelyingParty) { - if(entry.getExtraFields().any { field -> + val relyingParty = searchParameters.searchQuery + val credentialId = searchParameters.searchOption + val containsRelyingParty = entry.getExtraFields().any { field -> field.isRelyingParty() - && checkSearchQuery(field.protectedValue.stringValue, searchParameters) - }) - return true + && checkSearchQuery( + stringToCheck = field.protectedValue.stringValue, + searchParameters = SearchParameters().apply { + searchQuery = relyingParty + searchInRelyingParty = true + caseSensitive = false + isRegex = false + } + ) + } + val containsCredentialId = if(credentialId == null) true + else entry.getExtraFields().any { field -> + field.isCredentialId() + && checkSearchQuery( + stringToCheck = field.protectedValue.stringValue, + searchParameters = SearchParameters().apply { + searchQuery = credentialId + caseSensitive = false + isRegex = false + } + ) + } + return containsRelyingParty && containsCredentialId } if (searchParameters.searchInNotes) { if (checkSearchQuery(entry.notes, searchParameters)) 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 ceacf27aa..6516bdbf6 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 @@ -27,6 +27,8 @@ 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 allowEmptyQuery = true var caseSensitive = false var isRegex = false diff --git a/database/src/main/java/com/kunzisoft/keepass/model/PasskeyEntryFields.kt b/database/src/main/java/com/kunzisoft/keepass/model/PasskeyEntryFields.kt index 016480619..831dfe05f 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/PasskeyEntryFields.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/PasskeyEntryFields.kt @@ -176,6 +176,13 @@ object PasskeyEntryFields { } } + /** + * Detect if the current field is a Passkey credential id + */ + fun Field.isCredentialId(): Boolean { + return name == FIELD_CREDENTIAL_ID + } + /** * Detect if the current field is a Passkey relying party */ 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 978a68235..b73a456d2 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt @@ -35,6 +35,7 @@ class SearchInfo : ObjectNameResource, Parcelable { return if (webDomain == null) null else field } var relyingParty: String? = null + var credentialId: String? = null var otpString: String? = null constructor() @@ -46,6 +47,7 @@ class SearchInfo : ObjectNameResource, Parcelable { webDomain = toCopy?.webDomain webScheme = toCopy?.webScheme relyingParty = toCopy?.relyingParty + credentialId = toCopy?.credentialId otpString = toCopy?.otpString } @@ -61,6 +63,8 @@ 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 readOtp = parcel.readString() otpString = if (readOtp.isNullOrEmpty()) null else readOtp } @@ -76,6 +80,7 @@ class SearchInfo : ObjectNameResource, Parcelable { parcel.writeString(webDomain ?: "") parcel.writeString(webScheme ?: "") parcel.writeString(relyingParty ?: "") + parcel.writeString(credentialId ?: "") parcel.writeString(otpString ?: "") } @@ -94,6 +99,7 @@ class SearchInfo : ObjectNameResource, Parcelable { && webDomain == null && webScheme == null && relyingParty == null + && credentialId == null && otpString == null } @@ -127,6 +133,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 (otpString != other.otpString) return false return true @@ -139,6 +146,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 + (otpString?.hashCode() ?: 0) return result } @@ -147,6 +155,10 @@ class SearchInfo : ObjectNameResource, Parcelable { return otpString ?: webDomain ?: applicationId ?: relyingParty ?: tag ?: "" } + fun optionString(): String? { + return if (isPasskeySearch && credentialId != null) credentialId else null + } + fun toRegisterInfo(): RegisterInfo { return RegisterInfo(this) }