PasskeyCredentialPlanner.kt

3.0 KB · raw

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    /**24     * F1: [callerOrigin] is the framework-verified origin of the calling app25     * (see PasskeyCallerOrigin.resolve). An unknown caller gets no candidates26     * at all — fail closed.27     */28    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}