sync-padding.test.ts

4.1 KB · raw

1import { describe, expect, it } from "vitest";2import {3  padSyncFrame,4  syncFrameBucket,5  unpadSyncFrame,6  MAX_NOISE_PLAINTEXT_BYTES,7  FRAME_CONTINUATION_MARKER,8} from "./sync-padding";910describe("sync frame padding (E7)", () => {11  it("round-trips payloads of every shape, including empty", () => {12    for (const size of [0, 1, 5, 251, 252, 253, 1000, 70_000]) {13      const payload = Uint8Array.from({ length: size }, (_, i) => (i * 31) & 0xff);14      expect(Array.from(unpadSyncFrame(padSyncFrame(payload)))).toEqual(15        Array.from(payload),16      );17    }18  });1920  it("collapses every small control frame to one indistinguishable size", () => {21    // The point of the exercise: a relay must not be able to tell a HELLO from22    // a REQUEST from a DONE by looking at the frame it is forwarding.23    const hello = new TextEncoder().encode('{"type":"SYNC_HELLO","vaultId":"' + "a".repeat(64) + '"}');24    const done = new TextEncoder().encode('{"type":"SYNC_DONE"}');25    expect(padSyncFrame(hello).length).toBe(padSyncFrame(done).length);26    expect(padSyncFrame(done).length).toBe(256);27  });2829  it("pins the bucket ladder — cross-platform KAT, must match peach-android", () => {30    const cases: Array<[number, number]> = [31      [0, 256],32      [252, 256],33      [253, 512],34      [1024, 2048],35      [65_532, 65_536],36      [65_533, 131_072],37      [200_000, 262_144],38    ];39    for (const [payload, expected] of cases) {40      expect(syncFrameBucket(4 + payload)).toBe(expected);41      expect(padSyncFrame(new Uint8Array(payload)).length).toBe(expected);42    }43  });4445  it("keeps large-frame overhead bounded rather than doubling", () => {46    // Doubling a 12 MiB frame would exceed the 24 MiB transport cap for no47    // extra privacy, so buckets go linear above 64 KiB.48    const twelveMiB = 12 * 1024 * 1024;49    const padded = syncFrameBucket(twelveMiB);50    expect(padded).toBeGreaterThanOrEqual(twelveMiB);51    expect(padded - twelveMiB).toBeLessThan(64 * 1024);52  });5354  it("rejects a frame whose declared length exceeds what it carries", () => {55    const forged = new Uint8Array(256);56    new DataView(forged.buffer).setUint32(0, 9999, false);57    expect(() => unpadSyncFrame(forged)).toThrow(/more payload than it carries/);58  });5960  it("rejects a frame shorter than its length header", () => {61    expect(() => unpadSyncFrame(new Uint8Array(3))).toThrow(/length header/);62  });63});6465/**66 * E7b — chunking invariants. The padded-frame ladder can produce frames up67 * to 24 MiB; Noise caps a single transport message at 65519 plaintext68 * bytes. SyncSession.send/receive chunks across that boundary transparently69 * to callers. These tests pin the invariants that the cross-platform70 * transport (and a future Android port) must respect.71 */72describe("sync frame chunking (E7b)", () => {73  it("MAX_NOISE_PLAINTEXT_BYTES matches what the crypto core allows", () => {74    // crypto-core/src/lib.rs: encrypt refuses > MAX_NOISE_MESSAGE - TAG_LEN,75    // which is 65535 - 16 = 65519. If either side drifts, the wire breaks76    // here first, before a real device.77    expect(MAX_NOISE_PLAINTEXT_BYTES).toBe(65519);78  });7980  it("the bucket ladder can produce frames above the Noise limit — the bug", () => {81    // A padded frame for a payload of 32 KiB rounds up to 64 KiB, which82    // exceeds the Noise transport limit (65535 bytes ciphertext, 6551983    // plaintext). Before chunking, the crypto core rejected these with84    // "Noise plaintext exceeds transport limit" and the peer waited for a85    // frame that never arrived. This test pins that the condition is86    // reachable so anyone tightening the ladder in future notices.87    const payload = Uint8Array.from({ length: 32 * 1024 }, () => 0);88    const padded = padSyncFrame(payload);89    expect(padded.byteLength).toBeGreaterThan(MAX_NOISE_PLAINTEXT_BYTES);90  });9192  it("the continuation marker is the byte the chunking protocol uses", () => {93    // A byte that is never a plausible first byte of a real padded frame94    // (which begins with a 4-byte big-endian length, so byte 0 of a 3-byte95    // payload is 0). 0x01 is unambiguous and matches the KAT on Android.96    expect(FRAME_CONTINUATION_MARKER).toBe(0x01);97  });98});