PasskeyAssertion.kt

6.7 KB · raw

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    ): PasskeyAssertionResult? {42        val passkey = entry.passkey ?: return null43        val privateKeyJwk = passkey.privateKeyJwk ?: entry.privateKeyJwk ?: return null44        val request = runCatching { json.parseToJsonElement(requestJson).jsonObject }.getOrNull() ?: return null45        val rpId = request["rpId"]?.jsonPrimitive?.contentOrNull ?: passkey.rpId46        if (rpId != passkey.rpId) return null47        val challenge = request["challenge"]?.jsonPrimitive?.contentOrNull ?: return null4849        val nextSignCount = passkey.signCount + 150        val flags = 0x05 or (if (passkey.backupEligible) 0x08 else 0) or (if (passkey.backupState) 0x10 else 0)51        val authData = buildAuthenticatorData(passkey.rpId, flags, nextSignCount)52        val clientDataJson = buildClientDataJson(53            challenge = challenge,54            origin = origin?.takeIf { it.isNotBlank() } ?: originFromRpId(passkey.rpId),55        )56        val clientDataHash = clientDataHashOverride ?: sha256(clientDataJson)57        val signature = signPasskey(privateKeyJwk, passkey.algorithm, authData + clientDataHash) ?: return null5859        val responseJson = json.encodeToString(60            JsonObject.serializer(),61            buildJsonObject {62                put("id", passkey.credentialId)63                put("rawId", passkey.credentialId)64                put("type", "public-key")65                put("response", buildJsonObject {66                    put("clientDataJSON", b64(clientDataJson))67                    put("authenticatorData", b64(authData))68                    put("signature", b64(signature))69                    put("userHandle", passkey.userHandle)70                })71                put("authenticatorAttachment", "platform")72                put("clientExtensionResults", buildJsonObject {})73            },74        )75        return PasskeyAssertionResult(responseJson = responseJson, nextSignCount = nextSignCount)76    }7778    private fun buildClientDataJson(challenge: String, origin: String): ByteArray =79        json.encodeToString(80            JsonObject.serializer(),81            buildJsonObject {82                put("type", "webauthn.get")83                put("challenge", challenge)84                put("origin", origin)85                put("crossOrigin", JsonPrimitive(false))86            },87        ).toByteArray(Charsets.UTF_8)8889    private fun buildAuthenticatorData(rpId: String, flags: Int, signCount: Int): ByteArray {90        val out = ByteArray(37)91        sha256(rpId.toByteArray(Charsets.UTF_8)).copyInto(out, 0)92        out[32] = flags.toByte()93        out[33] = ((signCount ushr 24) and 0xff).toByte()94        out[34] = ((signCount ushr 16) and 0xff).toByte()95        out[35] = ((signCount ushr 8) and 0xff).toByte()96        out[36] = (signCount and 0xff).toByte()97        return out98    }99100    private fun signPasskey(privateKeyJwk: String, algorithm: Int, message: ByteArray): ByteArray? =101        when (algorithm) {102            -7 -> signEs256(privateKeyJwk, message)103            -8 -> signEd25519(privateKeyJwk, message)104            else -> null105        }106107    private fun signEs256(privateKeyJwk: String, message: ByteArray): ByteArray? {108        val jwk = runCatching { json.parseToJsonElement(privateKeyJwk).jsonObject }.getOrNull() ?: return null109        if (jwk["kty"]?.jsonPrimitive?.contentOrNull != "EC") return null110        if (jwk["crv"]?.jsonPrimitive?.contentOrNull != "P-256") return null111        val d = jwk["d"]?.jsonPrimitive?.contentOrNull?.let(::b64Decode) ?: return null112        val privateKey = ecPrivateKey(d)113        val signer = Signature.getInstance("SHA256withECDSA")114        signer.initSign(privateKey)115        signer.update(message)116        return signer.sign()117    }118119    private fun signEd25519(privateKeyJwk: String, message: ByteArray): ByteArray? {120        val jwk = runCatching { json.parseToJsonElement(privateKeyJwk).jsonObject }.getOrNull() ?: return null121        if (jwk["kty"]?.jsonPrimitive?.contentOrNull != "OKP") return null122        if (jwk["crv"]?.jsonPrimitive?.contentOrNull != "Ed25519") return null123        val d = jwk["d"]?.jsonPrimitive?.contentOrNull?.let(::b64Decode) ?: return null124        if (d.size != 32) return null125        val signer = Ed25519Signer()126        signer.init(true, Ed25519PrivateKeyParameters(d, 0))127        signer.update(message, 0, message.size)128        return signer.generateSignature()129    }130131    private fun ecPrivateKey(d: ByteArray): ECPrivateKey {132        val keyFactory = KeyFactory.getInstance("EC")133        val spec = ECPrivateKeySpec(BigInteger(1, d), ecP256Spec())134        return keyFactory.generatePrivate(spec) as ECPrivateKey135    }136137    private fun ecP256Spec(): ECParameterSpec {138        val params = AlgorithmParameters.getInstance("EC")139        params.init(ECGenParameterSpec("secp256r1"))140        return params.getParameterSpec(ECParameterSpec::class.java)141    }142143    private fun originFromRpId(rpId: String): String =144        if (rpId.equals("localhost", ignoreCase = true) || rpId == "127.0.0.1" || rpId == "[::1]") {145            "http://$rpId"146        } else {147            "https://$rpId"148        }149150    private fun sha256(data: ByteArray): ByteArray = MessageDigest.getInstance("SHA-256").digest(data)151    private fun b64(data: ByteArray): String = urlEncoder.encodeToString(data)152    private fun b64Decode(value: String): ByteArray {153        val padded = value.padEnd(((value.length + 3) / 4) * 4, '=')154        return urlDecoder.decode(padded)155    }156}