chirp-payload.test.ts

3.5 KB · raw

1import { describe, expect, it } from "vitest";2import {3  CHIRP_MAGIC_BYTES,4  CHIRP_PAYLOAD_LENGTH,5  CHIRP_PROTOCOL_VERSION,6  DEVICE_TYPE_ANDROID,7  DEVICE_TYPE_EXTENSION,8} from "./sync-types";9import {10  ChirpDecodeError,11  crc32,12  decodeChirpPayload,13  encodeChirpPayload,14} from "./chirp-payload";1516function fixedPayload() {17  return {18    version: CHIRP_PROTOCOL_VERSION,19    deviceType: DEVICE_TYPE_EXTENSION,20    ephemeralPublicKey: Uint8Array.from(21      Array.from({ length: 32 }, (_, i) => (i * 7 + 11) & 0xff),22    ),23    sessionNonce: Uint8Array.from(24      Array.from({ length: 16 }, (_, i) => (i * 31 + 1) & 0xff),25    ),26  };27}2829describe("crc32", () => {30  it("matches known IEEE 802.3 vectors", () => {31    expect(crc32(new Uint8Array())).toBe(0x00000000);32    expect(crc32(new TextEncoder().encode("123456789"))).toBe(0xcbf43926);33    expect(crc32(new TextEncoder().encode("a"))).toBe(0xe8b7be43);34  });35});3637describe("chirp payload codec", () => {38  it("round-trips identity for a fixed payload", () => {39    const original = fixedPayload();40    const bytes = encodeChirpPayload(original);41    expect(bytes.length).toBe(CHIRP_PAYLOAD_LENGTH);42    const decoded = decodeChirpPayload(bytes);43    expect(decoded.version).toBe(original.version);44    expect(decoded.deviceType).toBe(original.deviceType);45    expect(decoded.ephemeralPublicKey).toEqual(original.ephemeralPublicKey);46    expect(decoded.sessionNonce).toEqual(original.sessionNonce);47  });4849  it("writes magic bytes PEAC at offset 0", () => {50    const bytes = encodeChirpPayload(fixedPayload());51    expect(bytes.subarray(0, 4)).toEqual(CHIRP_MAGIC_BYTES);52  });5354  it("zero-fills the reserved padding region", () => {55    const bytes = encodeChirpPayload(fixedPayload());56    for (let i = 58; i < 64; i++) expect(bytes[i]).toBe(0);57  });5859  it("rejects wrong-length input", () => {60    expect(() => decodeChirpPayload(new Uint8Array(63))).toThrow(ChirpDecodeError);61    expect(() => decodeChirpPayload(new Uint8Array(65))).toThrow(ChirpDecodeError);62  });6364  it("rejects bad magic", () => {65    const bytes = encodeChirpPayload(fixedPayload());66    bytes[0] = 0;67    expect(() => decodeChirpPayload(bytes)).toThrow(/magic/);68  });6970  it("rejects unsupported version", () => {71    const bytes = encodeChirpPayload(fixedPayload());72    bytes[4] = 0xff;73    // CRC will also fail, but version is checked first74    expect(() => decodeChirpPayload(bytes)).toThrow(/version/);75  });7677  it("rejects unknown device type", () => {78    const bytes = encodeChirpPayload(fixedPayload());79    bytes[5] = 0x09;80    expect(() => decodeChirpPayload(bytes)).toThrow(/device type/);81  });8283  it("rejects checksum tamper", () => {84    const bytes = encodeChirpPayload(fixedPayload());85    bytes[10] ^= 1; // flip a pubkey bit86    expect(() => decodeChirpPayload(bytes)).toThrow(/CRC32/);87  });8889  it("rejects non-zero padding", () => {90    const bytes = encodeChirpPayload(fixedPayload());91    // Mutate padding then re-patch CRC over the first 54 bytes so that the92    // padding check is reached rather than failing at CRC first.93    bytes[63] = 0xab;94    const fresh = crc32(bytes.subarray(0, 54));95    bytes[54] = (fresh >>> 24) & 0xff;96    bytes[55] = (fresh >>> 16) & 0xff;97    bytes[56] = (fresh >>> 8) & 0xff;98    bytes[57] = fresh & 0xff;99    expect(() => decodeChirpPayload(bytes)).toThrow(/padding/);100  });101102  it("encodes android device type byte", () => {103    const bytes = encodeChirpPayload({ ...fixedPayload(), deviceType: DEVICE_TYPE_ANDROID });104    expect(bytes[5]).toBe(0x02);105  });106});