123456789import { beforeEach, describe, expect, it, vi } from "vitest";1011describe("sync identity encryption (M12/SEC-003)", () => {12 let storage: Record<string, unknown>;1314 beforeEach(() => {15 vi.resetModules();16 vi.useRealTimers();17 storage = {};1819 const chromeMock = {20 runtime: { id: "peach-extension", sendMessage: vi.fn() },21 storage: {22 local: {23 get: vi.fn(async (key?: string | string[]) => {24 if (!key) return storage;25 if (typeof key === "string") return { [key]: storage[key] };26 return key.reduce<Record<string, unknown>>((acc, k) => {27 acc[k] = storage[k];28 return acc;29 }, {});30 }),31 set: vi.fn(async (next: Record<string, unknown>) => {32 Object.assign(storage, next);33 }),34 remove: vi.fn(async (keys: string | string[]) => {35 const list = Array.isArray(keys) ? keys : [keys];36 for (const k of list) delete storage[k];37 }),38 },39 session: {40 get: vi.fn(async () => ({})),41 set: vi.fn(async () => {}),42 remove: vi.fn(async () => {}),43 },44 },45 alarms: {46 clear: vi.fn(),47 create: vi.fn(),48 onAlarm: { addListener: vi.fn() },49 },50 idle: {51 setDetectionInterval: vi.fn(),52 onStateChanged: { addListener: vi.fn() },53 },54 } as unknown as typeof chrome;55 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;56 });5758 it("throws vault_locked when called before unlock", async () => {59 const vault = await import("./vault");60 await expect(vault.getSyncIdentity()).rejects.toThrow(/vault_locked/);61 });6263 it("persisted record contains no plaintext private key field after generation", async () => {64 const vault = await import("./vault");65 await vault.createVault("test-pass-12345");66 const { privateKeyBytes } = await vault.getSyncIdentity();6768 const stored = JSON.parse(storage.peach_sync_identity as string) as Record<string, unknown>;69 expect(stored.priv).toBeUndefined();70 expect(typeof stored.enc_priv).toBe("string");71 expect(typeof stored.enc_nonce).toBe("string");72 expect(typeof stored.enc_tag).toBe("string");73 expect(typeof stored.pub).toBe("string");74 expect(privateKeyBytes.length).toBe(32);75 });7677 it("subsequent reads return the same private key bytes", async () => {78 const vault = await import("./vault");79 await vault.createVault("test-pass-12345");80 const first = await vault.getSyncIdentity();81 const second = await vault.getSyncIdentity();82 expect(Array.from(second.privateKeyBytes)).toEqual(Array.from(first.privateKeyBytes));83 expect(second.publicKeyHex).toBe(first.publicKeyHex);84 });8586 it("migrates v0 (unencrypted priv/pub) record to v1 encrypted form", async () => {87 const vault = await import("./vault");88 await vault.createVault("test-pass-12345");8990 91 const fakePriv = new Uint8Array(32).fill(7);92 const fakePub = new Uint8Array(32).fill(8);93 const b64 = (b: Uint8Array) => {94 let s = "";95 for (const v of b) s += String.fromCharCode(v);96 return btoa(s);97 };98 storage.peach_sync_identity = JSON.stringify({99 priv: b64(fakePriv),100 pub: b64(fakePub),101 });102103 const result = await vault.getSyncIdentity();104 expect(Array.from(result.privateKeyBytes)).toEqual(Array.from(fakePriv));105106 107 const stored = JSON.parse(storage.peach_sync_identity as string) as Record<string, unknown>;108 expect(stored.priv).toBeUndefined();109 expect(typeof stored.enc_priv).toBe("string");110 });111});