1package com.peachpasswords.passkeys23import com.peachpasswords.vault.VaultEntry4import java.math.BigInteger5import java.security.AlgorithmParameters6import java.security.KeyFactory7import java.security.MessageDigest8import java.security.Signature9import java.security.interfaces.ECPrivateKey10import java.security.spec.ECGenParameterSpec11import java.security.spec.ECParameterSpec12import java.security.spec.ECPrivateKeySpec13import java.util.Base6414import kotlinx.serialization.json.Json15import kotlinx.serialization.json.JsonObject16import kotlinx.serialization.json.JsonPrimitive17import kotlinx.serialization.json.buildJsonObject18import kotlinx.serialization.json.contentOrNull19import kotlinx.serialization.json.intOrNull20import kotlinx.serialization.json.jsonObject21import kotlinx.serialization.json.jsonPrimitive22import kotlinx.serialization.json.put2324data class PasskeyAssertionResult(25 val responseJson: String,26 val nextSignCount: Int,27)2829object PasskeyAssertion {30 private val json = Json { ignoreUnknownKeys = true }31 private val urlEncoder = Base64.getUrlEncoder().withoutPadding()32 private val urlDecoder = Base64.getUrlDecoder()3334 fun build(35 entry: VaultEntry,36 requestJson: String,37 origin: String?,38 clientDataHashOverride: ByteArray? = null,39 ): PasskeyAssertionResult? {40 val passkey = entry.passkey ?: return null41 if (passkey.algorithm != -7) return null42 val privateKeyJwk = passkey.privateKeyJwk ?: entry.privateKeyJwk ?: return null43 val request = runCatching { json.parseToJsonElement(requestJson).jsonObject }.getOrNull() ?: return null44 val rpId = request["rpId"]?.jsonPrimitive?.contentOrNull ?: passkey.rpId45 if (rpId != passkey.rpId) return null46 val challenge = request["challenge"]?.jsonPrimitive?.contentOrNull ?: return null4748 val nextSignCount = passkey.signCount + 149 val flags = 0x05 or (if (passkey.backupEligible) 0x08 else 0) or (if (passkey.backupState) 0x10 else 0)50 val authData = buildAuthenticatorData(passkey.rpId, flags, nextSignCount)51 val clientDataJson = buildClientDataJson(52 challenge = challenge,53 origin = origin?.takeIf { it.isNotBlank() } ?: originFromRpId(passkey.rpId),54 )55 val clientDataHash = clientDataHashOverride ?: sha256(clientDataJson)56 val signature = signEs256(privateKeyJwk, authData + clientDataHash) ?: return null5758 val responseJson = json.encodeToString(59 JsonObject.serializer(),60 buildJsonObject {61 put("id", passkey.credentialId)62 put("rawId", passkey.credentialId)63 put("type", "public-key")64 put("response", buildJsonObject {65 put("clientDataJSON", b64(clientDataJson))66 put("authenticatorData", b64(authData))67 put("signature", b64(signature))68 put("userHandle", passkey.userHandle)69 })70 put("authenticatorAttachment", "platform")71 put("clientExtensionResults", buildJsonObject {})72 },73 )74 return PasskeyAssertionResult(responseJson = responseJson, nextSignCount = nextSignCount)75 }7677 private fun buildClientDataJson(challenge: String, origin: String): ByteArray =78 json.encodeToString(79 JsonObject.serializer(),80 buildJsonObject {81 put("type", "webauthn.get")82 put("challenge", challenge)83 put("origin", origin)84 put("crossOrigin", JsonPrimitive(false))85 },86 ).toByteArray(Charsets.UTF_8)8788 private fun buildAuthenticatorData(rpId: String, flags: Int, signCount: Int): ByteArray {89 val out = ByteArray(37)90 sha256(rpId.toByteArray(Charsets.UTF_8)).copyInto(out, 0)91 out[32] = flags.toByte()92 out[33] = ((signCount ushr 24) and 0xff).toByte()93 out[34] = ((signCount ushr 16) and 0xff).toByte()94 out[35] = ((signCount ushr 8) and 0xff).toByte()95 out[36] = (signCount and 0xff).toByte()96 return out97 }9899 private fun signEs256(privateKeyJwk: String, message: ByteArray): ByteArray? {100 val jwk = runCatching { json.parseToJsonElement(privateKeyJwk).jsonObject }.getOrNull() ?: return null101 if (jwk["kty"]?.jsonPrimitive?.contentOrNull != "EC") return null102 if (jwk["crv"]?.jsonPrimitive?.contentOrNull != "P-256") return null103 val d = jwk["d"]?.jsonPrimitive?.contentOrNull?.let(::b64Decode) ?: return null104 val privateKey = ecPrivateKey(d)105 val signer = Signature.getInstance("SHA256withECDSA")106 signer.initSign(privateKey)107 signer.update(message)108 return signer.sign()109 }110111 private fun ecPrivateKey(d: ByteArray): ECPrivateKey {112 val keyFactory = KeyFactory.getInstance("EC")113 val spec = ECPrivateKeySpec(BigInteger(1, d), ecP256Spec())114 return keyFactory.generatePrivate(spec) as ECPrivateKey115 }116117 private fun ecP256Spec(): ECParameterSpec {118 val params = AlgorithmParameters.getInstance("EC")119 params.init(ECGenParameterSpec("secp256r1"))120 return params.getParameterSpec(ECParameterSpec::class.java)121 }122123 private fun originFromRpId(rpId: String): String =124 if (rpId.equals("localhost", ignoreCase = true) || rpId == "127.0.0.1" || rpId == "[::1]") {125 "http://$rpId"126 } else {127 "https://$rpId"128 }129130 private fun sha256(data: ByteArray): ByteArray = MessageDigest.getInstance("SHA-256").digest(data)131 private fun b64(data: ByteArray): String = urlEncoder.encodeToString(data)132 private fun b64Decode(value: String): ByteArray {133 val padded = value.padEnd(((value.length + 3) / 4) * 4, '=')134 return urlDecoder.decode(padded)135 }136}