fix: Passwordless for multiple CredentialIds

This commit is contained in:
J-Jamet
2025-11-25 13:06:19 +01:00
parent 82a8776911
commit ed095ad0a7
8 changed files with 43 additions and 37 deletions

View File

@@ -950,14 +950,14 @@ class GroupActivity : DatabaseLockActivity(),
} }
private fun errorIfNeededForPasskeySelection(searchInfo: SearchInfo?) { private fun errorIfNeededForPasskeySelection(searchInfo: SearchInfo?) {
if (mTypeMode == TypeMode.PASSKEY && searchInfo?.credentialId != null) { if (mTypeMode == TypeMode.PASSKEY && searchInfo?.credentialIds.isNullOrEmpty().not()) {
removeSearch() removeSearch()
// Build response with the entry selected // Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
buildPasskeyErrorAndSetResult( buildPasskeyErrorAndSetResult(
resources = resources, resources = resources,
relyingPartyId = searchInfo.relyingParty, relyingPartyId = searchInfo.relyingParty,
credentialId = searchInfo.credentialId credentialIds = searchInfo.credentialIds
) )
} }
onValidateSpecialMode() onValidateSpecialMode()

View File

@@ -91,12 +91,13 @@ class PasskeyProviderService : CredentialProviderService() {
super.onDestroy() super.onDestroy()
} }
private fun buildPasskeySearchInfo(relyingParty: String, credentialId: String? = null): SearchInfo { private fun buildPasskeySearchInfo(
relyingParty: String,
credentialIds: List<String> = listOf()
): SearchInfo {
return SearchInfo().apply { return SearchInfo().apply {
credentialId?.let {
this.credentialId = it
}
this.relyingParty = relyingParty this.relyingParty = relyingParty
this.credentialIds = credentialIds
} }
} }
@@ -145,9 +146,10 @@ class PasskeyProviderService : CredentialProviderService() {
val publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions(option.requestJson) val publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions(option.requestJson)
val relyingPartyId = publicKeyCredentialRequestOptions.rpId val relyingPartyId = publicKeyCredentialRequestOptions.rpId
val credentialId = publicKeyCredentialRequestOptions.allowCredentials.firstOrNull()?.id?.let { b64Encode(it) } val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials
val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialId) .map { b64Encode(it.id) }
Log.d(TAG, "Build passkey search for relying party $relyingPartyId, credentialId $credentialId") val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList)
Log.d(TAG, "Build passkey search for relying party $relyingPartyId, credentialIds $credentialIdList")
SearchHelper.checkAutoSearchInfo( SearchHelper.checkAutoSearchInfo(
context = this, context = this,
database = mDatabase, database = mDatabase,
@@ -181,7 +183,7 @@ class PasskeyProviderService : CredentialProviderService() {
}, },
onItemNotFound = { _ -> onItemNotFound = { _ ->
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId") 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") Log.d(TAG, "Add pending intent for passkey selection in opened database")
PasskeyLauncherActivity.getPendingIntent( PasskeyLauncherActivity.getPendingIntent(
context = applicationContext, context = applicationContext,
@@ -207,7 +209,7 @@ class PasskeyProviderService : CredentialProviderService() {
getString( getString(
R.string.error_passkey_credential_id, R.string.error_passkey_credential_id,
relyingPartyId, relyingPartyId,
credentialId credentialIdList
) )
) )
} }

View File

@@ -207,12 +207,12 @@ object PasskeyHelper {
fun Activity.buildPasskeyErrorAndSetResult( fun Activity.buildPasskeyErrorAndSetResult(
resources: Resources, resources: Resources,
relyingPartyId: String?, relyingPartyId: String?,
credentialId: String? credentialIds: List<String>
) { ) {
val error = resources.getString( val error = resources.getString(
R.string.error_passkey_credential_id, R.string.error_passkey_credential_id,
relyingPartyId, relyingPartyId,
credentialId credentialIds
) )
Log.e(javaClass.name, error) Log.e(javaClass.name, error)
Toast.makeText( Toast.makeText(

View File

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

View File

@@ -775,5 +775,5 @@
<string name="passkey_backup_eligibility">Passkey Backup Eligibility</string> <string name="passkey_backup_eligibility">Passkey Backup Eligibility</string>
<string name="passkey_backup_state">Passkey Backup State</string> <string name="passkey_backup_state">Passkey Backup State</string>
<string name="error_passkey_result">Unable to return the passkey</string> <string name="error_passkey_result">Unable to return the passkey</string>
<string name="error_passkey_credential_id">No passkey found with relying party %1$s and credentialId %2$s</string> <string name="error_passkey_credential_id">No passkey found with relying party %1$s and credentialIds %2$s</string>
</resources> </resources>

View File

@@ -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.isAppId
import com.kunzisoft.keepass.model.AppOriginEntryField.isAppIdSignature import com.kunzisoft.keepass.model.AppOriginEntryField.isAppIdSignature
import com.kunzisoft.keepass.model.AppOriginEntryField.isWebDomain 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.isPasskey
import com.kunzisoft.keepass.model.PasskeyEntryFields.isRelyingParty 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.isOTP
import com.kunzisoft.keepass.otp.OtpEntryFields.isOTPURIField import com.kunzisoft.keepass.otp.OtpEntryFields.isOTPURIField
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
@@ -178,7 +178,7 @@ class SearchHelper {
} }
if (searchParameters.searchInRelyingParty) { if (searchParameters.searchInRelyingParty) {
val relyingParty = searchParameters.searchQuery val relyingParty = searchParameters.searchQuery
val credentialId = searchParameters.searchOption val credentialIds = searchParameters.searchOptions
val containsRelyingParty = entry.getExtraFields().any { field -> val containsRelyingParty = entry.getExtraFields().any { field ->
field.isRelyingParty() field.isRelyingParty()
&& checkSearchQuery( && 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 -> else entry.getExtraFields().any { field ->
field.isCredentialId() field.isCredentialId()
&& checkSearchQuery( && credentialIds.any { credentialId ->
stringToCheck = field.protectedValue.stringValue, checkSearchQuery(
searchParameters = SearchParameters().apply { stringToCheck = field.protectedValue.stringValue,
searchQuery = credentialId searchParameters = SearchParameters().apply {
caseSensitive = false searchQuery = credentialId
isRegex = false caseSensitive = false
} isRegex = false
) }
)
}
} }
return containsRelyingParty && containsCredentialId return containsRelyingParty && containsCredentialId
} }

View File

@@ -28,7 +28,7 @@ import android.os.Parcelable
class SearchParameters() : Parcelable{ class SearchParameters() : Parcelable{
var searchQuery: String = "" var searchQuery: String = ""
// Add an optional string to search with the main search query // Add an optional string to search with the main search query
var searchOption: String? = null var searchOptions: List<String> = listOf()
var allowEmptyQuery = true var allowEmptyQuery = true
var caseSensitive = false var caseSensitive = false
var isRegex = false var isRegex = false

View File

@@ -35,7 +35,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
return if (webDomain == null) null else field return if (webDomain == null) null else field
} }
var relyingParty: String? = null var relyingParty: String? = null
var credentialId: String? = null var credentialIds: List<String> = listOf()
var otpString: String? = null var otpString: String? = null
constructor() constructor()
@@ -47,7 +47,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
webDomain = toCopy?.webDomain webDomain = toCopy?.webDomain
webScheme = toCopy?.webScheme webScheme = toCopy?.webScheme
relyingParty = toCopy?.relyingParty relyingParty = toCopy?.relyingParty
credentialId = toCopy?.credentialId credentialIds = toCopy?.credentialIds ?: listOf()
otpString = toCopy?.otpString otpString = toCopy?.otpString
} }
@@ -63,8 +63,9 @@ class SearchInfo : ObjectNameResource, Parcelable {
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
val readRelyingParty = parcel.readString() val readRelyingParty = parcel.readString()
relyingParty = if (readRelyingParty.isNullOrEmpty()) null else readRelyingParty relyingParty = if (readRelyingParty.isNullOrEmpty()) null else readRelyingParty
val readCredentialId = parcel.readString() val readCredentialIdList = mutableListOf<String>()
credentialId = if (readCredentialId.isNullOrEmpty()) null else readCredentialId parcel.readStringList(readCredentialIdList)
credentialIds = readCredentialIdList.toList()
val readOtp = parcel.readString() val readOtp = parcel.readString()
otpString = if (readOtp.isNullOrEmpty()) null else readOtp otpString = if (readOtp.isNullOrEmpty()) null else readOtp
} }
@@ -80,7 +81,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
parcel.writeString(webDomain ?: "") parcel.writeString(webDomain ?: "")
parcel.writeString(webScheme ?: "") parcel.writeString(webScheme ?: "")
parcel.writeString(relyingParty ?: "") parcel.writeString(relyingParty ?: "")
parcel.writeString(credentialId ?: "") parcel.writeStringList(credentialIds)
parcel.writeString(otpString ?: "") parcel.writeString(otpString ?: "")
} }
@@ -99,7 +100,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
&& webDomain == null && webDomain == null
&& webScheme == null && webScheme == null
&& relyingParty == null && relyingParty == null
&& credentialId == null && credentialIds.isEmpty()
&& otpString == null && otpString == null
} }
@@ -133,7 +134,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
if (webDomain != other.webDomain) return false if (webDomain != other.webDomain) return false
if (webScheme != other.webScheme) return false if (webScheme != other.webScheme) return false
if (relyingParty != other.relyingParty) 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 if (otpString != other.otpString) return false
return true return true
@@ -146,7 +147,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
result = 31 * result + (webDomain?.hashCode() ?: 0) result = 31 * result + (webDomain?.hashCode() ?: 0)
result = 31 * result + (webScheme?.hashCode() ?: 0) result = 31 * result + (webScheme?.hashCode() ?: 0)
result = 31 * result + (relyingParty?.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) result = 31 * result + (otpString?.hashCode() ?: 0)
return result return result
} }
@@ -155,8 +156,8 @@ class SearchInfo : ObjectNameResource, Parcelable {
return otpString ?: webDomain ?: applicationId ?: relyingParty ?: tag ?: "" return otpString ?: webDomain ?: applicationId ?: relyingParty ?: tag ?: ""
} }
fun optionString(): String? { fun optionsString(): List<String> {
return if (isPasskeySearch && credentialId != null) credentialId else null return if (isPasskeySearch && credentialIds.isNotEmpty()) credentialIds else listOf()
} }
fun toRegisterInfo(): RegisterInfo { fun toRegisterInfo(): RegisterInfo {