PairIdentityFramesTest.kt

4.2 KB · raw

1package com.peachpasswords.sync23import org.junit.Assert.assertEquals4import org.junit.Assert.assertNull5import org.junit.Assert.assertTrue6import org.junit.Assert.fail7import org.junit.Test89/**10 * Regression coverage for M-08: the hand-rolled PAIR_IDENTITY encoder escaped11 * `"`, `\`, and control bytes; the parser stopped at the next unescaped quote12 * and never unescaped. Names containing those characters truncated the frame13 * and corrupted everything after it. The kotlinx.serialization-backed codec14 * round-trips every JSON escape and validates fields at the trust boundary.15 */16private const val MAX_DEVICE_NAME_FOR_TEST = 641718class PairIdentityFramesTest {19    private val base = LocalDeviceIdentity(20        deviceId = "client-1",21        deviceName = "Phone",22        deviceType = DeviceType.ANDROID,23        longTermPublicKeyHex = "cd".repeat(32),24        signingPublicKeyHex = "ef".repeat(32),25        relayUrl = "wss://relay.peachpasswords.com",26    )2728    @Test29    fun `round-trips a device name containing every JSON escape character`() {30        val tricky = "a\"b\\c\nd\te\rf"31        val wire = encodePairIdentityFrame(base.copy(deviceName = tricky))32        val parsed = parsePairIdentityFrame(wire)33        assertEquals(tricky, parsed.deviceName)34    }3536    @Test37    fun `decoder rejects a name with a raw embedded newline`() {38        val raw = """{"type":"PAIR_IDENTITY","device_id":"d","device_name":"bad${"\n"}name","device_type":"android","long_term_public_key_hex":"${"cd".repeat(32)}","signing_public_key_hex":"${"ef".repeat(32)}","relay_url":"wss://r.example.com"}"""39        try {40            parsePairIdentityFrame(raw.toByteArray())41            fail("expected control-char rejection")42        } catch (e: IllegalArgumentException) {43            assertTrue(e.message!!.contains("device_name"))44        }45    }4647    @Test48    fun `decoder unescapes a literal backslash-quote in the name`() {49        // The legacy parser stopped at the first unescaped quote and read50        // `a\` as the value, then desynced the rest of the frame.51        val raw = """{"type":"PAIR_IDENTITY","device_id":"d","device_name":"a\"b","device_type":"android","long_term_public_key_hex":"${"cd".repeat(32)}","signing_public_key_hex":"${"ef".repeat(32)}","relay_url":"wss://r.example.com"}"""52        val parsed = parsePairIdentityFrame(raw.toByteArray())53        assertEquals("a\"b", parsed.deviceName)54    }5556    @Test57    fun `decoder rejects a non-hex long term public key`() {58        val raw = """{"type":"PAIR_IDENTITY","device_id":"d","device_name":"n","device_type":"android","long_term_public_key_hex":"${"zz".repeat(32)}","signing_public_key_hex":"${"ef".repeat(32)}","relay_url":"wss://r.example.com"}"""59        try {60            parsePairIdentityFrame(raw.toByteArray())61            fail("expected hex rejection")62        } catch (e: IllegalArgumentException) {63            assertTrue(e.message!!.contains("hex"))64        }65    }6667    @Test68    fun `decoder rejects a device name longer than the cap`() {69        val long = "x".repeat(MAX_DEVICE_NAME_FOR_TEST + 1)70        val wire = encodePairIdentityFrame(base.copy(deviceName = long))71        try {72            parsePairIdentityFrame(wire)73            fail("expected length rejection")74        } catch (e: IllegalArgumentException) {75            assertTrue(e.message!!.contains("device_name"))76        }77    }7879    @Test80    fun `signing key is null when omitted from the wire`() {81        val omitted = """{"type":"PAIR_IDENTITY","device_id":"d","device_name":"n","device_type":"android","long_term_public_key_hex":"${"cd".repeat(32)}","relay_url":"wss://r.example.com"}"""82        val parsed = parsePairIdentityFrame(omitted.toByteArray())83        assertNull(parsed.signingPublicKeyHex)84    }8586    @Test87    fun `wire format preserves the type discriminator and snake_case keys`() {88        val wire = String(encodePairIdentityFrame(base), Charsets.UTF_8)89        assertTrue("frame must include type discriminator",90            wire.contains("\"type\":\"PAIR_IDENTITY\""))91        assertTrue("frame must use snake_case keys", wire.contains("\"device_id\""))92        assertTrue("frame must use snake_case keys", wire.contains("\"long_term_public_key_hex\""))93    }94}