package com.peachpasswords.vault import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive /** * Reads a passkey's `privateKeyJwk` whether it arrives as a JSON object or a * string, and writes it back as an object. * * The browser types this field as `JsonWebKey` (`shared/types.ts`) — a real JSON * object. Android modelled it as a `String`, so kotlinx hit `{` where it wanted * a quote and threw "Expected beginning of the string, but got …" partway * through the payload. Any browser vault holding a passkey with a private key * was unreadable on Android — a Codex restore or a sync of that vault failed * with a raw parser error naming a byte offset, which looks like corruption and * is nothing of the sort. * * Kept as a compact string in memory because that is what the rest of the app * and PeachScript already pass around; only the wire form changes. */ object JwkStringSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("com.peachpasswords.vault.JwkString", PrimitiveKind.STRING) private val json = Json { encodeDefaults = true } override fun deserialize(decoder: Decoder): String { val input = decoder as? JsonDecoder ?: return decoder.decodeString() return when (val element = input.decodeJsonElement()) { is JsonPrimitive -> if (element.isString) element.content else element.toString() else -> element.toString() } } override fun serialize(encoder: Encoder, value: String) { val output = encoder as? JsonEncoder ?: return encoder.encodeString(value) // Write the browser's shape back, so a vault that round-trips through // Android stays readable there. val element = runCatching { json.parseToJsonElement(value) }.getOrNull() if (element is JsonObject) output.encodeJsonElement(element) else output.encodeString(value) } }