PasskeyCredentialPlanner.kt

2.5 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    fun candidates(entries: List<VaultEntry>, requestJson: String): List<PasskeyCredentialCandidate> {24        val request = runCatching { json.parseToJsonElement(requestJson).jsonObject }.getOrNull() ?: return emptyList()25        val requestedRpId = request["rpId"]?.jsonPrimitive?.contentOrNull?.trim()?.lowercase() ?: return emptyList()26        val allowedIds = runCatching { request["allowCredentials"]?.jsonArray }.getOrNull()27            ?.mapNotNull { item ->28                runCatching { item.jsonObject["id"]?.jsonPrimitive?.contentOrNull }.getOrNull()29            }30            ?.filter { it.isNotBlank() }31            ?.toSet()3233        return entries.asSequence()34            .filter { it.deletedAt == null && it.passkey != null }35            .mapNotNull { entry ->36                val passkey = entry.passkey ?: return@mapNotNull null37                val rpId = passkey.rpId.trim().lowercase()38                val credentialId = passkey.credentialId39                if (rpId != requestedRpId) return@mapNotNull null40                if (!allowedIds.isNullOrEmpty() && credentialId !in allowedIds) return@mapNotNull null41                val username = passkey.userName.ifBlank { entry.username ?: entry.email ?: entry.title }42                PasskeyCredentialCandidate(43                    entryId = entry.id,44                    title = entry.title,45                    rpId = passkey.rpId,46                    credentialId = credentialId,47                    username = username,48                    displayName = passkey.userDisplayName?.takeIf { it.isNotBlank() } ?: entry.title,49                    lastUsedAt = entry.lastUsedAt,50                )51            }52            .sortedWith(53                compareByDescending<PasskeyCredentialCandidate> { it.lastUsedAt ?: Long.MIN_VALUE }54                    .thenBy { it.displayName.lowercase() }55                    .thenBy { it.username.lowercase() },56            )57            .toList()58    }59}