SyncPaddingTest.kt

4.5 KB · raw

1package com.peachpasswords.sync23import org.junit.Assert.assertArrayEquals4import org.junit.Assert.assertEquals5import org.junit.Assert.assertTrue6import org.junit.Test78class SyncPaddingTest {910    @Test fun round_trips_payloads_of_every_shape() {11        for (size in listOf(0, 1, 5, 251, 252, 253, 1000, 70_000)) {12            val payload = ByteArray(size) { ((it * 31) and 0xFF).toByte() }13            assertArrayEquals(payload, SyncPadding.unpad(SyncPadding.pad(payload)))14        }15    }1617    @Test fun small_control_frames_collapse_to_one_indistinguishable_size() {18        // A relay must not be able to tell a HELLO from a DONE by frame size.19        val hello = ("{\"type\":\"SYNC_HELLO\",\"vaultId\":\"" + "a".repeat(64) + "\"}")20            .toByteArray(Charsets.UTF_8)21        val done = "{\"type\":\"SYNC_DONE\"}".toByteArray(Charsets.UTF_8)22        assertEquals(SyncPadding.pad(hello).size, SyncPadding.pad(done).size)23        assertEquals(256, SyncPadding.pad(done).size)24    }2526    @Test fun bucket_ladder_matches_the_browser_kat() {27        // Pinned against peach-browser sync-padding.test.ts. A mismatch here28        // means the two platforms cannot read each other's frames at all.29        val cases = listOf(30            0 to 256,31            252 to 256,32            253 to 512,33            1024 to 2048,34            65_532 to 65_536,35            65_533 to 131_072,36            200_000 to 262_144,37        )38        for ((payload, expected) in cases) {39            assertEquals(expected, SyncPadding.bucket(4 + payload))40            assertEquals(expected, SyncPadding.pad(ByteArray(payload)).size)41        }42    }4344    @Test fun large_frame_overhead_stays_bounded() {45        val twelveMiB = 12 * 1024 * 102446        val padded = SyncPadding.bucket(twelveMiB)47        assertTrue(padded >= twelveMiB)48        assertTrue(padded - twelveMiB < 64 * 1024)49    }5051    @Test fun rejects_a_frame_declaring_more_payload_than_it_carries() {52        val forged = ByteArray(256)53        forged[0] = 0; forged[1] = 0; forged[2] = 0x27; forged[3] = 0x0F.toByte() // 999954        var threw = false55        try { SyncPadding.unpad(forged) } catch (_: IllegalArgumentException) { threw = true }56        assertTrue(threw)57    }5859    @Test fun rejects_a_frame_shorter_than_its_length_header() {60        var threw = false61        try { SyncPadding.unpad(ByteArray(3)) } catch (_: IllegalArgumentException) { threw = true }62        assertTrue(threw)63    }6465    // ─────────────────────────────────────────────────────────────────────66    // E7b cross-platform KAT — mirrors peach-browser sync-padding.test.ts67    // `describe("sync frame chunking (E7b)")`. Pinned against the browser:68    // either side drifting breaks the wire silently on real hardware.69    // ─────────────────────────────────────────────────────────────────────7071    @Test fun max_noise_plaintext_bytes_matches_the_crypto_core_limit() {72        // crypto-core caps a Noise ciphertext at 65535 bytes; minus the 16-byte73        // Poly1305 tag that is 65519 plaintext. If this constant drifts, the74        // chunking layer accepts chunks the crypto core will reject.75        assertEquals(65519, MAX_NOISE_PLAINTEXT_BYTES)76    }7778    @Test fun the_bucket_ladder_produces_frames_above_the_noise_limit_the_bug() {79        // A 32 KiB payload rounds up to a 64 KiB padded frame, which exceeds80        // the Noise transport limit. Without chunking, the crypto core81        // refused these with "Noise plaintext exceeds transport limit" while82        // the receiver waited for a frame that never arrived — the exact83        // failure this layer exists to fix. Pinning the condition reachable84        // means a future tightening of the ladder is caught here, not on a85        // real device in someone's pocket.86        val padded = SyncPadding.pad(ByteArray(32 * 1024))87        assertTrue(padded.size > MAX_NOISE_PLAINTEXT_BYTES)88    }8990    @Test fun the_continuation_marker_is_the_byte_the_chunking_protocol_uses() {91        // 0x01 is unambiguous against a real padded frame: the first byte of92        // the 4-byte big-endian length header for any payload under 64 KiB93        // is 0x00, and the marker is itself the FIRST byte of the chunk —94        // not a slot inside the padded frame. Must match the browser KAT.95        assertEquals(0x01.toByte(), FRAME_CONTINUATION_MARKER)96    }97}