JwkString.kt

2.4 KB · raw

1package com.peachpasswords.vault23import kotlinx.serialization.KSerializer4import kotlinx.serialization.descriptors.PrimitiveKind5import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor6import kotlinx.serialization.descriptors.SerialDescriptor7import kotlinx.serialization.encoding.Decoder8import kotlinx.serialization.encoding.Encoder9import kotlinx.serialization.json.Json10import kotlinx.serialization.json.JsonDecoder11import kotlinx.serialization.json.JsonEncoder12import kotlinx.serialization.json.JsonObject13import kotlinx.serialization.json.JsonPrimitive1415/**16 * Reads a passkey's `privateKeyJwk` whether it arrives as a JSON object or a17 * string, and writes it back as an object.18 *19 * The browser types this field as `JsonWebKey` (`shared/types.ts`) — a real JSON20 * object. Android modelled it as a `String`, so kotlinx hit `{` where it wanted21 * a quote and threw "Expected beginning of the string, but got …" partway22 * through the payload. Any browser vault holding a passkey with a private key23 * was unreadable on Android — a Codex restore or a sync of that vault failed24 * with a raw parser error naming a byte offset, which looks like corruption and25 * is nothing of the sort.26 *27 * Kept as a compact string in memory because that is what the rest of the app28 * and PeachScript already pass around; only the wire form changes.29 */30object JwkStringSerializer : KSerializer<String> {31    override val descriptor: SerialDescriptor =32        PrimitiveSerialDescriptor("com.peachpasswords.vault.JwkString", PrimitiveKind.STRING)3334    private val json = Json { encodeDefaults = true }3536    override fun deserialize(decoder: Decoder): String {37        val input = decoder as? JsonDecoder ?: return decoder.decodeString()38        return when (val element = input.decodeJsonElement()) {39            is JsonPrimitive -> if (element.isString) element.content else element.toString()40            else -> element.toString()41        }42    }4344    override fun serialize(encoder: Encoder, value: String) {45        val output = encoder as? JsonEncoder ?: return encoder.encodeString(value)46        // Write the browser's shape back, so a vault that round-trips through47        // Android stays readable there.48        val element = runCatching { json.parseToJsonElement(value) }.getOrNull()49        if (element is JsonObject) output.encodeJsonElement(element) else output.encodeString(value)50    }51}