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)3031internal enum class PasskeyAssertionFailure {32 MISSING_PASSKEY,33 MISSING_PRIVATE_KEY,34 MISSING_ORIGIN,35 CALLER_NOT_ALLOWED,36 INVALID_REQUEST,37 RP_MISMATCH,38 MISSING_CHALLENGE,39 SIGNATURE_FAILED,40}4142internal sealed interface PasskeyAssertionBuild {43 data class Success(val result: PasskeyAssertionResult) : PasskeyAssertionBuild44 data class Failure(val reason: PasskeyAssertionFailure) : PasskeyAssertionBuild45}4647object PasskeyAssertion {48 private val json = Json { ignoreUnknownKeys = true }49 private val urlEncoder = Base64.getUrlEncoder().withoutPadding()50 private val urlDecoder = Base64.getUrlDecoder()5152 fun build(53 entry: VaultEntry,54 requestJson: String,55 origin: String?,56 clientDataHashOverride: ByteArray? = null,57 58 59 60 userVerified: Boolean = false,61 ): PasskeyAssertionResult? = (buildDetailed(62 entry = entry,63 requestJson = requestJson,64 origin = origin,65 clientDataHashOverride = clientDataHashOverride,66 userVerified = userVerified,67 ) as? PasskeyAssertionBuild.Success)?.result6869 internal fun buildDetailed(70 entry: VaultEntry,71 requestJson: String,72 origin: String?,73 clientDataHashOverride: ByteArray? = null,74 userVerified: Boolean = false,75 ): PasskeyAssertionBuild {76 val passkey = entry.passkey77 ?: return PasskeyAssertionBuild.Failure(PasskeyAssertionFailure.MISSING_PASSKEY)78 val privateKeyJwk = passkey.privateKeyJwk ?: entry.privateKeyJwk79 ?: return PasskeyAssertionBuild.Failure(PasskeyAssertionFailure.MISSING_PRIVATE_KEY)80 81 82 83 val callerOrigin = origin?.takeIf { it.isNotBlank() }84 ?: return PasskeyAssertionBuild.Failure(PasskeyAssertionFailure.MISSING_ORIGIN)85 if (!PasskeyCallerOrigin.canServeCredential(passkey.callerOrigin, passkey.rpId, callerOrigin)) {86 return PasskeyAssertionBuild.Failure(PasskeyAssertionFailure.CALLER_NOT_ALLOWED)87 }88 val request = runCatching { json.parseToJsonElement(requestJson).jsonObject }.getOrNull()89 ?: return PasskeyAssertionBuild.Failure(PasskeyAssertionFailure.INVALID_REQUEST)90 val rpId = request["rpId"]?.jsonPrimitive?.contentOrNull ?: passkey.rpId91 if (rpId != passkey.rpId) {92 return PasskeyAssertionBuild.Failure(PasskeyAssertionFailure.RP_MISMATCH)93 }94 val challenge = request["challenge"]?.jsonPrimitive?.contentOrNull95 ?: return PasskeyAssertionBuild.Failure(PasskeyAssertionFailure.MISSING_CHALLENGE)9697 val nextSignCount = passkey.signCount + 198 99 100 101 val flags = assertionFlags(passkey.backupEligible, passkey.backupState, userVerified)102 val authData = buildAuthenticatorData(passkey.rpId, flags, nextSignCount)103 val clientDataJson = buildClientDataJson(104 challenge = challenge,105 origin = callerOrigin,106 )107 val clientDataHash = clientDataHashOverride ?: sha256(clientDataJson)108 val signature = signPasskey(privateKeyJwk, passkey.algorithm, authData + clientDataHash)109 ?: return PasskeyAssertionBuild.Failure(PasskeyAssertionFailure.SIGNATURE_FAILED)110111 val responseJson = json.encodeToString(112 JsonObject.serializer(),113 buildJsonObject {114 put("id", passkey.credentialId)115 put("rawId", passkey.credentialId)116 put("type", "public-key")117 put("response", buildJsonObject {118 if (clientDataHashOverride == null) {119 put("clientDataJSON", b64(clientDataJson))120 }121 put("authenticatorData", b64(authData))122 put("signature", b64(signature))123 put("userHandle", passkey.userHandle)124 })125 put("authenticatorAttachment", "platform")126 put("clientExtensionResults", buildJsonObject {})127 },128 )129 return PasskeyAssertionBuild.Success(130 PasskeyAssertionResult(responseJson = responseJson, nextSignCount = nextSignCount),131 )132 }133134 private fun buildClientDataJson(challenge: String, origin: String): ByteArray =135 json.encodeToString(136 JsonObject.serializer(),137 buildJsonObject {138 put("type", "webauthn.get")139 put("challenge", challenge)140 put("origin", origin)141 put("crossOrigin", JsonPrimitive(false))142 },143 ).toByteArray(Charsets.UTF_8)144145 private fun buildAuthenticatorData(rpId: String, flags: Int, signCount: Int): ByteArray {146 val out = ByteArray(37)147 sha256(rpId.toByteArray(Charsets.UTF_8)).copyInto(out, 0)148 out[32] = flags.toByte()149 out[33] = ((signCount ushr 24) and 0xff).toByte()150 out[34] = ((signCount ushr 16) and 0xff).toByte()151 out[35] = ((signCount ushr 8) and 0xff).toByte()152 out[36] = (signCount and 0xff).toByte()153 return out154 }155156 157 158 private fun assertionFlags(backupEligible: Boolean, backupState: Boolean, userVerified: Boolean): Int {159 var f = FLAG_UP160 if (userVerified) f = f or FLAG_UV161 if (backupEligible) f = f or FLAG_BE162 if (backupState) f = f or FLAG_BS163 return f164 }165166 private const val FLAG_UP: Int = 0x01167 private const val FLAG_UV: Int = 0x04168 private const val FLAG_BE: Int = 0x08169 private const val FLAG_BS: Int = 0x10170171 private fun signPasskey(privateKeyJwk: String, algorithm: Int, message: ByteArray): ByteArray? =172 when (algorithm) {173 -7 -> signEs256(privateKeyJwk, message)174 -8 -> signEd25519(privateKeyJwk, message)175 else -> null176 }177178 private fun signEs256(privateKeyJwk: String, message: ByteArray): ByteArray? {179 val jwk = runCatching { json.parseToJsonElement(privateKeyJwk).jsonObject }.getOrNull() ?: return null180 if (jwk["kty"]?.jsonPrimitive?.contentOrNull != "EC") return null181 if (jwk["crv"]?.jsonPrimitive?.contentOrNull != "P-256") return null182 val d = jwk["d"]?.jsonPrimitive?.contentOrNull?.let(::b64Decode) ?: return null183 val privateKey = ecPrivateKey(d)184 val signer = Signature.getInstance("SHA256withECDSA")185 signer.initSign(privateKey)186 signer.update(message)187 return signer.sign()188 }189190 private fun signEd25519(privateKeyJwk: String, message: ByteArray): ByteArray? {191 val jwk = runCatching { json.parseToJsonElement(privateKeyJwk).jsonObject }.getOrNull() ?: return null192 if (jwk["kty"]?.jsonPrimitive?.contentOrNull != "OKP") return null193 if (jwk["crv"]?.jsonPrimitive?.contentOrNull != "Ed25519") return null194 val d = jwk["d"]?.jsonPrimitive?.contentOrNull?.let(::b64Decode) ?: return null195 if (d.size != 32) return null196 val signer = Ed25519Signer()197 signer.init(true, Ed25519PrivateKeyParameters(d, 0))198 signer.update(message, 0, message.size)199 return signer.generateSignature()200 }201202 private fun ecPrivateKey(d: ByteArray): ECPrivateKey {203 val keyFactory = KeyFactory.getInstance("EC")204 val spec = ECPrivateKeySpec(BigInteger(1, d), ecP256Spec())205 return keyFactory.generatePrivate(spec) as ECPrivateKey206 }207208 private fun ecP256Spec(): ECParameterSpec {209 val params = AlgorithmParameters.getInstance("EC")210 params.init(ECGenParameterSpec("secp256r1"))211 return params.getParameterSpec(ECParameterSpec::class.java)212 }213214 private fun sha256(data: ByteArray): ByteArray = MessageDigest.getInstance("SHA-256").digest(data)215 private fun b64(data: ByteArray): String = urlEncoder.encodeToString(data)216 private fun b64Decode(value: String): ByteArray {217 val padded = value.padEnd(((value.length + 3) / 4) * 4, '=')218 return urlDecoder.decode(padded)219 }220}221222223224225226227228229230231232233234internal fun shouldSignAssertion(uvRequired: Boolean, userVerified: Boolean): Boolean =235 !uvRequired || userVerified