1import { beforeEach, describe, expect, it, vi } from "vitest";2import {3 CHUNK_SIZE,4 CodexError,5 computeParityShards,6 STORAGE_KEY_VERIFY_CODE,7 type ChunkEnvelope,8 type CodexDeps,9} from "../../shared/codex-types";10import type { VaultEntry } from "../../shared/types";11import {12 base64UrlToBytes,13 buildChunkEnvelope,14 bytesToBase64Url,15 sha256Hex,16 verifyChunkIntegrity,17} from "./helpers";18import {19 decodeWithErasure,20 encodeIntoStripes,21 encodeWithErasure,22} from "./erasure";23import { buildAztecMatrix, generateVerifiedAztec, verifyMatrixRoundTrip } from "./aztec";24import { generateCodex } from "./generate";25262728function installChromeStorageMock(): { storage: Record<string, unknown> } {29 const storage: Record<string, unknown> = {};30 const chromeMock = {31 storage: {32 local: {33 get: vi.fn(async (key?: string | string[]) => {34 if (!key) return { ...storage };35 if (typeof key === "string") return { [key]: storage[key] };36 return key.reduce<Record<string, unknown>>((acc, k) => {37 acc[k] = storage[k];38 return acc;39 }, {});40 }),41 set: vi.fn(async (entries: Record<string, unknown>) => {42 Object.assign(storage, entries);43 }),44 remove: vi.fn(async (k: string | string[]) => {45 const keys = Array.isArray(k) ? k : [k];46 for (const key of keys) delete storage[key];47 }),48 },49 },50 51 52 53 54 runtime: {},55 };56 57 globalThis.chrome = chromeMock;58 return { storage };59}60616263function makeDeps(overrides: Partial<CodexDeps> = {}): CodexDeps {64 65 66 67 68 69 70 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";71 72 73 let seed = 0x1234abcd;74 const slotALen = 8192;75 const chars: string[] = new Array(slotALen);76 for (let i = 0; i < slotALen; i++) {77 seed = (seed * 1103515245 + 12345) >>> 0;78 chars[i] = alphabet[(seed >>> 16) & 0x3f]!;79 }80 const slotA = "ENCRYPTED::" + chars.join("");81 const slotB: string | null = null;82 const entries: VaultEntry[] = [83 { id: "1", title: "GitHub", username: "alice", password: "secret", createdAt: 1, modifiedAt: 2 },84 { id: "2", title: "Email", username: "bob", password: "hunter2", createdAt: 1, modifiedAt: 2 },85 { id: "3", title: "Bank", username: "carol", password: "money", createdAt: 1, modifiedAt: 2 },86 ];8788 return {89 getSlotA: async () => slotA,90 getSlotB: async () => slotB,91 getLastModified: async () => "1700000000000",92 getAllEntries: async () => entries,93 getVerifyCode: async () => "PX-TEST-CODE",94 isVaultUnlocked: () => true,95 ...overrides,96 };97}9899100101describe("helpers", () => {102 beforeEach(() => {103 installChromeStorageMock();104 });105106 it("base64url round-trips arbitrary binary", () => {107 const original = new Uint8Array([0, 1, 2, 250, 251, 252, 253, 254, 255, 0x3f, 0x2b, 0x2f]);108 const encoded = bytesToBase64Url(original);109 expect(encoded).not.toMatch(/[+/=]/);110 const decoded = base64UrlToBytes(encoded);111 expect(Array.from(decoded)).toEqual(Array.from(original));112 });113114 it("sha256Hex returns uppercase 64-char hex for known input", async () => {115 const empty = new Uint8Array();116 const hex = await sha256Hex(empty);117 expect(hex).toBe("E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855");118 });119120 it("buildChunkEnvelope + verifyChunkIntegrity agree", async () => {121 const chunk = new Uint8Array(CHUNK_SIZE).fill(0x42);122 const env = await buildChunkEnvelope(chunk, 3, 7, false, "PX-AAAA-BBBB");123 expect(env.v).toBe(3);124 expect(env.i).toBe(3);125 expect(env.t).toBe(7);126 expect(env.p).toBe(false);127 expect(env.c).toBe("PX-AAAA-BBBB");128 expect(env.h).toHaveLength(32); 129 expect(await verifyChunkIntegrity(env)).toBe(true);130 });131132 it("verifyChunkIntegrity rejects a corrupted envelope", async () => {133 const chunk = new Uint8Array(CHUNK_SIZE).fill(0x42);134 const env = await buildChunkEnvelope(chunk, 0, 4, false, "PX-AAAA-BBBB");135 136 const tampered = { ...env, d: bytesToBase64Url(new Uint8Array(CHUNK_SIZE).fill(0x43)) };137 expect(await verifyChunkIntegrity(tampered)).toBe(false);138 });139});140141describe("erasure", () => {142 beforeEach(() => {143 installChromeStorageMock();144 });145146 it("round-trips with the full chunk set", async () => {147 148 const original = new Uint8Array(CHUNK_SIZE * 3 + 17);149 for (let i = 0; i < original.length; i++) original[i] = (i * 31) & 0xff;150151 const { dataChunks, parityChunks, dataShards, parityShards, totalChunks } =152 await encodeWithErasure(original);153 expect(dataShards).toBe(4);154 expect(parityShards).toBe(computeParityShards(dataShards));155 expect(parityChunks.length).toBe(parityShards);156 expect(totalChunks).toBe(dataShards + parityShards);157158 const present = new Map<number, Uint8Array>();159 dataChunks.forEach((c, i) => present.set(i, c));160 parityChunks.forEach((c, i) => present.set(dataShards + i, c));161162 const recovered = await decodeWithErasure(present, dataShards, totalChunks);163 for (let i = 0; i < original.length; i++) {164 expect(recovered[i]).toBe(original[i]);165 }166 });167168 it("reconstructs after losing two random data chunks", async () => {169 const original = new Uint8Array(CHUNK_SIZE * 4);170 for (let i = 0; i < original.length; i++) original[i] = (i * 7 + 5) & 0xff;171172 const { dataChunks, parityChunks, dataShards, totalChunks } = await encodeWithErasure(original);173 const present = new Map<number, Uint8Array>();174 dataChunks.forEach((c, i) => present.set(i, c));175 parityChunks.forEach((c, i) => present.set(dataShards + i, c));176177 178 present.delete(0);179 present.delete(2);180181 const recovered = await decodeWithErasure(present, dataShards, totalChunks);182 for (let i = 0; i < original.length; i++) {183 expect(recovered[i]).toBe(original[i]);184 }185 });186187 it("throws INSUFFICIENT_CHUNKS when too many are missing", async () => {188 const original = new Uint8Array(CHUNK_SIZE * 4);189 const { dataChunks, parityChunks, dataShards, totalChunks } = await encodeWithErasure(original);190 const present = new Map<number, Uint8Array>();191 dataChunks.forEach((c, i) => present.set(i, c));192 parityChunks.forEach((c, i) => present.set(dataShards + i, c));193194 195 present.delete(0);196 present.delete(1);197 present.delete(2);198199 await expect(decodeWithErasure(present, dataShards, totalChunks)).rejects.toMatchObject({200 name: "CodexError",201 code: "INSUFFICIENT_CHUNKS",202 });203 });204});205206describe("generateCodex", () => {207 beforeEach(() => {208 installChromeStorageMock();209 });210211 it("produces a non-trivial PDF and a coherent manifest", async () => {212 const deps = makeDeps();213 const { pdfBytes, manifest } = await generateCodex(deps);214215 expect(pdfBytes.byteLength).toBeGreaterThan(1000);216 217 expect(pdfBytes[0]).toBe(0x25); 218 expect(pdfBytes[1]).toBe(0x50); 219 expect(pdfBytes[2]).toBe(0x44); 220 expect(pdfBytes[3]).toBe(0x46); 221222 expect(manifest.verifyCode).toBe("PX-TEST-CODE");223 expect(manifest.entryCount).toBe(3);224 expect(manifest.parityChunks).toBe(computeParityShards(manifest.dataChunks));225 expect(manifest.totalChunks).toBe(manifest.dataChunks + manifest.parityChunks);226 expect(manifest.pageCount).toBeGreaterThanOrEqual(1);227 expect(manifest.vaultFingerprint).toMatch(/^[0-9A-F]{32}$/); 228 }, 60_000);229230 it("rejects generation when the vault is locked", async () => {231 const deps = makeDeps({ isVaultUnlocked: () => false });232 await expect(generateCodex(deps)).rejects.toMatchObject({233 name: "CodexError",234 code: "VAULT_LOCKED",235 });236 });237238 it("reports progress at named milestones", async () => {239 const deps = makeDeps();240 const steps: string[] = [];241 await generateCodex(deps, (step) => {242 if (!steps.includes(step)) steps.push(step);243 });244 expect(steps).toEqual(["compress", "erasure", "aztec", "verify", "pdf"]);245 }, 60_000);246247 it("persists the storage_key_verify_code constant correctly", () => {248 249 250 expect(STORAGE_KEY_VERIFY_CODE).toBe("peach_codex_verify_code");251 expect(CodexError).toBeTypeOf("function");252 });253});254255describe("parity scaling", () => {256 beforeEach(() => {257 installChromeStorageMock();258 });259260 it("computeParityShards returns minimum 2 for very small vaults", () => {261 expect(computeParityShards(1)).toBe(2);262 expect(computeParityShards(10)).toBe(2);263 expect(computeParityShards(13)).toBe(2);264 });265266 it("computeParityShards scales at 15% for medium vaults", () => {267 expect(computeParityShards(56)).toBe(9); 268 expect(computeParityShards(100)).toBe(15);269 expect(computeParityShards(213)).toBe(32);270 });271272 it("computeParityShards caps at 35 for large vaults", () => {273 expect(computeParityShards(600)).toBe(35);274 expect(computeParityShards(660)).toBe(35);275 expect(computeParityShards(1000)).toBe(35);276 });277278 it("encodeWithErasure returns scaled parityShards", async () => {279 const data = new Uint8Array(50_000);280 for (let i = 0; i < data.length; i++) data[i] = (i * 37) & 0xff;281 const { dataShards, parityShards } = await encodeWithErasure(data);282 expect(parityShards).toBe(computeParityShards(dataShards));283 });284285 it("large vault: each stripe survives loss of up to its parityShards codes", async () => {286 287 288 289 const data = new Uint8Array(200_000);290 for (let i = 0; i < data.length; i++) data[i] = (i * 37) & 0xff;291292 const { stripes } = await encodeIntoStripes(data);293 expect(stripes.length).toBeGreaterThan(1);294295 let byteCursor = 0;296 for (const stripe of stripes) {297 const present = new Map<number, Uint8Array>();298 stripe.dataChunks.forEach((c, i) => present.set(i, c));299 stripe.parityChunks.forEach((c, i) =>300 present.set(stripe.dataShards + i, c),301 );302 303 for (let i = 0; i < stripe.parityShards; i++) present.delete(i);304305 const recovered = await decodeWithErasure(306 present,307 stripe.dataShards,308 stripe.totalChunks,309 );310 const stripeRealBytes = Math.min(311 stripe.dataShards * CHUNK_SIZE,312 Math.max(0, data.length - byteCursor),313 );314 for (let i = 0; i < stripeRealBytes; i++) {315 expect(recovered[i]).toBe(data[byteCursor + i]);316 }317 byteCursor += stripe.dataShards * CHUNK_SIZE;318 }319 }, 60_000);320321 it("large vault: fails gracefully when stripe loss exceeds parityShards", async () => {322 const data = new Uint8Array(200_000);323 for (let i = 0; i < data.length; i++) data[i] = (i * 41) & 0xff;324 const { stripes } = await encodeIntoStripes(data);325 expect(stripes.length).toBeGreaterThan(1);326 const stripe = stripes[0]!;327 const present = new Map<number, Uint8Array>();328 stripe.dataChunks.forEach((c, i) => present.set(i, c));329 stripe.parityChunks.forEach((c, i) =>330 present.set(stripe.dataShards + i, c),331 );332 for (let i = 0; i < stripe.parityShards + 1; i++) present.delete(i);333334 await expect(335 decodeWithErasure(present, stripe.dataShards, stripe.totalChunks),336 ).rejects.toThrow(CodexError);337 }, 60_000);338});339340341342describe("aztec round-trip", () => {343 beforeEach(() => {344 installChromeStorageMock();345 });346347 it("buildAztecMatrix + verifyMatrixRoundTrip: 200 varied payloads all pass", () => {348 349 350 351 352 for (let i = 0; i < 200; i++) {353 354 355 const data = new Uint8Array(CHUNK_SIZE);356 for (let j = 0; j < CHUNK_SIZE; j++) data[j] = ((i * 37 + j * 7) ^ (i << 3)) & 0xff;357358 359 360 361 const b64 = btoa(String.fromCharCode(...data.slice(0, 60))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");362 const payload = JSON.stringify({363 v: 3, i, t: 200, p: i >= 188, c: "PX-TEST-CODE",364 d: b64,365 h: "abcdef1234567890abcdef1234567890",366 s: 0, st: 1,367 });368369 const matrix = buildAztecMatrix(payload);370 const ok = verifyMatrixRoundTrip(matrix, payload);371 if (!ok) {372 throw new Error(`verifyMatrixRoundTrip failed for chunk ${i} (payload len=${payload.length})`);373 }374 }375 });376377 it("generateVerifiedAztec: 20 real envelopes survive round-trip", async () => {378 379 380 381 const envelopes: ChunkEnvelope[] = [];382 for (let i = 0; i < 20; i++) {383 const data = new Uint8Array(CHUNK_SIZE);384 for (let j = 0; j < CHUNK_SIZE; j++) data[j] = ((i * 53 + j * 11) ^ (i * 19)) & 0xff;385 envelopes.push(await buildChunkEnvelope(data, i, 20, i >= 17, "PX-RTRP-CODE", 0, 1));386 }387388 for (const env of envelopes) {389 const img = await generateVerifiedAztec(env);390 expect(img.pngBytes.length).toBeGreaterThan(0);391 }392 }, 120_000);393394 it("generateCodex: large vault (100 KB) completes without AZTEC errors", async () => {395 396 397 installChromeStorageMock();398399 400 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";401 let seed = 0xdeadbeef;402 const chars: string[] = new Array(80_000);403 for (let i = 0; i < 80_000; i++) {404 seed = (seed * 1664525 + 1013904223) >>> 0;405 chars[i] = alphabet[(seed >>> 16) & 0x3f]!;406 }407 const slotA = "ENCRYPTED::" + chars.join("");408409 const deps: CodexDeps = {410 getSlotA: async () => slotA,411 getSlotB: async () => null,412 getLastModified: async () => "1700000000000",413 getAllEntries: async () => [414 { id: "1", title: "GitHub", username: "alice", password: "secret", createdAt: 1, modifiedAt: 2 },415 ],416 getVerifyCode: async () => "PX-LARG-TEST",417 isVaultUnlocked: () => true,418 };419420 const steps: string[] = [];421 const { pdfBytes, manifest } = await generateCodex(deps, (step) => {422 if (!steps.includes(step)) steps.push(step);423 });424425 expect(pdfBytes[0]).toBe(0x25); 426 expect(manifest.totalChunks).toBeGreaterThan(50);427 expect(steps).toEqual(["compress", "erasure", "aztec", "verify", "pdf"]);428 }, 300_000); 429});