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("string table deduplicates shared domains", async () => {164 const entries = Array.from({ length: 20 }, (_, i) =>165 loginEntry({166 title: `acct-${i}`,167 username: "[email protected]",168 url: "https://github.com",169 password: `p-${i}`,170 })171 );172 const env = await buildPayload({ entries, settings: {}, passphrase: PASS });173 const out = await parsePayload(env, PASS);174 expect(out.entries).toHaveLength(20);175 for (const e of out.entries) {176 expect(e.url).toBe("https://github.com");177 expect(e.username).toBe("[email protected]");178 }179 });180181 test("settings round-trip", async () => {182 const settings: VaultSettings = {183 accentColor: "rose",184 theme: "light",185 autoLockMinutes: 15,186 lockOnClose: true,187 };188 const env = await buildPayload({ entries: [], settings, passphrase: PASS });189 const out = await parsePayload(env, PASS);190 expect(out.settings).toEqual(settings);191 });192193 test("license round-trip and activation timestamp", async () => {194 const env = await buildPayload({195 entries: [],196 settings: {},197 license: { key: "PCH-AAAA-BBBB-CCCC", activatedAt: 1700000000000 },198 passphrase: PASS,199 });200 const out = await parsePayload(env, PASS);201 expect(out.license).toEqual({ key: "PCH-AAAA-BBBB-CCCC", activatedAt: 1700000000000 });202 });203204 test("wrong passphrase throws INVALID_PASSPHRASE", async () => {205 const env = await buildPayload({206 entries: [loginEntry()],207 settings: {},208 passphrase: PASS,209 });210 await expect(parsePayload(env, "wrong")).rejects.toThrow("INVALID_PASSPHRASE");211 });212213 test("tampered ciphertext fails AEAD", async () => {214 const env = await buildPayload({215 entries: [loginEntry()],216 settings: {},217 passphrase: PASS,218 });219 env[env.length - 17] ^= 0xff; 220 await expect(parsePayload(env, PASS)).rejects.toThrow();221 });222223 test("trashed entries preserve deletedAt", async () => {224 const entry = loginEntry({ deletedAt: 1700002000000 });225 const env = await buildPayload({226 entries: [entry],227 settings: {},228 passphrase: PASS,229 });230 const out = await parsePayload(env, PASS);231 expect(out.entries[0].deletedAt).toBe(entry.deletedAt);232 });233234 test("100 entries fit comfortably in a small envelope", async () => {235 const entries = Array.from({ length: 100 }, (_, i) =>236 loginEntry({237 title: `account-${i}`,238 username: `user${i % 10}@example.com`,239 url: "https://example.com",240 password: "qwerty-12345-zxcvbn",241 })242 );243 const env = await buildPayload({ entries, settings: {}, passphrase: PASS });244 expect(env.length).toBeLessThan(8000);245 const out = await parsePayload(env, PASS);246 expect(out.entries).toHaveLength(100);247 });248});249250describe("peachPayload bucket padding (v2 format)", () => {251 252 253 test("single-entry vault outer envelope is exactly 1068 bytes", async () => {254 const env = await buildPayload({255 entries: [loginEntry()],256 settings: {},257 passphrase: PASS,258 });259 expect(env.length).toBe(1068); 260 });261262 test("small vault and tiny vault produce the same outer envelope size", async () => {263 const tiny = await buildPayload({ entries: [], settings: {}, passphrase: PASS });264 const small = await buildPayload({265 entries: [loginEntry(), loginEntry(), loginEntry()],266 settings: {},267 passphrase: PASS,268 });269 270 expect(tiny.length).toBe(small.length);271 });272273 test("round-trip through padding: entries survive buildPayload + parsePayload", async () => {274 const entry = loginEntry({ title: "padding-test", password: "correcthorse" });275 const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });276 const out = await parsePayload(env, PASS);277 expect(out.entries).toHaveLength(1);278 expect(out.entries[0].title).toBe("padding-test");279 expect(out.entries[0].password).toBe("correcthorse");280 });281282 test("wrong passphrase still returns INVALID_PASSPHRASE (padding does not help attacker)", async () => {283 const env = await buildPayload({ entries: [loginEntry()], settings: {}, passphrase: PASS });284 await expect(parsePayload(env, "wrong")).rejects.toThrow("INVALID_PASSPHRASE");285 });286});