1package com.peachpasswords.passkeys23import com.peachpasswords.vault.VaultEntry4import kotlinx.serialization.json.Json5import kotlinx.serialization.json.contentOrNull6import kotlinx.serialization.json.jsonArray7import kotlinx.serialization.json.jsonObject8import kotlinx.serialization.json.jsonPrimitive910data class PasskeyCredentialCandidate(11 val entryId: String,12 val title: String,13 val rpId: String,14 val credentialId: String,15 val username: String,16 val displayName: String,17 val lastUsedAt: Long?,18)1920object PasskeyCredentialPlanner {21 private val json = Json { ignoreUnknownKeys = true }2223 2425262728 fun candidates(29 entries: List<VaultEntry>,30 requestJson: String,31 callerOrigin: String?,32 ): List<PasskeyCredentialCandidate> {33 if (callerOrigin.isNullOrBlank()) return emptyList()34 val request = runCatching { json.parseToJsonElement(requestJson).jsonObject }.getOrNull() ?: return emptyList()35 val requestedRpId = request["rpId"]?.jsonPrimitive?.contentOrNull?.trim()?.lowercase() ?: return emptyList()36 val allowedIds = runCatching { request["allowCredentials"]?.jsonArray }.getOrNull()37 ?.mapNotNull { item ->38 runCatching { item.jsonObject["id"]?.jsonPrimitive?.contentOrNull }.getOrNull()39 }40 ?.filter { it.isNotBlank() }41 ?.toSet()4243 return entries.asSequence()44 .filter { it.deletedAt == null && it.passkey != null }45 .mapNotNull { entry ->46 val passkey = entry.passkey ?: return@mapNotNull null47 val rpId = passkey.rpId.trim().lowercase()48 val credentialId = passkey.credentialId49 if (rpId != requestedRpId) return@mapNotNull null50 if (!allowedIds.isNullOrEmpty() && credentialId !in allowedIds) return@mapNotNull null51 if (!PasskeyCallerOrigin.canServeCredential(passkey.callerOrigin, rpId, callerOrigin)) return@mapNotNull null52 val username = passkey.userName.ifBlank { entry.username ?: entry.email ?: entry.title }53 PasskeyCredentialCandidate(54 entryId = entry.id,55 title = entry.title,56 rpId = passkey.rpId,57 credentialId = credentialId,58 username = username,59 displayName = passkey.userDisplayName?.takeIf { it.isNotBlank() } ?: entry.title,60 lastUsedAt = entry.lastUsedAt,61 )62 }63 .sortedWith(64 compareByDescending<PasskeyCredentialCandidate> { it.lastUsedAt ?: Long.MIN_VALUE }65 .thenBy { it.displayName.lowercase() }66 .thenBy { it.username.lowercase() },67 )68 .toList()69 }70}