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 org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters15import org.bouncycastle.crypto.signers.Ed25519Signer16import kotlinx.serialization.json.Json17import kotlinx.serialization.json.JsonObject18import kotlinx.serialization.json.JsonPrimitive19import kotlinx.serialization.json.buildJsonObject20import kotlinx.serialization.json.contentOrNull21import kotlinx.serialization.json.intOrNull22import kotlinx.serialization.json.jsonObject23import kotlinx.serialization.json.jsonPrimitive24import kotlinx.serialization.json.put2526data class PasskeyAssertionResult(27 val responseJson: String,28 val nextSignCount: Int,29)3031object PasskeyAssertion {32 private val json = Json { ignoreUnknownKeys = true }33 private val urlEncoder = Base64.getUrlEncoder().withoutPadding()34 private val urlDecoder = Base64.getUrlDecoder()3536 fun build(37 entry: VaultEntry,38 requestJson: String,39 origin: String?,40 clientDataHashOverride: ByteArray? = null,41 42 43 44 userVerified: Boolean = false,45 ): PasskeyAssertionResult? {46 val passkey = entry.passkey ?: return null47 val privateKeyJwk = passkey.privateKeyJwk ?: entry.privateKeyJwk ?: return null48 49 50 51 val callerOrigin = origin?.takeIf { it.isNotBlank() } ?: return null52 if (!PasskeyCallerOrigin.canServeCredential(passkey.callerOrigin, passkey.rpId, callerOrigin)) return null53 val request = runCatching { json.parseToJsonElement(requestJson).jsonObject }.getOrNull() ?: return null54 val rpId = request["rpId"]?.jsonPrimitive?.contentOrNull ?: passkey.rpId55 if (rpId != passkey.rpId) return null56 val challenge = request["challenge"]?.jsonPrimitive?.contentOrNull ?: return null5758 val nextSignCount = passkey.signCount + 159 60 61 62 val flags = assertionFlags(passkey.backupEligible, passkey.backupState, userVerified)63 val authData = buildAuthenticatorData(passkey.rpId, flags, nextSignCount)64 val clientDataJson = buildClientDataJson(65 challenge = challenge,66 origin = callerOrigin,67 )68 val clientDataHash = clientDataHashOverride ?: sha256(clientDataJson)69 val signature = signPasskey(privateKeyJwk, passkey.algorithm, authData + clientDataHash) ?: return null7071 val responseJson = json.encodeToString(72 JsonObject.serializer(),73 buildJsonObject {74 put("id", passkey.credentialId)75 put("rawId", passkey.credentialId)76 put("type", "public-key")77 put("response", buildJsonObject {78 if (clientDataHashOverride == null) {79 put("clientDataJSON", b64(clientDataJson))80 }81 put("authenticatorData", b64(authData))82 put("signature", b64(signature))83 put("userHandle", passkey.userHandle)84 })85 put("authenticatorAttachment", "platform")86 put("clientExtensionResults", buildJsonObject {})87 },88 )89 return PasskeyAssertionResult(responseJson = responseJson, nextSignCount = nextSignCount)90 }9192 private fun buildClientDataJson(challenge: String, origin: String): ByteArray =93 json.encodeToString(94 JsonObject.serializer(),95 buildJsonObject {96 put("type", "webauthn.get")97 put("challenge", challenge)98 put("origin", origin)99 put("crossOrigin", JsonPrimitive(false))100 },101 ).toByteArray(Charsets.UTF_8)102103 private fun buildAuthenticatorData(rpId: String, flags: Int, signCount: Int): ByteArray {104 val out = ByteArray(37)105 sha256(rpId.toByteArray(Charsets.UTF_8)).copyInto(out, 0)106 out[32] = flags.toByte()107 out[33] = ((signCount ushr 24) and 0xff).toByte()108 out[34] = ((signCount ushr 16) and 0xff).toByte()109 out[35] = ((signCount ushr 8) and 0xff).toByte()110 out[36] = (signCount and 0xff).toByte()111 return out112 }113114 115 116 private fun assertionFlags(backupEligible: Boolean, backupState: Boolean, userVerified: Boolean): Int {117 var f = FLAG_UP118 if (userVerified) f = f or FLAG_UV119 if (backupEligible) f = f or FLAG_BE120 if (backupState) f = f or FLAG_BS121 return f122 }123124 private const val FLAG_UP: Int = 0x01125 private const val FLAG_UV: Int = 0x04126 private const val FLAG_BE: Int = 0x08127 private const val FLAG_BS: Int = 0x10128129 private fun signPasskey(privateKeyJwk: String, algorithm: Int, message: ByteArray): ByteArray? =130 when (algorithm) {131 -7 -> signEs256(privateKeyJwk, message)132 -8 -> signEd25519(privateKeyJwk, message)133 else -> null134 }135136 private fun signEs256(privateKeyJwk: String, message: ByteArray): ByteArray? {137 val jwk = runCatching { json.parseToJsonElement(privateKeyJwk).jsonObject }.getOrNull() ?: return null138 if (jwk["kty"]?.jsonPrimitive?.contentOrNull != "EC") return null139 if (jwk["crv"]?.jsonPrimitive?.contentOrNull != "P-256") return null140 val d = jwk["d"]?.jsonPrimitive?.contentOrNull?.let(::b64Decode) ?: return null141 val privateKey = ecPrivateKey(d)142 val signer = Signature.getInstance("SHA256withECDSA")143 signer.initSign(privateKey)144 signer.update(message)145 return signer.sign()146 }147148 private fun signEd25519(privateKeyJwk: String, message: ByteArray): ByteArray? {149 val jwk = runCatching { json.parseToJsonElement(privateKeyJwk).jsonObject }.getOrNull() ?: return null150 if (jwk["kty"]?.jsonPrimitive?.contentOrNull != "OKP") return null151 if (jwk["crv"]?.jsonPrimitive?.contentOrNull != "Ed25519") return null152 val d = jwk["d"]?.jsonPrimitive?.contentOrNull?.let(::b64Decode) ?: return null153 if (d.size != 32) return null154 val signer = Ed25519Signer()155 signer.init(true, Ed25519PrivateKeyParameters(d, 0))156 signer.update(message, 0, message.size)157 return signer.generateSignature()158 }159160 private fun ecPrivateKey(d: ByteArray): ECPrivateKey {161 val keyFactory = KeyFactory.getInstance("EC")162 val spec = ECPrivateKeySpec(BigInteger(1, d), ecP256Spec())163 return keyFactory.generatePrivate(spec) as ECPrivateKey164 }165166 private fun ecP256Spec(): ECParameterSpec {167 val params = AlgorithmParameters.getInstance("EC")168 params.init(ECGenParameterSpec("secp256r1"))169 return params.getParameterSpec(ECParameterSpec::class.java)170 }171172 private fun sha256(data: ByteArray): ByteArray = MessageDigest.getInstance("SHA-256").digest(data)173 private fun b64(data: ByteArray): String = urlEncoder.encodeToString(data)174 private fun b64Decode(value: String): ByteArray {175 val padded = value.padEnd(((value.length + 3) / 4) * 4, '=')176 return urlDecoder.decode(padded)177 }178}179180181182183184185186187188189190191192internal fun shouldSignAssertion(uvRequired: Boolean, userVerified: Boolean): Boolean =193 !uvRequired || userVerified