codex.adversarial.test.ts

24.8 KB · raw

1/**2 * codex.adversarial.test.ts — Layer 3 §3.4 adversarial Codex tests.3 *4 * Exercises ChunkBuffer, parseRawScan, encodeWithErasure/decodeWithErasure,5 * and the reconstruction pipeline with adversarial inputs: tampered chunks,6 * wrong verify codes, parity-only sets, out-of-range indices, malformed7 * envelopes. Parameterized for >=200 tests.8 */910import { beforeEach, describe, expect, it } from "vitest";11import { deflate } from "pako";1213import {14  CodexError,15  CHUNK_SIZE,16  computeParityShards,17  deriveShardCounts,18  type ChunkEnvelope,19  type CodexPayload,20} from "../../shared/codex-types";21import {22  bytesToBase64Url,23  buildChunkEnvelope,24  sha256Hex,25  verifyChunkIntegrity,26} from "./helpers";27import { encodeWithErasure, decodeWithErasure } from "./erasure";28import { _resetReedSolomonCacheForTest } from "./rs-loader";29import { ChunkBuffer, parseRawScan, reconstructAndVerify } from "./restore";3031beforeEach(() => {32  _resetReedSolomonCacheForTest();33});3435const VERIFY_CODE = "PX-TEST-1234";3637async function makeEnv(opts: {38  index: number;39  total: number;40  isParity?: boolean;41  verifyCode?: string;42  bytes?: Uint8Array;43}): Promise<ChunkEnvelope> {44  return buildChunkEnvelope(45    opts.bytes ?? new Uint8Array([1, 2, 3, 4, 5]),46    opts.index,47    opts.total,48    opts.isParity ?? false,49    opts.verifyCode ?? VERIFY_CODE50  );51}5253async function makePayload(seedLength = 12_000): Promise<CodexPayload> {54  const blobBytes = new Uint8Array(seedLength);55  let s = 0x9e3779b9;56  for (let i = 0; i < seedLength; i++) {57    s = (s ^ (s << 13)) >>> 0;58    s = (s ^ (s >>> 17)) >>> 0;59    s = (s ^ (s << 5)) >>> 0;60    blobBytes[i] = s & 0xff;61  }62  const vaultBlob = bytesToBase64Url(blobBytes);63  const vaultFingerprint = (64    await sha256Hex(new TextEncoder().encode(vaultBlob))65  )66    .slice(0, 32)67    .toUpperCase();68  return {69    version: 2,70    verifyCode: VERIFY_CODE,71    vaultBlob,72    timestamp: 1_700_000_000_000,73    entryCount: 42,74    vaultFingerprint,75    generatedAt: 1_700_000_001_000,76  };77}7879async function buildEnvelopesForPayload(80  payload: CodexPayload81): Promise<ChunkEnvelope[]> {82  const json = JSON.stringify(payload);83  const compressed = deflate(new TextEncoder().encode(json), { level: 9 });84  const { dataChunks, parityChunks, totalChunks } =85    await encodeWithErasure(compressed);86  const all = [...dataChunks, ...parityChunks];87  return Promise.all(88    all.map((bytes, i) =>89      buildChunkEnvelope(90        bytes,91        i,92        totalChunks,93        i >= dataChunks.length,94        payload.verifyCode95      )96    )97  );98}99100// ── parseRawScan adversarial ────────────────────────────────────────────────101102describe("parseRawScan adversarial", () => {103  const garbageInputs = [104    "",105    " ",106    "\n",107    "not json at all",108    "{not_valid_json",109    "null",110    "true",111    "42",112    '"a string"',113    "[]",114    "[1,2,3]",115  ];116  for (const raw of garbageInputs) {117    it(`returns parse_error for ${JSON.stringify(raw)}`, () => {118      const result = parseRawScan(raw);119      expect(result).toHaveProperty("error");120    });121  }122123  const wrongVersionInputs = [124    JSON.stringify({ v: 1, c: VERIFY_CODE, i: 0, t: 5 }),125    JSON.stringify({ v: 4, c: VERIFY_CODE, i: 0, t: 5 }),126    JSON.stringify({ v: 0, c: VERIFY_CODE, i: 0, t: 5 }),127    JSON.stringify({ v: "2", c: VERIFY_CODE, i: 0, t: 5 }),128    JSON.stringify({ v: -1 }),129    JSON.stringify({ v: 99 }),130  ];131  for (const raw of wrongVersionInputs) {132    it(`reports wrong_version for ${JSON.stringify(raw).slice(0, 40)}`, () => {133      const result = parseRawScan(raw);134      expect(result).toMatchObject({ error: "wrong_version" });135    });136  }137138  const missingFieldInputs = [139    JSON.stringify({ v: 3 }),140    JSON.stringify({ v: 3, c: VERIFY_CODE }),141    JSON.stringify({ v: 3, c: VERIFY_CODE, i: 0 }),142    JSON.stringify({ v: 3, c: VERIFY_CODE, i: 0, t: 5 }),143    JSON.stringify({ v: 3, c: VERIFY_CODE, i: 0, t: 5, p: false }),144    JSON.stringify({ v: 3, c: VERIFY_CODE, i: 0, t: 5, p: false, d: "x" }),145  ];146  for (const raw of missingFieldInputs) {147    it(`parse_error for missing fields: ${raw.slice(0, 60)}`, () => {148      const result = parseRawScan(raw);149      expect(result).toMatchObject({ error: "parse_error" });150    });151  }152153  it("returns envelope for a valid envelope JSON", async () => {154    const env = await makeEnv({ index: 0, total: 5 });155    const result = parseRawScan(JSON.stringify(env));156    expect(result).toMatchObject({ v: 3, c: VERIFY_CODE, i: 0, t: 5 });157  });158159  it("extra unknown fields do not break parseRawScan", async () => {160    const env = await makeEnv({ index: 0, total: 5 });161    const raw = JSON.stringify({ ...env, extra: "field", more: 42 });162    const result = parseRawScan(raw);163    expect(result).toMatchObject({ v: 3, c: VERIFY_CODE });164  });165});166167// ── ChunkBuffer adversarial ────────────────────────────────────────────────168169describe("ChunkBuffer adversarial", () => {170  it("foreign verifyCode is rejected as foreign_codex", async () => {171    const buf = new ChunkBuffer();172    await buf.add(await makeEnv({ index: 0, total: 5 }));173    const res = await buf.add(174      await makeEnv({ index: 1, total: 5, verifyCode: "PX-OTHR-XXXX" })175    );176    expect(res).toEqual({ accepted: false, reason: "foreign_codex" });177  });178179  it("malformed hash length is rejected as parse_error before hashing", async () => {180    // PB-26-207: `h` must be exactly 32 chars; a short hash is malformed, so181    // ChunkBuffer.add rejects it before base64 conversion or hashing.182    const buf = new ChunkBuffer();183    const env = await makeEnv({ index: 0, total: 5 });184    const tampered = { ...env, h: "00000000" };185    const res = await buf.add(tampered);186    expect(res).toEqual({ accepted: false, reason: "parse_error" });187  });188189  it("data tampered without rehashing is rejected", async () => {190    const buf = new ChunkBuffer();191    const env = await makeEnv({ index: 0, total: 5 });192    const tampered: ChunkEnvelope = {193      ...env,194      d: bytesToBase64Url(new Uint8Array([99, 99, 99])),195    };196    const res = await buf.add(tampered);197    expect(res).toEqual({ accepted: false, reason: "bad_integrity" });198  });199200  it("wrong envelope version is rejected as parse_error", async () => {201    const buf = new ChunkBuffer();202    const env = await makeEnv({ index: 0, total: 5 });203    const wrong = { ...env, v: 1 } as unknown as ChunkEnvelope;204    const res = await buf.add(wrong);205    expect(res).toEqual({ accepted: false, reason: "parse_error" });206  });207208  it("duplicate chunk index is dedup'd", async () => {209    const buf = new ChunkBuffer();210    const env = await makeEnv({ index: 2, total: 5 });211    expect((await buf.add(env)).accepted).toBe(true);212    const second = await buf.add(env);213    expect(second).toEqual({ accepted: false, reason: "duplicate" });214    expect(buf.size()).toBe(1);215  });216217  for (const idx of [0, 1, 2, 3, 4]) {218    it(`accepts chunk at index ${idx}`, async () => {219      const buf = new ChunkBuffer();220      const env = await makeEnv({ index: idx, total: 5 });221      const res = await buf.add(env);222      expect(res.accepted).toBe(true);223    });224  }225226  for (const total of [3, 5, 7, 10, 20]) {227    it(`context dataShards matches deriveShardCounts at total=${total}`, async () => {228      const buf = new ChunkBuffer();229      await buf.add(await makeEnv({ index: 0, total }));230      const ctx = buf.getContext();231      expect(ctx?.dataShards).toBe(deriveShardCounts(total).dataShards);232    });233  }234235  it("reconstructAndVerify on empty buffer throws CodexError", async () => {236    const buf = new ChunkBuffer();237    await expect(reconstructAndVerify(buf)).rejects.toThrow(CodexError);238  });239240  // PCH-01-010: computeParityShards saturates at 35, so without an upper bound on241  // dataShards the inversion accepts absurd totals and wedges a stripe.242  it("derive_shard_counts_rejects_absurd_total", () => {243    expect(() => deriveShardCounts(1_000_000_000)).toThrow(CodexError);244  });245246  it("derive_shard_counts_accepts_real_max", () => {247    // dataShards = 200 (MAX_DATA_SHARDS_PER_STRIPE) with its saturated parity (35).248    const total = 200 + computeParityShards(200);249    const counts = deriveShardCounts(total);250    expect(counts.dataShards).toBe(200);251  });252253  // PCH-01-011: a crafted stripe count S must be rejected before it pins254  // this.stripeCount and drives O(S) allocation on the popup's render path.255  it("chunk_buffer_rejects_oversized_stripe_count", async () => {256    const buf = new ChunkBuffer();257    const env = await buildChunkEnvelope(new Uint8Array([1, 2, 3, 4, 5]), 0, 5, false, VERIFY_CODE, 0, 2 ** 31);258    const res = await buf.add(env);259    expect(res).toEqual({ accepted: false, reason: "parse_error" });260    expect(buf.getContext()).toBeNull();261  });262263  it("chunk_buffer_accepts_max_stripe_count", async () => {264    const buf = new ChunkBuffer();265    const env = await buildChunkEnvelope(new Uint8Array([1, 2, 3, 4, 5]), 0, 5, false, VERIFY_CODE, 0, 64);266    const res = await buf.add(env);267    expect(res.accepted).toBe(true);268  });269270  // PCH-01-012: fractional i/t/s/S must be rejected by parseRawScan (NaN/Infinity271  // are not JSON-representable, so fractional is the reachable hostile case).272  it("parse_raw_scan_rejects_non_integer_index", async () => {273    const env = await makeEnv({ index: 0, total: 5 });274    expect(parseRawScan(JSON.stringify({ ...env, i: 1.5 }))).toEqual({ error: "parse_error" });275    expect(parseRawScan(JSON.stringify({ ...env, t: 5.5 }))).toEqual({ error: "parse_error" });276    expect(parseRawScan(JSON.stringify({ ...env, s: 0.5 }))).toEqual({ error: "parse_error" });277    expect(parseRawScan(JSON.stringify({ ...env, S: 1.5 }))).toEqual({ error: "parse_error" });278  });279280  // PCH-01-026 #2: ChunkBuffer.add must never throw — an out-of-alphabet base64281  // payload becomes a parse_error rejection.282  it("chunk_buffer_add_does_not_throw_on_invalid_base64", async () => {283    const buf = new ChunkBuffer();284    const env = await makeEnv({ index: 0, total: 5 });285    const res = await buf.add({ ...env, d: "!!!@@" });286    expect(res).toEqual({ accepted: false, reason: "parse_error" });287    expect(buf.getContext()).toBeNull();288  });289290  // PCH-01-026 #7: an out-of-range index is malformed (parse_error), never291  // mis-reported as a duplicate.292  it("chunk_buffer_out_of_range_index_reports_parse_error", async () => {293    const buf = new ChunkBuffer();294    const env = await makeEnv({ index: 5, total: 5 }); // i === t, one past the end295    const res = await buf.add(env);296    expect(res).toEqual({ accepted: false, reason: "parse_error" });297  });298});299300// ── verifyChunkIntegrity adversarial ────────────────────────────────────────301302describe("verifyChunkIntegrity adversarial", () => {303  it("correct envelope verifies true", async () => {304    const env = await makeEnv({ index: 0, total: 5 });305    expect(await verifyChunkIntegrity(env)).toBe(true);306  });307308  const corruptHashes = [309    "00000000",310    "FFFFFFFF",311    "ZZZZZZZZ",312    "abcdef12",313    "",314  ];315  for (const h of corruptHashes) {316    it(`tampered hash ${JSON.stringify(h)} verifies false`, async () => {317      const env = await makeEnv({ index: 0, total: 5 });318      const tampered = { ...env, h };319      expect(await verifyChunkIntegrity(tampered)).toBe(false);320    });321  }322323  for (let i = 0; i < 5; i++) {324    it(`random data tamper #${i} verifies false`, async () => {325      const env = await makeEnv({ index: 0, total: 5 });326      const bytes = new Uint8Array([327        Math.floor(Math.random() * 256),328        Math.floor(Math.random() * 256),329        Math.floor(Math.random() * 256),330      ]);331      const tampered = { ...env, d: bytesToBase64Url(bytes) };332      // The new data hash will almost certainly differ from env.h.333      const recomputed = (await sha256Hex(bytes)).slice(0, 16);334      if (recomputed === env.h) return; // 1-in-2^64 lottery; skip.335      expect(await verifyChunkIntegrity(tampered)).toBe(false);336    });337  }338});339340// ── encodeWithErasure / decodeWithErasure adversarial ───────────────────────341342describe("encodeWithErasure / decodeWithErasure adversarial", () => {343  it("encoding empty input still produces at least 1 data shard", async () => {344    const { dataChunks, parityChunks, totalChunks, dataShards, parityShards } =345      await encodeWithErasure(new Uint8Array());346    expect(dataShards).toBeGreaterThanOrEqual(1);347    expect(parityShards).toBe(computeParityShards(dataShards));348    expect(totalChunks).toBe(dataShards + parityShards);349    expect(dataChunks.length).toBe(dataShards);350    expect(parityChunks.length).toBe(parityShards);351  });352353  for (const size of [1, 100, 1023, 1024, 1025, 5000, 10000]) {354    it(`encode/decode round-trips data of size ${size}`, async () => {355      const data = crypto.getRandomValues(new Uint8Array(size));356      const { dataChunks, parityChunks, dataShards, totalChunks } =357        await encodeWithErasure(data);358      const map = new Map<number, Uint8Array>();359      [...dataChunks, ...parityChunks].forEach((c, i) => map.set(i, c));360      const decoded = await decodeWithErasure(map, dataShards, totalChunks);361      expect(decoded.length).toBe(dataShards * CHUNK_SIZE);362      // First `size` bytes of decoded should equal original.363      for (let i = 0; i < size; i++) {364        expect(decoded[i]).toBe(data[i]);365      }366    });367  }368369  it("decoding with empty chunk map throws CodexError", async () => {370    await expect(371      decodeWithErasure(new Map(), 5, 7)372    ).rejects.toThrow(CodexError);373  });374375  it("decoding with fewer chunks than dataShards throws CodexError", async () => {376    const data = crypto.getRandomValues(new Uint8Array(5000));377    const { dataChunks, dataShards, totalChunks } = await encodeWithErasure(378      data379    );380    // Use only one data chunk — less than dataShards.381    const map = new Map<number, Uint8Array>();382    map.set(0, dataChunks[0]!);383    await expect(384      decodeWithErasure(map, dataShards, totalChunks)385    ).rejects.toThrow(CodexError);386  });387388  it("chunk index out of range throws CodexError", async () => {389    const data = crypto.getRandomValues(new Uint8Array(2048));390    const { dataChunks, dataShards, totalChunks } = await encodeWithErasure(391      data392    );393    const map = new Map<number, Uint8Array>();394    map.set(999, dataChunks[0]!);395    await expect(396      decodeWithErasure(map, dataShards, totalChunks)397    ).rejects.toThrow(CodexError);398  });399400  it("chunk with wrong size throws CodexError", async () => {401    const data = crypto.getRandomValues(new Uint8Array(2048));402    const { dataChunks, dataShards, totalChunks } = await encodeWithErasure(403      data404    );405    const map = new Map<number, Uint8Array>();406    // Stuff a wrong-size chunk at index 0.407    map.set(0, new Uint8Array(100));408    for (let i = 1; i < dataShards; i++) map.set(i, dataChunks[i]!);409    await expect(410      decodeWithErasure(map, dataShards, totalChunks)411    ).rejects.toThrow(CodexError);412  });413414  it("dropping 1 data chunk (within parity budget) reconstructs", async () => {415    const data = crypto.getRandomValues(new Uint8Array(8000));416    const { dataChunks, parityChunks, dataShards, totalChunks } =417      await encodeWithErasure(data);418    const map = new Map<number, Uint8Array>();419    // Drop dataChunks[0]; rely on parity.420    for (let i = 1; i < dataShards; i++) map.set(i, dataChunks[i]!);421    parityChunks.forEach((c, j) => map.set(dataShards + j, c));422    const decoded = await decodeWithErasure(map, dataShards, totalChunks);423    expect(decoded.length).toBe(dataShards * CHUNK_SIZE);424    for (let i = 0; i < data.length; i++) {425      expect(decoded[i]).toBe(data[i]);426    }427  });428429  it("dropping all data chunks (only parity present) fails when k < dataShards", async () => {430    const data = crypto.getRandomValues(new Uint8Array(8000));431    const { parityChunks, dataShards, totalChunks } = await encodeWithErasure(432      data433    );434    // Only parity present; with parity < dataShards the decoder must refuse.435    if (parityChunks.length >= dataShards) return;436    const map = new Map<number, Uint8Array>();437    parityChunks.forEach((c, j) => map.set(dataShards + j, c));438    await expect(439      decodeWithErasure(map, dataShards, totalChunks)440    ).rejects.toThrow(CodexError);441  });442443  it("dropping too many data chunks (more than parity) fails cleanly", async () => {444    const data = crypto.getRandomValues(new Uint8Array(20000));445    const { dataChunks, parityChunks, dataShards, parityShards, totalChunks } =446      await encodeWithErasure(data);447    if (dataShards <= parityShards + 1) return; // need enough shards to test.448    const map = new Map<number, Uint8Array>();449    // Drop parityShards + 1 data chunks (one more than the budget allows).450    for (let i = parityShards + 1; i < dataShards; i++) {451      map.set(i, dataChunks[i]!);452    }453    parityChunks.forEach((c, j) => map.set(dataShards + j, c));454    await expect(455      decodeWithErasure(map, dataShards, totalChunks)456    ).rejects.toThrow(CodexError);457  });458});459460// ── reconstructAndVerify with full pipeline ────────────────────────────────461462describe("reconstructAndVerify adversarial", () => {463  it("round-trips a payload via ChunkBuffer + reconstructAndVerify", async () => {464    const payload = await makePayload(2000);465    const envs = await buildEnvelopesForPayload(payload);466    const buf = new ChunkBuffer();467    for (const e of envs) await buf.add(e);468    const reconstructed = await reconstructAndVerify(buf);469    expect(reconstructed.vaultFingerprint).toBe(payload.vaultFingerprint);470    expect(reconstructed.entryCount).toBe(payload.entryCount);471  });472473  // PCH-01-013: the declared fingerprint length is attacker-controlled. An474  // empty fingerprint must not pass via slice(0,0) === "".475  it("reconstruct_rejects_empty_fingerprint", async () => {476    const base = await makePayload(2000);477    const payload = { ...base, vaultFingerprint: "" };478    const envs = await buildEnvelopesForPayload(payload);479    const buf = new ChunkBuffer();480    for (const e of envs) await buf.add(e);481    await expect(reconstructAndVerify(buf)).rejects.toThrow(CodexError);482  });483484  it("reconstruct_accepts_current_fingerprint_length", async () => {485    const base = await makePayload(2500);486    const fp32 = (await sha256Hex(new TextEncoder().encode(base.vaultBlob))).slice(0, 32).toUpperCase();487    const payload = { ...base, vaultFingerprint: fp32 };488    const envs = await buildEnvelopesForPayload(payload);489    const buf = new ChunkBuffer();490    for (const e of envs) await buf.add(e);491    const reconstructed = await reconstructAndVerify(buf);492    expect(reconstructed.vaultFingerprint).toBe(fp32);493  });494495  it("missing chunks but enough parity still reconstructs", async () => {496    const payload = await makePayload(5000);497    const envs = await buildEnvelopesForPayload(payload);498    if (envs.length <= 3) return; // need room to drop one and still have parity499    const buf = new ChunkBuffer();500    // Skip the first data chunk (index 0). Fill the rest.501    for (let i = 1; i < envs.length; i++) await buf.add(envs[i]!);502    const reconstructed = await reconstructAndVerify(buf);503    expect(reconstructed.vaultFingerprint).toBe(payload.vaultFingerprint);504  });505506  it("tampered chunk data (after acceptance) does not round-trip", async () => {507    const payload = await makePayload(3000);508    const envs = await buildEnvelopesForPayload(payload);509    const buf = new ChunkBuffer();510    // Tamper one envelope's `d` field by replacing with random bytes plus511    // recomputed hash so the integrity check passes.512    const evil = envs[0]!;513    const fakeBytes = crypto.getRandomValues(new Uint8Array(CHUNK_SIZE));514    const fakeHash = (await sha256Hex(fakeBytes)).slice(0, 32);515    const fakeEnv: ChunkEnvelope = {516      ...evil,517      d: bytesToBase64Url(fakeBytes),518      h: fakeHash,519    };520    await buf.add(fakeEnv);521    for (let i = 1; i < envs.length; i++) await buf.add(envs[i]!);522    // Reconstruction should fail fingerprint check.523    await expect(reconstructAndVerify(buf)).rejects.toThrow(CodexError);524  });525});526527// ── Aztec-decoded raw payload adversarial ──────────────────────────────────528529describe("parseRawScan with adversarial raw strings (no-crash)", () => {530  const inputs = [531    "{}",532    '{"a":1,"b":2}',533    '{"v":2}',534    '{"v":2,"c":""}',535    '{"v":2,"c":"' + "A".repeat(10000) + '"}',536    '{"v":2,"c":"PX-XXXX-XXXX","i":-1,"t":5,"p":false,"d":"","h":""}',537    '{"v":2,"c":"PX-XXXX-XXXX","i":99,"t":5,"p":false,"d":"","h":""}',538    '{"v":2,"c":"PX-XXXX-XXXX","i":0,"t":-5,"p":false,"d":"","h":""}',539    '{"v":2,"c":"PX-XXXX-XXXX","i":0,"t":5,"p":"yes","d":"","h":""}',540    '{"v":2,"c":"PX-XXXX-XXXX","i":0,"t":5,"p":false,"d":null,"h":null}',541  ];542  for (const raw of inputs) {543    it(`parseRawScan does not crash on ${raw.slice(0, 50)}`, () => {544      const result = parseRawScan(raw);545      expect(result).toBeDefined();546    });547  }548});549550// ── Verify code matching ────────────────────────────────────────────────────551552describe("ChunkBuffer verify code adversarial", () => {553  const foreignCodes = [554    "PX-OTHR-1234",555    "PX-AAAA-AAAA",556    "PX-1111-1111",557    "PX-9999-9999",558    "PX-ABCD-EFGH",559  ];560  for (const code of foreignCodes) {561    it(`buffer rejects foreign code ${code}`, async () => {562      const buf = new ChunkBuffer();563      await buf.add(await makeEnv({ index: 0, total: 5 }));564      const foreign = await makeEnv({ index: 1, total: 5, verifyCode: code });565      const res = await buf.add(foreign);566      expect(res.accepted).toBe(false);567      expect(res.reason).toBe("foreign_codex");568    });569  }570});571572// ── Round-trip fuzz across data sizes ───────────────────────────────────────573574describe("erasure round-trip size sweep", () => {575  for (const size of [576    0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1023, 1024, 1025, 1500, 2048,577    3000, 4096, 5000, 7500, 10000,578  ]) {579    it(`size ${size} round-trips correctly`, async () => {580      const data = crypto.getRandomValues(new Uint8Array(size));581      const { dataChunks, parityChunks, dataShards, totalChunks } =582        await encodeWithErasure(data);583      const map = new Map<number, Uint8Array>();584      [...dataChunks, ...parityChunks].forEach((c, i) => map.set(i, c));585      const decoded = await decodeWithErasure(map, dataShards, totalChunks);586      for (let i = 0; i < size; i++) {587        expect(decoded[i]).toBe(data[i]);588      }589    });590591    it(`size ${size} drop-first-parity still decodes`, async () => {592      const data = crypto.getRandomValues(new Uint8Array(size));593      const { dataChunks, dataShards, totalChunks } = await encodeWithErasure(594        data595      );596      const map = new Map<number, Uint8Array>();597      dataChunks.forEach((c, i) => map.set(i, c));598      // No parity present, but all data; decode succeeds.599      const decoded = await decodeWithErasure(map, dataShards, totalChunks);600      for (let i = 0; i < size; i++) {601        expect(decoded[i]).toBe(data[i]);602      }603    });604  }605});606607// ── Boundary: many envelopes ────────────────────────────────────────────────608609describe("ChunkBuffer scale", () => {610  for (const total of [3, 5, 10]) {611    const dataShards = deriveShardCounts(total).dataShards;612    it(`buffer accepts ${dataShards} data chunks and reports ready`, async () => {613      const buf = new ChunkBuffer();614      for (let i = 0; i < dataShards; i++) {615        const res = await buf.add(await makeEnv({ index: i, total }));616        expect(res.accepted).toBe(true);617      }618      expect(buf.isReady()).toBe(true);619    });620  }621});622623// ── AUD-004: 128-bit integrity prefixes ────────────────────────────────────624625describe("Codex integrity prefix length (AUD-004)", () => {626  it("chunk hash is 32 hex chars (128 bits)", async () => {627    const bytes = new Uint8Array([1, 2, 3, 4, 5]);628    const env = await buildChunkEnvelope(bytes, 0, 10, false, "PX-TEST-1234");629    expect(env.h).toHaveLength(32);630    expect(env.h).toMatch(/^[0-9A-F]{32}$/);631  });632633  it("verifyChunkIntegrity passes for current 32-char hash", async () => {634    const bytes = new Uint8Array([1, 2, 3]);635    const env = await buildChunkEnvelope(bytes, 0, 5, false, "PX-TEST-1234");636    expect(env.h).toHaveLength(32);637    expect(await verifyChunkIntegrity(env)).toBe(true);638  });639640  it("verifyChunkIntegrity rejects tampered 32-char hash", async () => {641    const bytes = new Uint8Array([1, 2, 3]);642    const env = await buildChunkEnvelope(bytes, 0, 5, false, "PX-TEST-1234");643    const tampered = { ...env, h: "X".repeat(32) };644    expect(await verifyChunkIntegrity(tampered)).toBe(false);645  });646647});