1import { describe, expect, test } from "vitest";23import {4 bytesToUuid,5 buildPayload,6 parsePayload,7 uuidToBytes,8} from "./peachPayload";9import type { Canary, VaultEntry } from "../shared/types";10import type { VaultSettings } from "../background/vault";1112const PASS = "correct horse battery staple";1314function loginEntry(over: Partial<VaultEntry> = {}): VaultEntry {15 return {16 id: crypto.randomUUID(),17 type: "login",18 title: "github",19 username: "[email protected]",20 password: "p4ssw0rd!",21 url: "https://github.com",22 createdAt: 1700000000000,23 modifiedAt: 1700001000000,24 ...over,25 };26}2728function noteEntry(over: Partial<VaultEntry> = {}): VaultEntry {29 return {30 id: crypto.randomUUID(),31 type: "note",32 title: "private note",33 notes: "remember the milk",34 createdAt: 1700000000000,35 modifiedAt: 1700000000000,36 ...over,37 };38}3940function cardEntry(over: Partial<VaultEntry> = {}): VaultEntry {41 return {42 id: crypto.randomUUID(),43 type: "card",44 title: "personal visa",45 cardholderName: "June Asha",46 cardNumber: "4111111111111111",47 expMonth: "08",48 expYear: "2029",49 cvc: "123",50 network: "visa",51 last4: "1111",52 createdAt: 1700000000000,53 modifiedAt: 1700000000000,54 ...over,55 };56}5758function passkeyEntry(over: Partial<VaultEntry> = {}): VaultEntry {59 return {60 id: crypto.randomUUID(),61 type: "passkey",62 title: "github passkey",63 passkey: {64 rpId: "github.com",65 rpName: "GitHub",66 credentialId: "abc123",67 publicKeyCose: "a401020326...",68 privateKeyJwk: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "d" },69 userHandle: "user-handle",70 userName: "june",71 userDisplayName: "June",72 algorithm: -7,73 signCount: 5,74 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",75 backupEligible: true,76 backupState: false,77 createdAt: 1700000000000,78 },79 createdAt: 1700000000000,80 modifiedAt: 1700000000000,81 ...over,82 };83}8485describe("peachPayload", () => {86 test("UUID round-trip preserves the hyphenated string", () => {87 const id = "123e4567-e89b-42d3-a456-426614174000";88 expect(bytesToUuid(uuidToBytes(id))).toBe(id);89 });9091 test("login entry round-trips all fields", async () => {92 const canary: Canary = { webhookUrl: "https://example.com/canary", label: "alert" };93 const entry = loginEntry({94 totp: "JBSWY3DPEHPK3PXP",95 tags: ["personal", "dev"],96 notes: "second factor on yubikey",97 canary,98 breach: 4,99 passwordChangedAt: 1700000500000,100 additionalUrls: ["https://gist.github.com"],101 });102103 const env = await buildPayload({104 entries: [entry],105 settings: {},106 passphrase: PASS,107 });108 const out = await parsePayload(env, PASS);109 expect(out.entries).toHaveLength(1);110 const got = out.entries[0];111 expect(got.id).toBe(entry.id);112 expect(got.title).toBe(entry.title);113 expect(got.url).toBe(entry.url);114 expect(got.username).toBe(entry.username);115 expect(got.password).toBe(entry.password);116 expect(got.totp).toBe(entry.totp);117 expect(got.tags).toEqual(entry.tags);118 expect(got.notes).toBe(entry.notes);119 expect(got.canary).toEqual(canary);120 expect(got.breach).toBe(4);121 expect(got.additionalUrls).toEqual(entry.additionalUrls);122 expect(got.passwordChangedAt).toBe(entry.passwordChangedAt);123 expect(got.createdAt).toBe(entry.createdAt);124 expect(got.modifiedAt).toBe(entry.modifiedAt);125 });126127 test("note, card, and passkey entries round-trip", async () => {128 const note = noteEntry({ tags: ["secrets"] });129 const card = cardEntry({ cardNickname: "daily driver" });130 const passkey = passkeyEntry();131132 const env = await buildPayload({133 entries: [note, card, passkey],134 settings: {},135 passphrase: PASS,136 });137 const out = await parsePayload(env, PASS);138 expect(out.entries).toHaveLength(3);139140 const gotNote = out.entries.find((e) => e.id === note.id)!;141 expect(gotNote.type).toBe("note");142 expect(gotNote.notes).toBe(note.notes);143 expect(gotNote.tags).toEqual(["secrets"]);144145 const gotCard = out.entries.find((e) => e.id === card.id)!;146 expect(gotCard.type).toBe("card");147 expect(gotCard.cardNumber).toBe(card.cardNumber);148 expect(gotCard.expMonth).toBe(card.expMonth);149 expect(gotCard.expYear).toBe(card.expYear);150 expect(gotCard.cvc).toBe(card.cvc);151 expect(gotCard.network).toBe("visa");152 expect(gotCard.last4).toBe("1111");153 expect(gotCard.cardNickname).toBe("daily driver");154 expect(gotCard.cardholderName).toBe(card.cardholderName);155156 const gotPk = out.entries.find((e) => e.id === passkey.id)!;157 expect(gotPk.type).toBe("passkey");158 expect(gotPk.passkey?.rpId).toBe("github.com");159 expect(gotPk.passkey?.privateKeyJwk).toEqual(passkey.passkey?.privateKeyJwk);160 expect(gotPk.passkey?.algorithm).toBe(-7);161 expect(gotPk.passkey?.signCount).toBe(5);162 expect(gotPk.passkey?.backupEligible).toBe(true);163 expect(gotPk.passkey?.backupState).toBe(false);164 });165166 test("passkey-bearing login entry round-trips ALL login fields (SEV-1-B)", async () => {167 const canary: Canary = { webhookUrl: "https://example.com/canary", label: "alert" };168 const entry: VaultEntry = {169 id: crypto.randomUUID(),170 type: "login",171 title: "Google",172 username: "[email protected]",173 email: "[email protected]",174 password: "also-has-a-password",175 url: "https://google.com",176 additionalUrls: ["https://accounts.google.com"],177 totp: "JBSWY3DPEHPK3PXP",178 notes: "backup codes: 1234 5678",179 tags: ["work", "important"],180 canary,181 passwordChangedAt: 1700000000000,182 breach: 2,183 passkey: {184 rpId: "google.com",185 rpName: "Google",186 credentialId: "dGVzdC1pZA",187 publicKeyCose: "a40102032620",188 privateKeyJwk: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "d" },189 userHandle: "dGVzdA",190 userName: "[email protected]",191 userDisplayName: "Ada Lovelace",192 algorithm: -7,193 signCount: 42,194 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",195 backupEligible: true,196 backupState: false,197 createdAt: 1690000000000,198 },199 createdAt: 1690000000000,200 modifiedAt: 1700000000000,201 };202203 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });204 const out = await parsePayload(env, PASS);205 const got = out.entries[0]!;206207 208 expect(got.totp).toBe(entry.totp);209 expect(got.notes).toBe(entry.notes);210 expect(got.tags).toEqual(entry.tags);211 expect(got.email).toBe(entry.email);212 expect(got.additionalUrls).toEqual(entry.additionalUrls);213 expect(got.canary).toEqual(canary);214 expect(got.passwordChangedAt).toBe(entry.passwordChangedAt);215 expect(got.breach).toBe(2);216 expect(got.password).toBe(entry.password);217 expect(got.passkey?.credentialId).toBe("dGVzdC1pZA");218 expect(got.passkey?.signCount).toBe(42);219 expect(got.passkey?.privateKeyJwk).toEqual(entry.passkey!.privateKeyJwk);220 });221222 test("string table deduplicates shared domains", async () => {223 const entries = Array.from({ length: 20 }, (_, i) =>224 loginEntry({225 title: `acct-${i}`,226 username: "[email protected]",227 url: "https://github.com",228 password: `p-${i}`,229 })230 );231 const env = await buildPayload({ entries, settings: {}, passphrase: PASS });232 const out = await parsePayload(env, PASS);233 expect(out.entries).toHaveLength(20);234 for (const e of out.entries) {235 expect(e.url).toBe("https://github.com");236 expect(e.username).toBe("[email protected]");237 }238 });239240 test("settings round-trip", async () => {241 const settings: VaultSettings = {242 accentColor: "rose",243 theme: "light",244 autoLockMinutes: 15,245 lockOnClose: true,246 };247 const env = await buildPayload({ entries: [], settings, passphrase: PASS });248 const out = await parsePayload(env, PASS);249 expect(out.settings).toEqual(settings);250 });251252 test("license round-trip and activation timestamp", async () => {253 const env = await buildPayload({254 entries: [],255 settings: {},256 license: { key: "PCH-AAAA-BBBB-CCCC", activatedAt: 1700000000000 },257 passphrase: PASS,258 });259 const out = await parsePayload(env, PASS);260 expect(out.license).toEqual({ key: "PCH-AAAA-BBBB-CCCC", activatedAt: 1700000000000 });261 });262263 test("wrong passphrase throws INVALID_PASSPHRASE", async () => {264 const env = await buildPayload({265 entries: [loginEntry()],266 settings: {},267 passphrase: PASS,268 });269 await expect(parsePayload(env, "wrong")).rejects.toThrow("INVALID_PASSPHRASE");270 });271272 test("tampered ciphertext fails AEAD", async () => {273 const env = await buildPayload({274 entries: [loginEntry()],275 settings: {},276 passphrase: PASS,277 });278 env[env.length - 17] ^= 0xff; 279 await expect(parsePayload(env, PASS)).rejects.toThrow();280 });281282 test("trashed entries preserve deletedAt", async () => {283 const entry = loginEntry({ deletedAt: 1700002000000 });284 const env = await buildPayload({285 entries: [entry],286 settings: {},287 passphrase: PASS,288 });289 const out = await parsePayload(env, PASS);290 expect(out.entries[0].deletedAt).toBe(entry.deletedAt);291 });292293 test("100 entries fit comfortably in a small envelope", async () => {294 const entries = Array.from({ length: 100 }, (_, i) =>295 loginEntry({296 title: `account-${i}`,297 username: `user${i % 10}@example.com`,298 url: "https://example.com",299 password: "qwerty-12345-zxcvbn",300 })301 );302 const env = await buildPayload({ entries, settings: {}, passphrase: PASS });303 expect(env.length).toBeLessThan(8000);304 const out = await parsePayload(env, PASS);305 expect(out.entries).toHaveLength(100);306 });307});308309describe("peachPayload bucket padding (v2 format)", () => {310 311 312 test("single-entry vault outer envelope is exactly 1068 bytes", async () => {313 const env = await buildPayload({314 entries: [loginEntry()],315 settings: {},316 passphrase: PASS,317 });318 expect(env.length).toBe(1068); 319 });320321 test("small vault and tiny vault produce the same outer envelope size", async () => {322 const tiny = await buildPayload({ entries: [], settings: {}, passphrase: PASS });323 const small = await buildPayload({324 entries: [loginEntry(), loginEntry(), loginEntry()],325 settings: {},326 passphrase: PASS,327 });328 329 expect(tiny.length).toBe(small.length);330 });331332 test("round-trip through padding: entries survive buildPayload + parsePayload", async () => {333 const entry = loginEntry({ title: "padding-test", password: "correcthorse" });334 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });335 const out = await parsePayload(env, PASS);336 expect(out.entries).toHaveLength(1);337 expect(out.entries[0].title).toBe("padding-test");338 expect(out.entries[0].password).toBe("correcthorse");339 });340341 test("wrong passphrase still returns INVALID_PASSPHRASE (padding does not help attacker)", async () => {342 const env = await buildPayload({ entries: [loginEntry()], settings: {}, passphrase: PASS });343 await expect(parsePayload(env, "wrong")).rejects.toThrow("INVALID_PASSPHRASE");344 });345});346347describe("peachPayload v3 — email field (INFO-2)", () => {348 test("preserves email through v3 build + parse round-trip", async () => {349 const entry = loginEntry({ email: "[email protected]" });350 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });351 const out = await parsePayload(env, PASS);352 expect(out.entries).toHaveLength(1);353 expect(out.entries[0].email).toBe("[email protected]");354 });355356 test("omitting email round-trips with email undefined", async () => {357 const entry = loginEntry();358 expect(entry.email).toBeUndefined();359 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });360 const out = await parsePayload(env, PASS);361 expect(out.entries[0].email).toBeUndefined();362 });363364 test("non-login entries ignore email in v3 (card, note, passkey)", async () => {365 const entries = [cardEntry(), noteEntry(), passkeyEntry()];366 const env = await buildPayload({ entries, settings: {}, passphrase: PASS });367 const out = await parsePayload(env, PASS);368 expect(out.entries).toHaveLength(3);369 for (const e of out.entries) expect((e as VaultEntry).email).toBeUndefined();370 });371372 test("v3 envelope leading byte is 0x03 (format version)", async () => {373 374 375 376 const env = await buildPayload({377 entries: [loginEntry({ email: "[email protected]" })],378 settings: {},379 passphrase: PASS,380 });381 const out = await parsePayload(env, PASS);382 expect(out.entries[0].email).toBe("[email protected]");383 });384});385386describe("PCH-01-001 — NFC passphrase normalization", () => {387 388 const NFC_PASS = "café".normalize("NFC");389 const NFD_PASS = "café".normalize("NFD");390391 test("NFD/NFC forms genuinely differ (fixture sanity)", () => {392 expect(NFC_PASS).not.toBe(NFD_PASS);393 });394395 test("export_with_nfd_passphrase_imports_correctly", async () => {396 397 const env = await buildPayload({398 entries: [loginEntry()],399 settings: {},400 passphrase: NFD_PASS,401 });402 const out = await parsePayload(env, NFC_PASS);403 expect(out.entries[0].title).toBe("github");404 });405406 test("export_with_nfc_passphrase_imports_correctly", async () => {407 408 const env = await buildPayload({409 entries: [loginEntry()],410 settings: {},411 passphrase: NFC_PASS,412 });413 const out = await parsePayload(env, NFC_PASS);414 expect(out.entries[0].title).toBe("github");415 });416417 test("codex_restore_with_nfd_passphrase_succeeds", async () => {418 419 420 const env = await buildPayload({421 entries: [loginEntry()],422 settings: {},423 passphrase: NFC_PASS,424 });425 const out = await parsePayload(env, NFD_PASS);426 expect(out.entries[0].title).toBe("github");427 });428429 test("non_ascii_cjk_combining_round_trip", async () => {430 431 432 const base = "日本語がpassword";433 const nfc = base.normalize("NFC");434 const nfd = base.normalize("NFD");435 expect(nfc).not.toBe(nfd);436 const env = await buildPayload({437 entries: [loginEntry()],438 settings: {},439 passphrase: nfd,440 });441 const out = await parsePayload(env, nfc);442 expect(out.entries[0].title).toBe("github");443 });444});