package com.peachpasswords.vault import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test /** * A passkey's `privateKeyJwk` is a JSON OBJECT in the browser (`JsonWebKey` in * shared/types.ts); Android holds it as a compact string. Before the custom * serializer, decoding a real browser vault threw * "Expected beginning of the string, but got {" partway through the payload — * so any browser vault containing a passkey with a private key could not be * restored from a Codex or synced onto Android at all, and the failure named a * byte offset, which reads as file corruption rather than a schema mismatch. */ class BrowserPasskeyJwkTest { /** Shaped exactly as the browser writes it. */ private val browserEntryJson = """ { "id": "e1", "type": "login", "title": "GitHub", "username": "june", "createdAt": 1, "modifiedAt": 2, "passkey": { "rpId": "github.com", "rpName": "GitHub", "credentialId": "Y3JlZA", "publicKeyCose": "cG9zZQ", "privateKeyJwk": { "kty": "EC", "crv": "P-256", "x": "abc", "y": "def", "d": "secret" }, "userHandle": "dXNlcg", "userName": "june", "userDisplayName": "June", "algorithm": -7, "signCount": 0, "aaguid": "", "backupEligible": false, "backupState": false, "createdAt": 1 } } """.trimIndent() @Test fun `a browser passkey entry decodes instead of throwing on the jwk object`() { val entry = VaultCodec.decodeEntryForTest(browserEntryJson) assertEquals("GitHub", entry.title) val jwk = entry.passkey?.privateKeyJwk assertTrue("the jwk should survive as compact json, got $jwk", jwk != null && jwk.contains("\"kty\"")) assertTrue("the private component must not be lost", jwk!!.contains("secret")) } @Test fun `re-encoding writes the jwk back as an object the browser can read`() { val entry = VaultCodec.decodeEntryForTest(browserEntryJson) val json = VaultCodec.encodeEntryForTest(entry) // A quoted string here would be silently unreadable to the browser. assertTrue("expected an object, got: $json", json.contains("\"privateKeyJwk\":{")) assertTrue(json.contains("\"kty\":\"EC\"")) } @Test fun `a jwk already stored as a string still round-trips`() { val asString = browserEntryJson.replace( Regex("\"privateKeyJwk\": \\{[^}]*\\}"), "\"privateKeyJwk\": \"not-json\"", ) val entry = VaultCodec.decodeEntryForTest(asString) assertEquals("not-json", entry.passkey?.privateKeyJwk) assertTrue(VaultCodec.encodeEntryForTest(entry).contains("\"privateKeyJwk\":\"not-json\"")) } }