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 rpId: "github.com",64 rpName: "GitHub",65 credentialId: "abc123",66 publicKeyCose: "a401020326...",67 privateKeyJwk: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "d" },68 userHandle: "user-handle",69 userName: "june",70 userDisplayName: "June",71 algorithm: -7,72 signCount: 5,73 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",74 backupEligible: true,75 backupState: false,76 createdAt: 1700000000000,77 modifiedAt: 1700000000000,78 ...over,79 };80}8182describe("peachPayload", () => {83 test("UUID round-trip preserves the hyphenated string", () => {84 const id = "123e4567-e89b-42d3-a456-426614174000";85 expect(bytesToUuid(uuidToBytes(id))).toBe(id);86 });8788 test("login entry round-trips all fields", async () => {89 const canary: Canary = { webhookUrl: "https://example.com/canary", label: "alert" };90 const entry = loginEntry({91 totp: "JBSWY3DPEHPK3PXP",92 tags: ["personal", "dev"],93 notes: "second factor on yubikey",94 canary,95 breach: 4,96 passwordChangedAt: 1700000500000,97 additionalUrls: ["https://gist.github.com"],98 });99100 const env = await buildPayload({101 entries: [entry],102 settings: {},103 passphrase: PASS,104 });105 const out = await parsePayload(env, PASS);106 expect(out.entries).toHaveLength(1);107 const got = out.entries[0];108 expect(got.id).toBe(entry.id);109 expect(got.title).toBe(entry.title);110 expect(got.url).toBe(entry.url);111 expect(got.username).toBe(entry.username);112 expect(got.password).toBe(entry.password);113 expect(got.totp).toBe(entry.totp);114 expect(got.tags).toEqual(entry.tags);115 expect(got.notes).toBe(entry.notes);116 expect(got.canary).toEqual(canary);117 expect(got.breach).toBe(4);118 expect(got.additionalUrls).toEqual(entry.additionalUrls);119 expect(got.passwordChangedAt).toBe(entry.passwordChangedAt);120 expect(got.createdAt).toBe(entry.createdAt);121 expect(got.modifiedAt).toBe(entry.modifiedAt);122 });123124 test("note, card, and passkey entries round-trip", async () => {125 const note = noteEntry({ tags: ["secrets"] });126 const card = cardEntry({ cardNickname: "daily driver" });127 const passkey = passkeyEntry();128129 const env = await buildPayload({130 entries: [note, card, passkey],131 settings: {},132 passphrase: PASS,133 });134 const out = await parsePayload(env, PASS);135 expect(out.entries).toHaveLength(3);136137 const gotNote = out.entries.find((e) => e.id === note.id)!;138 expect(gotNote.type).toBe("note");139 expect(gotNote.notes).toBe(note.notes);140 expect(gotNote.tags).toEqual(["secrets"]);141142 const gotCard = out.entries.find((e) => e.id === card.id)!;143 expect(gotCard.type).toBe("card");144 expect(gotCard.cardNumber).toBe(card.cardNumber);145 expect(gotCard.expMonth).toBe(card.expMonth);146 expect(gotCard.expYear).toBe(card.expYear);147 expect(gotCard.cvc).toBe(card.cvc);148 expect(gotCard.network).toBe("visa");149 expect(gotCard.last4).toBe("1111");150 expect(gotCard.cardNickname).toBe("daily driver");151 expect(gotCard.cardholderName).toBe(card.cardholderName);152153 const gotPk = out.entries.find((e) => e.id === passkey.id)!;154 expect(gotPk.type).toBe("passkey");155 expect(gotPk.rpId).toBe("github.com");156 expect(gotPk.privateKeyJwk).toEqual(passkey.privateKeyJwk);157 expect(gotPk.algorithm).toBe(-7);158 expect(gotPk.signCount).toBe(5);159 expect(gotPk.backupEligible).toBe(true);160 expect(gotPk.backupState).toBe(false);161 });162163 test("passkey-bearing login entry round-trips ALL login fields (SEV-1-B)", async () => {164 const canary: Canary = { webhookUrl: "https://example.com/canary", label: "alert" };165 const entry: VaultEntry = {166 id: crypto.randomUUID(),167 type: "login",168 title: "Google",169 username: "[email protected]",170 email: "[email protected]",171 password: "also-has-a-password",172 url: "https://google.com",173 additionalUrls: ["https://accounts.google.com"],174 totp: "JBSWY3DPEHPK3PXP",175 notes: "backup codes: 1234 5678",176 tags: ["work", "important"],177 canary,178 passwordChangedAt: 1700000000000,179 breach: 2,180 passkey: {181 rpId: "google.com",182 rpName: "Google",183 credentialId: "dGVzdC1pZA",184 publicKeyCose: "a40102032620",185 privateKeyJwk: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "d" },186 userHandle: "dGVzdA",187 userName: "[email protected]",188 userDisplayName: "Ada Lovelace",189 algorithm: -7,190 signCount: 42,191 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",192 backupEligible: true,193 backupState: false,194 createdAt: 1690000000000,195 },196 createdAt: 1690000000000,197 modifiedAt: 1700000000000,198 };199200 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });201 const out = await parsePayload(env, PASS);202 const got = out.entries[0]!;203204 205 expect(got.totp).toBe(entry.totp);206 expect(got.notes).toBe(entry.notes);207 expect(got.tags).toEqual(entry.tags);208 expect(got.email).toBe(entry.email);209 expect(got.additionalUrls).toEqual(entry.additionalUrls);210 expect(got.canary).toEqual(canary);211 expect(got.passwordChangedAt).toBe(entry.passwordChangedAt);212 expect(got.breach).toBe(2);213 expect(got.password).toBe(entry.password);214 215 216 expect(got.credentialId).toBe("dGVzdC1pZA");217 expect(got.signCount).toBe(42);218 expect(got.privateKeyJwk).toEqual(entry.passkey!.privateKeyJwk);219 });220221 test("string table deduplicates shared domains", async () => {222 const entries = Array.from({ length: 20 }, (_, i) =>223 loginEntry({224 title: `acct-${i}`,225 username: "[email protected]",226 url: "https://github.com",227 password: `p-${i}`,228 })229 );230 const env = await buildPayload({ entries, settings: {}, passphrase: PASS });231 const out = await parsePayload(env, PASS);232 expect(out.entries).toHaveLength(20);233 for (const e of out.entries) {234 expect(e.url).toBe("https://github.com");235 expect(e.username).toBe("[email protected]");236 }237 });238239 test("settings round-trip", async () => {240 const settings: VaultSettings = {241 accentColor: "rose",242 theme: "light",243 autoLockMinutes: 15,244 lockOnClose: true,245 };246 const env = await buildPayload({ entries: [], settings, passphrase: PASS });247 const out = await parsePayload(env, PASS);248 expect(out.settings).toEqual(settings);249 });250251 test("license round-trip and activation timestamp", async () => {252 const env = await buildPayload({253 entries: [],254 settings: {},255 license: { key: "PCH-AAAA-BBBB-CCCC", activatedAt: 1700000000000 },256 passphrase: PASS,257 });258 const out = await parsePayload(env, PASS);259 expect(out.license).toEqual({ key: "PCH-AAAA-BBBB-CCCC", activatedAt: 1700000000000 });260 });261262 test("wrong passphrase throws INVALID_PASSPHRASE", async () => {263 const env = await buildPayload({264 entries: [loginEntry()],265 settings: {},266 passphrase: PASS,267 });268 await expect(parsePayload(env, "wrong")).rejects.toThrow("INVALID_PASSPHRASE");269 });270271 test("tampered ciphertext fails AEAD", async () => {272 const env = await buildPayload({273 entries: [loginEntry()],274 settings: {},275 passphrase: PASS,276 });277 env[env.length - 17] ^= 0xff; 278 await expect(parsePayload(env, PASS)).rejects.toThrow();279 });280281 test("trashed entries preserve deletedAt", async () => {282 const entry = loginEntry({ deletedAt: 1700002000000 });283 const env = await buildPayload({284 entries: [entry],285 settings: {},286 passphrase: PASS,287 });288 const out = await parsePayload(env, PASS);289 expect(out.entries[0].deletedAt).toBe(entry.deletedAt);290 });291292 test("100 entries fit comfortably in a small envelope", async () => {293 const entries = Array.from({ length: 100 }, (_, i) =>294 loginEntry({295 title: `account-${i}`,296 username: `user${i % 10}@example.com`,297 url: "https://example.com",298 password: "qwerty-12345-zxcvbn",299 })300 );301 const env = await buildPayload({ entries, settings: {}, passphrase: PASS });302 expect(env.length).toBeLessThan(8000);303 const out = await parsePayload(env, PASS);304 expect(out.entries).toHaveLength(100);305 });306});307308describe("peachPayload bucket padding (v2 format)", () => {309 310 311 test("single-entry vault outer envelope is exactly 1068 bytes", async () => {312 const env = await buildPayload({313 entries: [loginEntry()],314 settings: {},315 passphrase: PASS,316 });317 expect(env.length).toBe(1068); 318 });319320 test("small vault and tiny vault produce the same outer envelope size", async () => {321 const tiny = await buildPayload({ entries: [], settings: {}, passphrase: PASS });322 const small = await buildPayload({323 entries: [loginEntry(), loginEntry(), loginEntry()],324 settings: {},325 passphrase: PASS,326 });327 328 expect(tiny.length).toBe(small.length);329 });330331 test("round-trip through padding: entries survive buildPayload + parsePayload", async () => {332 const entry = loginEntry({ title: "padding-test", password: "correcthorse" });333 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });334 const out = await parsePayload(env, PASS);335 expect(out.entries).toHaveLength(1);336 expect(out.entries[0].title).toBe("padding-test");337 expect(out.entries[0].password).toBe("correcthorse");338 });339340 test("wrong passphrase still returns INVALID_PASSPHRASE (padding does not help attacker)", async () => {341 const env = await buildPayload({ entries: [loginEntry()], settings: {}, passphrase: PASS });342 await expect(parsePayload(env, "wrong")).rejects.toThrow("INVALID_PASSPHRASE");343 });344});345346describe("peachPayload v3 — email field (INFO-2)", () => {347 test("preserves email through v3 build + parse round-trip", async () => {348 const entry = loginEntry({ email: "[email protected]" });349 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });350 const out = await parsePayload(env, PASS);351 expect(out.entries).toHaveLength(1);352 expect(out.entries[0].email).toBe("[email protected]");353 });354355 test("omitting email round-trips with email undefined", async () => {356 const entry = loginEntry();357 expect(entry.email).toBeUndefined();358 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });359 const out = await parsePayload(env, PASS);360 expect(out.entries[0].email).toBeUndefined();361 });362363 test("non-login entries ignore email in v3 (card, note, passkey)", async () => {364 const entries = [cardEntry(), noteEntry(), passkeyEntry()];365 const env = await buildPayload({ entries, settings: {}, passphrase: PASS });366 const out = await parsePayload(env, PASS);367 expect(out.entries).toHaveLength(3);368 for (const e of out.entries) expect((e as VaultEntry).email).toBeUndefined();369 });370371 test("v3 envelope leading byte is 0x03 (format version)", async () => {372 373 374 375 const env = await buildPayload({376 entries: [loginEntry({ email: "[email protected]" })],377 settings: {},378 passphrase: PASS,379 });380 const out = await parsePayload(env, PASS);381 expect(out.entries[0].email).toBe("[email protected]");382 });383});384385describe("PCH-01-001 — NFC passphrase normalization", () => {386 387 const NFC_PASS = "café".normalize("NFC");388 const NFD_PASS = "café".normalize("NFD");389390 test("NFD/NFC forms genuinely differ (fixture sanity)", () => {391 expect(NFC_PASS).not.toBe(NFD_PASS);392 });393394 test("export_with_nfd_passphrase_imports_correctly", async () => {395 396 const env = await buildPayload({397 entries: [loginEntry()],398 settings: {},399 passphrase: NFD_PASS,400 });401 const out = await parsePayload(env, NFC_PASS);402 expect(out.entries[0].title).toBe("github");403 });404405 test("export_with_nfc_passphrase_imports_correctly", async () => {406 407 const env = await buildPayload({408 entries: [loginEntry()],409 settings: {},410 passphrase: NFC_PASS,411 });412 const out = await parsePayload(env, NFC_PASS);413 expect(out.entries[0].title).toBe("github");414 });415416 test("codex_restore_with_nfd_passphrase_succeeds", async () => {417 418 419 const env = await buildPayload({420 entries: [loginEntry()],421 settings: {},422 passphrase: NFC_PASS,423 });424 const out = await parsePayload(env, NFD_PASS);425 expect(out.entries[0].title).toBe("github");426 });427428 test("legacy_raw_fallback_opens_old_backup", async () => {429 430 431 432 const env = await buildPayload(433 { entries: [loginEntry()], settings: {}, passphrase: NFD_PASS },434 { raw: true },435 );436 const out = await parsePayload(env, NFD_PASS);437 expect(out.entries[0].title).toBe("github");438 });439440 test("non_ascii_cjk_combining_round_trip", async () => {441 442 443 const base = "日本語がpassword";444 const nfc = base.normalize("NFC");445 const nfd = base.normalize("NFD");446 expect(nfc).not.toBe(nfd);447 const env = await buildPayload({448 entries: [loginEntry()],449 settings: {},450 passphrase: nfd,451 });452 const out = await parsePayload(env, nfc);453 expect(out.entries[0].title).toBe("github");454 });455});