vault.session-escrow.test.ts

2.9 KB · raw

1import { beforeEach, describe, expect, it, vi } from "vitest";2import { deriveKey } from "../crypto/argon2";34// PCH-01-008: a password change must refresh the MV3 session-key escrow so the5// next service-worker restore decrypts the new-key Slot A. The backing stores6// are closed over by the chrome mock on globalThis, so they survive the7// vi.resetModules() we use to simulate a service-worker restart mid-test.89type StorageShape = Record<string, unknown>;1011describe("vault session escrow refresh (PCH-01-008)", () => {12  let local: StorageShape;13  let session: StorageShape;1415  beforeEach(() => {16    vi.resetModules();17    local = {};18    session = {};19    const area = (store: StorageShape) => ({20      get: vi.fn(async (key?: string | string[]) => {21        if (!key) return store;22        if (typeof key === "string") return { [key]: store[key] };23        return key.reduce<Record<string, unknown>>((r, k) => { r[k] = store[k]; return r; }, {});24      }),25      set: vi.fn(async (next: Record<string, unknown>) => { Object.assign(store, next); }),26      remove: vi.fn(async (keys: string | string[]) => {27        for (const k of Array.isArray(keys) ? keys : [keys]) delete store[k];28      }),29    });30    (globalThis as { chrome?: typeof chrome }).chrome = {31      storage: { local: area(local), session: area(session) },32      alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() } },33      idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() } },34    } as unknown as typeof chrome;35    vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("network disabled"); }));36  });3738  it("change_password_updates_session_escrow", async () => {39    const v1 = await import("./vault");40    await v1.createVault("old-pass-abc123");41    await v1.addEntry({ type: "login", title: "Acct", password: "pw" });42    expect(await v1.changePassword("old-pass-abc123", "new-pass-xyz789")).toBe(true);4344    // Flush the fire-and-forget saveSessionKey microtask, then simulate a SW45    // restart: a fresh module has derivedKey=null but session/local persist.46    await Promise.resolve();47    vi.resetModules();48    const v2 = await import("./vault");49    expect(await v2.restoreFromSession()).toBe(true);50    expect(v2.getEntries().some((e) => e.title === "Acct")).toBe(true);51  });5253  it("change_password_with_new_key_updates_session_escrow", async () => {54    const v1 = await import("./vault");55    await v1.createVault("orig-pass-abc123");56    await v1.addEntry({ type: "login", title: "Recovered", password: "pw" });5758    const newSalt = crypto.getRandomValues(new Uint8Array(16));59    const newKey = await deriveKey("reset-pass-xyz789", newSalt);60    await v1.changePasswordWithNewKey(newSalt, newKey);6162    await Promise.resolve();63    vi.resetModules();64    const v2 = await import("./vault");65    expect(await v2.restoreFromSession()).toBe(true);66    expect(v2.getEntries().some((e) => e.title === "Recovered")).toBe(true);67  });68});