fix: Select passkey in passwordless mode #2282

This commit is contained in:
J-Jamet
2025-11-24 15:25:16 +01:00
parent 3b793a72b8
commit bc854c63f7
8 changed files with 91 additions and 14 deletions

View File

@@ -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<CredentialEntry>) -> Unit
) {
val passkeyEntries: MutableList<CredentialEntry> = 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<CreateEntry> = 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(

View File

@@ -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"))

View File

@@ -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<PublicKeyCredentialDescriptor>
val userVerification: String = json.optString("userVerification", "preferred")
init {
val allowCredentialsJson = json.getJSONArray("allowCredentials")
val allowCredentialsTmp: MutableList<PublicKeyCredentialDescriptor> = mutableListOf()
for (i in 0 until allowCredentialsJson.length()) {
val allowCredentialJson = allowCredentialsJson.getJSONObject(i)
val transports: MutableList<String> = 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()
}
}

View File

@@ -94,6 +94,7 @@ object SearchHelper {
callback.invoke(
SearchParameters().apply {
searchQuery = query
searchOption = optionString()
allowEmptyQuery = false
searchInTitles = false
searchInUsernames = false

View File

@@ -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))

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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)
}