1import { describe, expect, test } from "vitest";2import { readFileSync } from "node:fs";34import {5 bytesToUuid,6 buildPayload,7 parsePayload,8 uuidToBytes,9} from "./peachPayload";10import type { Canary, VaultEntry } from "../shared/types";11import type { VaultSettings } from "../background/vault";1213const PASS = "correct horse battery staple";1415const V3_VECTOR = JSON.parse(16 readFileSync("fixtures/golden/peach-payload-v3.json", "utf8"),17) as {18 passphrase: string;19 entry: { id: string; title: string; username: string; email: string; password: string; url: string };20 envelopeBase64: string;21};2223function loginEntry(over: Partial<VaultEntry> = {}): VaultEntry {24 return {25 id: crypto.randomUUID(),26 type: "login",27 title: "github",28 username: "[email protected]",29 password: "p4ssw0rd!",30 url: "https://github.com",31 createdAt: 1700000000000,32 modifiedAt: 1700001000000,33 ...over,34 };35}3637function noteEntry(over: Partial<VaultEntry> = {}): VaultEntry {38 return {39 id: crypto.randomUUID(),40 type: "note",41 title: "private note",42 notes: "remember the milk",43 createdAt: 1700000000000,44 modifiedAt: 1700000000000,45 ...over,46 };47}4849function cardEntry(over: Partial<VaultEntry> = {}): VaultEntry {50 return {51 id: crypto.randomUUID(),52 type: "card",53 title: "personal visa",54 cardholderName: "June Asha",55 cardNumber: "4111111111111111",56 expMonth: "08",57 expYear: "2029",58 cvc: "123",59 network: "visa",60 last4: "1111",61 createdAt: 1700000000000,62 modifiedAt: 1700000000000,63 ...over,64 };65}6667function passkeyEntry(over: Partial<VaultEntry> = {}): VaultEntry {68 return {69 id: crypto.randomUUID(),70 type: "passkey",71 title: "github passkey",72 passkey: {73 rpId: "github.com",74 rpName: "GitHub",75 credentialId: "abc123",76 publicKeyCose: "a401020326...",77 privateKeyJwk: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "d" },78 userHandle: "user-handle",79 userName: "june",80 userDisplayName: "June",81 algorithm: -7,82 signCount: 5,83 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",84 backupEligible: true,85 backupState: false,86 createdAt: 1700000000000,87 },88 createdAt: 1700000000000,89 modifiedAt: 1700000000000,90 ...over,91 };92}9394describe("peachPayload", () => {95 test("UUID round-trip preserves the hyphenated string", () => {96 const id = "123e4567-e89b-42d3-a456-426614174000";97 expect(bytesToUuid(uuidToBytes(id))).toBe(id);98 });99100 test("login entry round-trips all fields", async () => {101 const canary: Canary = { webhookUrl: "https://example.com/canary", label: "alert" };102 const entry = loginEntry({103 totp: "JBSWY3DPEHPK3PXP",104 tags: ["personal", "dev"],105 notes: "second factor on yubikey",106 canary,107 breach: 4,108 passwordChangedAt: 1700000500000,109 additionalUrls: ["https://gist.github.com"],110 });111112 const env = await buildPayload({113 entries: [entry],114 settings: {},115 passphrase: PASS,116 });117 const out = await parsePayload(env, PASS);118 expect(out.entries).toHaveLength(1);119 const got = out.entries[0];120 expect(got.id).toBe(entry.id);121 expect(got.title).toBe(entry.title);122 expect(got.url).toBe(entry.url);123 expect(got.username).toBe(entry.username);124 expect(got.password).toBe(entry.password);125 expect(got.totp).toBe(entry.totp);126 expect(got.tags).toEqual(entry.tags);127 expect(got.notes).toBe(entry.notes);128 expect(got.canary).toEqual(canary);129 expect(got.breach).toBe(4);130 expect(got.additionalUrls).toEqual(entry.additionalUrls);131 expect(got.passwordChangedAt).toBe(entry.passwordChangedAt);132 expect(got.createdAt).toBe(entry.createdAt);133 expect(got.modifiedAt).toBe(entry.modifiedAt);134 });135136 test.each([137 ["256 ASCII characters", "a".repeat(256)],138 ["256 multibyte characters", "界".repeat(256)],139 ])("round-trips a %s password through the V4 payload", async (_name, password) => {140 const env = await buildPayload({141 entries: [loginEntry({ password })],142 settings: {},143 passphrase: PASS,144 });145146 const out = await parsePayload(env, PASS);147148 expect(out.entries[0].password).toBe(password);149 });150151 test("decodes the shared V3 compatibility vector", async () => {152 const out = await parsePayload(Buffer.from(V3_VECTOR.envelopeBase64, "base64"), V3_VECTOR.passphrase);153154 expect(out.entries[0]).toMatchObject(V3_VECTOR.entry);155 });156157 test("note, card, and passkey entries round-trip", async () => {158 const note = noteEntry({ tags: ["secrets"] });159 const card = cardEntry({ cardNickname: "daily driver" });160 const passkey = passkeyEntry();161162 const env = await buildPayload({163 entries: [note, card, passkey],164 settings: {},165 passphrase: PASS,166 });167 const out = await parsePayload(env, PASS);168 expect(out.entries).toHaveLength(3);169170 const gotNote = out.entries.find((e) => e.id === note.id)!;171 expect(gotNote.type).toBe("note");172 expect(gotNote.notes).toBe(note.notes);173 expect(gotNote.tags).toEqual(["secrets"]);174175 const gotCard = out.entries.find((e) => e.id === card.id)!;176 expect(gotCard.type).toBe("card");177 expect(gotCard.cardNumber).toBe(card.cardNumber);178 expect(gotCard.expMonth).toBe(card.expMonth);179 expect(gotCard.expYear).toBe(card.expYear);180 expect(gotCard.cvc).toBe(card.cvc);181 expect(gotCard.network).toBe("visa");182 expect(gotCard.last4).toBe("1111");183 expect(gotCard.cardNickname).toBe("daily driver");184 expect(gotCard.cardholderName).toBe(card.cardholderName);185186 const gotPk = out.entries.find((e) => e.id === passkey.id)!;187 expect(gotPk.type).toBe("passkey");188 expect(gotPk.passkey?.rpId).toBe("github.com");189 expect(gotPk.passkey?.privateKeyJwk).toEqual(passkey.passkey?.privateKeyJwk);190 expect(gotPk.passkey?.algorithm).toBe(-7);191 expect(gotPk.passkey?.signCount).toBe(5);192 expect(gotPk.passkey?.backupEligible).toBe(true);193 expect(gotPk.passkey?.backupState).toBe(false);194 });195196 test("passkey-bearing login entry round-trips ALL login fields (SEV-1-B)", async () => {197 const canary: Canary = { webhookUrl: "https://example.com/canary", label: "alert" };198 const entry: VaultEntry = {199 id: crypto.randomUUID(),200 type: "login",201 title: "Google",202 username: "[email protected]",203 email: "[email protected]",204 password: "also-has-a-password",205 url: "https://google.com",206 additionalUrls: ["https://accounts.google.com"],207 totp: "JBSWY3DPEHPK3PXP",208 notes: "backup codes: 1234 5678",209 tags: ["work", "important"],210 canary,211 passwordChangedAt: 1700000000000,212 breach: 2,213 passkey: {214 rpId: "google.com",215 rpName: "Google",216 credentialId: "dGVzdC1pZA",217 publicKeyCose: "a40102032620",218 privateKeyJwk: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "d" },219 userHandle: "dGVzdA",220 userName: "[email protected]",221 userDisplayName: "Ada Lovelace",222 algorithm: -7,223 signCount: 42,224 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",225 backupEligible: true,226 backupState: false,227 createdAt: 1690000000000,228 },229 createdAt: 1690000000000,230 modifiedAt: 1700000000000,231 };232233 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });234 const out = await parsePayload(env, PASS);235 const got = out.entries[0]!;236237 238 expect(got.totp).toBe(entry.totp);239 expect(got.notes).toBe(entry.notes);240 expect(got.tags).toEqual(entry.tags);241 expect(got.email).toBe(entry.email);242 expect(got.additionalUrls).toEqual(entry.additionalUrls);243 expect(got.canary).toEqual(canary);244 expect(got.passwordChangedAt).toBe(entry.passwordChangedAt);245 expect(got.breach).toBe(2);246 expect(got.password).toBe(entry.password);247 expect(got.passkey?.credentialId).toBe("dGVzdC1pZA");248 expect(got.passkey?.signCount).toBe(42);249 expect(got.passkey?.privateKeyJwk).toEqual(entry.passkey!.privateKeyJwk);250 });251252 test("string table deduplicates shared domains", async () => {253 const entries = Array.from({ length: 20 }, (_, i) =>254 loginEntry({255 title: `acct-${i}`,256 username: "[email protected]",257 url: "https://github.com",258 password: `p-${i}`,259 })260 );261 const env = await buildPayload({ entries, settings: {}, passphrase: PASS });262 const out = await parsePayload(env, PASS);263 expect(out.entries).toHaveLength(20);264 for (const e of out.entries) {265 expect(e.url).toBe("https://github.com");266 expect(e.username).toBe("[email protected]");267 }268 });269270 test("settings round-trip", async () => {271 const settings: VaultSettings = {272 accentColor: "rose",273 theme: "light",274 autoLockMinutes: 15,275 lockOnClose: true,276 };277 const env = await buildPayload({ entries: [], settings, passphrase: PASS });278 const out = await parsePayload(env, PASS);279 expect(out.settings).toEqual(settings);280 });281282 test("license round-trip and activation timestamp", async () => {283 const env = await buildPayload({284 entries: [],285 settings: {},286 license: { key: "PCH-AAAA-BBBB-CCCC", activatedAt: 1700000000000 },287 passphrase: PASS,288 });289 const out = await parsePayload(env, PASS);290 expect(out.license).toEqual({ key: "PCH-AAAA-BBBB-CCCC", activatedAt: 1700000000000 });291 });292293 test("wrong passphrase throws INVALID_PASSPHRASE", async () => {294 const env = await buildPayload({295 entries: [loginEntry()],296 settings: {},297 passphrase: PASS,298 });299 await expect(parsePayload(env, "wrong")).rejects.toThrow("INVALID_PASSPHRASE");300 });301302 test("tampered ciphertext fails AEAD", async () => {303 const env = await buildPayload({304 entries: [loginEntry()],305 settings: {},306 passphrase: PASS,307 });308 env[env.length - 17] ^= 0xff; 309 await expect(parsePayload(env, PASS)).rejects.toThrow();310 });311312 test("trashed entries preserve deletedAt", async () => {313 const entry = loginEntry({ deletedAt: 1700002000000 });314 const env = await buildPayload({315 entries: [entry],316 settings: {},317 passphrase: PASS,318 });319 const out = await parsePayload(env, PASS);320 expect(out.entries[0].deletedAt).toBe(entry.deletedAt);321 });322323 test("100 entries fit comfortably in a small envelope", async () => {324 const entries = Array.from({ length: 100 }, (_, i) =>325 loginEntry({326 title: `account-${i}`,327 username: `user${i % 10}@example.com`,328 url: "https://example.com",329 password: "qwerty-12345-zxcvbn",330 })331 );332 const env = await buildPayload({ entries, settings: {}, passphrase: PASS });333 expect(env.length).toBeLessThan(8000);334 const out = await parsePayload(env, PASS);335 expect(out.entries).toHaveLength(100);336 });337});338339describe("peachPayload bucket padding (v2 format)", () => {340 341 342 test("single-entry vault outer envelope is exactly 1068 bytes", async () => {343 const env = await buildPayload({344 entries: [loginEntry()],345 settings: {},346 passphrase: PASS,347 });348 expect(env.length).toBe(1068); 349 });350351 test("small vault and tiny vault produce the same outer envelope size", async () => {352 const tiny = await buildPayload({ entries: [], settings: {}, passphrase: PASS });353 const small = await buildPayload({354 entries: [loginEntry(), loginEntry(), loginEntry()],355 settings: {},356 passphrase: PASS,357 });358 359 expect(tiny.length).toBe(small.length);360 });361362 test("round-trip through padding: entries survive buildPayload + parsePayload", async () => {363 const entry = loginEntry({ title: "padding-test", password: "correcthorse" });364 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });365 const out = await parsePayload(env, PASS);366 expect(out.entries).toHaveLength(1);367 expect(out.entries[0].title).toBe("padding-test");368 expect(out.entries[0].password).toBe("correcthorse");369 });370371 test("wrong passphrase still returns INVALID_PASSPHRASE (padding does not help attacker)", async () => {372 const env = await buildPayload({ entries: [loginEntry()], settings: {}, passphrase: PASS });373 await expect(parsePayload(env, "wrong")).rejects.toThrow("INVALID_PASSPHRASE");374 });375});376377describe("peachPayload v3 — email field (INFO-2)", () => {378 test("preserves email through v3 build + parse round-trip", async () => {379 const entry = loginEntry({ email: "[email protected]" });380 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });381 const out = await parsePayload(env, PASS);382 expect(out.entries).toHaveLength(1);383 expect(out.entries[0].email).toBe("[email protected]");384 });385386 test("omitting email round-trips with email undefined", async () => {387 const entry = loginEntry();388 expect(entry.email).toBeUndefined();389 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });390 const out = await parsePayload(env, PASS);391 expect(out.entries[0].email).toBeUndefined();392 });393394 test("non-login entries ignore email in v3 (card, note, passkey)", async () => {395 const entries = [cardEntry(), noteEntry(), passkeyEntry()];396 const env = await buildPayload({ entries, settings: {}, passphrase: PASS });397 const out = await parsePayload(env, PASS);398 expect(out.entries).toHaveLength(3);399 for (const e of out.entries) expect((e as VaultEntry).email).toBeUndefined();400 });401402 test("v3 envelope leading byte is 0x03 (format version)", async () => {403 404 405 406 const env = await buildPayload({407 entries: [loginEntry({ email: "[email protected]" })],408 settings: {},409 passphrase: PASS,410 });411 const out = await parsePayload(env, PASS);412 expect(out.entries[0].email).toBe("[email protected]");413 });414});415416describe("PCH-01-001 — NFC passphrase normalization", () => {417 418 const NFC_PASS = "café".normalize("NFC");419 const NFD_PASS = "café".normalize("NFD");420421 test("NFD/NFC forms genuinely differ (fixture sanity)", () => {422 expect(NFC_PASS).not.toBe(NFD_PASS);423 });424425 test("export_with_nfd_passphrase_imports_correctly", async () => {426 427 const env = await buildPayload({428 entries: [loginEntry()],429 settings: {},430 passphrase: NFD_PASS,431 });432 const out = await parsePayload(env, NFC_PASS);433 expect(out.entries[0].title).toBe("github");434 });435436 test("export_with_nfc_passphrase_imports_correctly", async () => {437 438 const env = await buildPayload({439 entries: [loginEntry()],440 settings: {},441 passphrase: NFC_PASS,442 });443 const out = await parsePayload(env, NFC_PASS);444 expect(out.entries[0].title).toBe("github");445 });446447 test("codex_restore_with_nfd_passphrase_succeeds", async () => {448 449 450 const env = await buildPayload({451 entries: [loginEntry()],452 settings: {},453 passphrase: NFC_PASS,454 });455 const out = await parsePayload(env, NFD_PASS);456 expect(out.entries[0].title).toBe("github");457 });458459 test("non_ascii_cjk_combining_round_trip", async () => {460 461 462 const base = "日本語がpassword";463 const nfc = base.normalize("NFC");464 const nfd = base.normalize("NFD");465 expect(nfc).not.toBe(nfd);466 const env = await buildPayload({467 entries: [loginEntry()],468 settings: {},469 passphrase: nfd,470 });471 const out = await parsePayload(env, nfc);472 expect(out.entries[0].title).toBe("github");473 });474});