sync-identity-encryption.test.ts

3.9 KB · raw

1/**2 * sync-identity-encryption.test.ts — M12/SEC-0033 *4 * getSyncIdentity must encrypt the X25519 private key under the vault's5 * derived key before persisting, must require unlock to read, and must6 * migrate v0 (unencrypted) records to v1 (encrypted) on first read.7 */89import { 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    // Overwrite with a v0-shaped record.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    // Storage was upgraded.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});