BrowserPasskeyJwkTest.kt

2.9 KB · raw

1package com.peachpasswords.vault23import org.junit.Assert.assertEquals4import org.junit.Assert.assertTrue5import org.junit.Test67/**8 * A passkey's `privateKeyJwk` is a JSON OBJECT in the browser (`JsonWebKey` in9 * shared/types.ts); Android holds it as a compact string. Before the custom10 * serializer, decoding a real browser vault threw11 * "Expected beginning of the string, but got {" partway through the payload —12 * so any browser vault containing a passkey with a private key could not be13 * restored from a Codex or synced onto Android at all, and the failure named a14 * byte offset, which reads as file corruption rather than a schema mismatch.15 */16class BrowserPasskeyJwkTest {1718    /** Shaped exactly as the browser writes it. */19    private val browserEntryJson = """20        {21          "id": "e1",22          "type": "login",23          "title": "GitHub",24          "username": "june",25          "createdAt": 1,26          "modifiedAt": 2,27          "passkey": {28            "rpId": "github.com",29            "rpName": "GitHub",30            "credentialId": "Y3JlZA",31            "publicKeyCose": "cG9zZQ",32            "privateKeyJwk": {33              "kty": "EC",34              "crv": "P-256",35              "x": "abc",36              "y": "def",37              "d": "secret"38            },39            "userHandle": "dXNlcg",40            "userName": "june",41            "userDisplayName": "June",42            "algorithm": -7,43            "signCount": 0,44            "aaguid": "",45            "backupEligible": false,46            "backupState": false,47            "createdAt": 148          }49        }50    """.trimIndent()5152    @Test fun `a browser passkey entry decodes instead of throwing on the jwk object`() {53        val entry = VaultCodec.decodeEntryForTest(browserEntryJson)54        assertEquals("GitHub", entry.title)55        val jwk = entry.passkey?.privateKeyJwk56        assertTrue("the jwk should survive as compact json, got $jwk", jwk != null && jwk.contains("\"kty\""))57        assertTrue("the private component must not be lost", jwk!!.contains("secret"))58    }5960    @Test fun `re-encoding writes the jwk back as an object the browser can read`() {61        val entry = VaultCodec.decodeEntryForTest(browserEntryJson)62        val json = VaultCodec.encodeEntryForTest(entry)63        // A quoted string here would be silently unreadable to the browser.64        assertTrue("expected an object, got: $json", json.contains("\"privateKeyJwk\":{"))65        assertTrue(json.contains("\"kty\":\"EC\""))66    }6768    @Test fun `a jwk already stored as a string still round-trips`() {69        val asString = browserEntryJson.replace(70            Regex("\"privateKeyJwk\": \\{[^}]*\\}"),71            "\"privateKeyJwk\": \"not-json\"",72        )73        val entry = VaultCodec.decodeEntryForTest(asString)74        assertEquals("not-json", entry.passkey?.privateKeyJwk)75        assertTrue(VaultCodec.encodeEntryForTest(entry).contains("\"privateKeyJwk\":\"not-json\""))76    }77}