/** * vault.adversarial.test.ts — Layer 3 §3.1 adversarial vault tests. * * Exercises boundary values, unicode/control/RTL passwords, large vaults, * concurrent operations, storage corruption, and lockout-shape claims. Tests * are parameterized to comfortably exceed the audit's 400-test target. * * IMPORTANT: where the audit references handlers or fields that the * production code does not implement (e.g. `lockoutMs` on UNLOCK responses, * `IMPORT_PEACHSCRIPT`/`RESTORE_FROM_PEACHSCRIPT`), the tests assert that the * handler returns a defined response and does not crash — see findings doc. */ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { Message, MessageResponse } from "../shared/types"; type StorageShape = Record; type MessageListener = ( msg: unknown, sender: chrome.runtime.MessageSender, sendResponse: (response: MessageResponse) => void ) => boolean; // Shared password fixtures — exercised across many tests. const PASSWORD_CASES = [ { name: "single character", password: "a" }, { name: "2 chars", password: "ab" }, { name: "exactly 8 chars", password: "abcdefgh" }, { name: "unicode password", password: "密码测试🔑" }, { name: "emoji only", password: "🔑🔒🛡️" }, { name: "very long (1000 chars)", password: "a".repeat(1000) }, { name: "very long (10000 chars)", password: "a".repeat(10000) }, { name: "all spaces (12)", password: " " }, { name: "RTL text", password: "كلمة المرور" }, { name: "mixed scripts", password: "パスワードPassword密码" }, { name: "control characters", password: "\t\n\r\x1b\x7f" }, { name: "SQL injection attempt", password: "' OR '1'='1" }, { name: "JSON injection", password: '{"type":"UNLOCK"}' }, { name: "leading whitespace", password: " pass-with-lead" }, { name: "trailing whitespace", password: "pass-with-trail " }, { name: "tab separated", password: "p1\tp2\tp3\tp4" }, { name: "newline separated", password: "p1\np2\np3\np4" }, { name: "high unicode", password: "\u{1F600}\u{1F601}\u{1F602}" }, { name: "mixed case", password: "PassWordCorrect" }, { name: "all digits", password: "1234567890" }, { name: "punctuation heavy", password: "!@#$%^&*()_+-=[]{}|;':\",./<>?`~" }, { name: "combining marks", password: "áb̂c̃" }, ]; function makeChromeMock( storage: StorageShape, sessionStorage: StorageShape, listenerSink: { listener?: MessageListener } ): typeof chrome { return { runtime: { id: "peach-extension", getURL: (path = "") => `chrome-extension://peach-extension/${path}`, onMessage: { addListener: vi.fn((fn: MessageListener) => { listenerSink.listener = fn; }), }, sendMessage: vi.fn(async () => ({ ok: true })), }, tabs: { query: vi.fn((_q: unknown, cb?: (tabs: unknown[]) => void) => { if (typeof cb === "function") { cb([{ id: 1 }]); return; } return Promise.resolve([{ id: 1 }]); }), sendMessage: vi.fn(async () => ({ ok: true })), }, storage: { local: { get: vi.fn(async (key?: string | string[]) => { if (!key) return storage; if (typeof key === "string") return { [key]: storage[key] }; return key.reduce>((acc, k) => { acc[k] = storage[k]; return acc; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(storage, next); }), remove: vi.fn(async (keys: string | string[]) => { const list = Array.isArray(keys) ? keys : [keys]; for (const key of list) delete storage[key]; }), }, session: { get: vi.fn(async (key?: string | string[]) => { if (!key) return sessionStorage; if (typeof key === "string") return { [key]: sessionStorage[key] }; return key.reduce>((acc, k) => { acc[k] = sessionStorage[k]; return acc; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(sessionStorage, next); }), remove: vi.fn(async (keys: string | string[]) => { const list = Array.isArray(keys) ? keys : [keys]; for (const key of list) delete sessionStorage[key]; }), clear: vi.fn(async () => { for (const k of Object.keys(sessionStorage)) delete sessionStorage[k]; }), }, }, alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() }, }, idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() }, }, scripting: { executeScript: vi.fn(async () => []) }, } as unknown as typeof chrome; } describe("vault adversarial inputs", () => { let storage: StorageShape; let sessionStorage: StorageShape; const sink: { listener?: MessageListener } = {}; beforeEach(async () => { vi.resetModules(); vi.useRealTimers(); storage = { peach_settings: { breachMonitoring: false } }; sessionStorage = {}; sink.listener = undefined; vi.stubGlobal( "fetch", vi.fn(async () => ({ ok: true, status: 200, text: async () => "" })) ); vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false); (globalThis as { chrome?: typeof chrome }).chrome = makeChromeMock( storage, sessionStorage, sink ); const messages = await import("./messages"); messages.setupMessageHandler(); const vault = await import("./vault"); await vault.ensureSettingsLoaded(); }); async function dispatch(message: Message): Promise { if (!sink.listener) throw new Error("listener not registered"); return new Promise((resolve) => { const keepOpen = sink.listener!( message, { id: "peach-extension", url: "chrome-extension://peach-extension/popup.html", } as chrome.runtime.MessageSender, resolve ); expect(keepOpen).toBe(true); }); } // ── Password boundary values ──────────────────────────────────────────── describe("password boundary values — CREATE_VAULT + UNLOCK round-trips", () => { for (const { name, password } of PASSWORD_CASES) { it(`CREATE_VAULT with ${name} produces a vault unlockable with same password`, async () => { const vault = await import("./vault"); await vault.createVault(password); vault.lock(); const result = await dispatch({ type: "UNLOCK", payload: { password }, }); expect(result.ok).toBe(true); }); it(`UNLOCK with wrong password after ${name} creation fails (and no crash)`, async () => { const vault = await import("./vault"); await vault.createVault(password); vault.lock(); const wrong = password + "x"; const result = await dispatch({ type: "UNLOCK", payload: { password: wrong }, }); expect(result.ok).toBe(false); }); it(`UNLOCK with empty after ${name} creation fails`, async () => { const vault = await import("./vault"); await vault.createVault(password); vault.lock(); const result = await dispatch({ type: "UNLOCK", payload: { password: "" }, }); // Empty when actual password is non-empty -> false; equal -> true. expect(result.ok).toBe(password === ""); }); it(`re-locking after ${name} unlock cleans up state (IS_UNLOCKED=false)`, async () => { const vault = await import("./vault"); await vault.createVault(password); await dispatch({ type: "LOCK" }); const r = await dispatch({ type: "IS_UNLOCKED" }); expect(r.ok).toBe(true); expect((r as { data: boolean }).data).toBe(false); }); } }); // ── Entry boundary values ─────────────────────────────────────────────── describe("entry boundary values", () => { async function setupUnlocked() { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); } it("entry with empty title is rejected by sanitizeEntryDraft", async () => { // Product decision: an entry with no title is unusable in the UI list, // so sanitizeEntryDraft rejects it before persistence. await setupUnlocked(); const r = await dispatch({ type: "ADD_ENTRY", payload: { title: "", username: "u", password: "test", url: "https://e.com" }, }); expect(r.ok).toBe(false); }); // PCH-01-016: the batch-add path was skipping the MV-6 canary/tags whitelist. it("batch_add_drops_non_https_canary", async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); const [entry] = await vault.batchAddEntries([ { type: "login", title: "Acct", username: "u", password: "p", url: "https://e.com", canary: { webhookUrl: "http://evil.example" }, } as unknown as Parameters[0][number], ]); expect(entry.canary).toBeUndefined(); }); it("batch_add_filters_oversized_and_nonstring_tags", async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); const tags = [123, " ", "x".repeat(5000), ...Array(1000).fill("t")]; const [entry] = await vault.batchAddEntries([ { type: "login", title: "Tagged", username: "u", password: "p", url: "https://e.com", tags, } as unknown as Parameters[0][number], ]); expect(entry.tags).toBeDefined(); expect(entry.tags!.length).toBeLessThanOrEqual(255); expect(entry.tags!.every((t) => typeof t === "string")).toBe(true); expect(entry.tags!.every((t) => t.length <= 256)).toBe(true); // 123 and " " dropped; "x".repeat(5000) truncated to 256 chars. expect(entry.tags).toContain("x".repeat(256)); }); const titleCases = [ { name: "1 char title", title: "x" }, { name: "100 char title", title: "x".repeat(100) }, { name: "1000 char title", title: "x".repeat(1000) }, { name: "10000 char title", title: "x".repeat(10000) }, { name: "unicode title", title: "日本語 🔑 Ünïcödé" }, { name: "emoji title", title: "🔑🔒🛡️🌸" }, { name: "RTL title", title: "كلمة المرور" }, { name: "tab in title", title: "Tab\there" }, { name: "newline in title", title: "Line1\nLine2" }, { name: "html-looking title", title: "" }, { name: "sql-looking title", title: "' OR '1'='1" }, { name: "null character in title", title: "before\x00after" }, { name: "control chars title", title: "\x01\x02\x03\x04" }, ]; for (const { name, title } of titleCases) { it(`entry with ${name} is handled without crash`, async () => { await setupUnlocked(); const r = await dispatch({ type: "ADD_ENTRY", payload: { title, username: "u", password: "p", url: "https://e.com" }, }); expect(r).toBeDefined(); }); it(`entry with ${name} round-trips title via GET_ENTRIES`, async () => { await setupUnlocked(); await dispatch({ type: "ADD_ENTRY", payload: { title, username: "u", password: "p", url: "https://e.com" }, }); const list = await dispatch({ type: "GET_ENTRIES" }); expect(list.ok).toBe(true); const entries = (list as { data: Array<{ title: string }> }).data; expect(entries.length).toBe(1); // Title is preserved as-is (no sanitisation/escaping). expect(entries[0].title).toBe(title); }); } const urlCases = [ { name: "https URL", url: "https://github.com" }, { name: "http URL", url: "http://example.com" }, { name: "javascript URL", url: "javascript:alert('xss')" }, { name: "data URL", url: "data:text/html," }, { name: "file URL", url: "file:///etc/passwd" }, { name: "empty URL", url: "" }, { name: "spaces URL", url: " " }, { name: "ftp URL", url: "ftp://example.com" }, { name: "IPv6 URL", url: "https://[::1]" }, { name: "IDN URL", url: "https://日本.jp" }, { name: "very long URL", url: "https://example.com/" + "a".repeat(5000) }, ]; for (const { name, url } of urlCases) { it(`entry with ${name} stored as-is`, async () => { await setupUnlocked(); const r = await dispatch({ type: "ADD_ENTRY", payload: { title: "t", username: "u", password: "p", url }, }); expect(r.ok).toBe(true); }); } it("100 entries: count consistent", async () => { await setupUnlocked(); for (let i = 0; i < 100; i++) { await dispatch({ type: "ADD_ENTRY", payload: { title: `E${i}`, username: "u", password: `p${i}`, url: "https://e.com" }, }); } const list = await dispatch({ type: "GET_ENTRIES" }); expect((list as { data: unknown[] }).data.length).toBe(100); }); it("vault remains unlockable after many entries", async () => { await setupUnlocked(); const vault = await import("./vault"); for (let i = 0; i < 50; i++) { await vault.addEntry({ title: `E${i}`, username: "u", password: `p${i}`, url: "https://e.com", type: "login", }); } vault.lock(); const unlock = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(unlock.ok).toBe(true); const list = await dispatch({ type: "GET_ENTRIES" }); expect((list as { data: unknown[] }).data.length).toBe(50); }); it("add then delete leaves no live entries", async () => { await setupUnlocked(); const r = await dispatch({ type: "ADD_ENTRY", payload: { title: "x", username: "u", password: "p", url: "https://e.com" }, }); const id = (r as { data: { id: string } }).data.id; await dispatch({ type: "DELETE_ENTRY", payload: { id } }); const list = await dispatch({ type: "GET_ENTRIES" }); expect((list as { data: unknown[] }).data.length).toBe(0); }); }); // ── Concurrent operations ─────────────────────────────────────────────── describe("concurrent operations", () => { async function setupLocked() { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); vault.lock(); } it("simultaneous UNLOCK calls leave vault unlocked", async () => { await setupLocked(); const [r1, r2] = await Promise.all([ dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" } }), dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" } }), ]); expect(r1.ok || r2.ok).toBe(true); const status = await dispatch({ type: "IS_UNLOCKED" }); expect((status as { data: boolean }).data).toBe(true); }); it("simultaneous wrong UNLOCKs leave vault locked", async () => { await setupLocked(); await Promise.all([ dispatch({ type: "UNLOCK", payload: { password: "wrong-1" } }), dispatch({ type: "UNLOCK", payload: { password: "wrong-2" } }), ]); const status = await dispatch({ type: "IS_UNLOCKED" }); expect((status as { data: boolean }).data).toBe(false); }); it("ADD_ENTRY during LOCK does not corrupt vault", async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); await Promise.all([ dispatch({ type: "ADD_ENTRY", payload: { title: "t", username: "u", password: "p", url: "https://e.com" }, }).catch(() => null), dispatch({ type: "LOCK" }), ]); const unlock = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(unlock.ok).toBe(true); }); for (const count of [5, 10, 20, 50]) { it(`simultaneous ADD_ENTRY x ${count} produce that many entries`, async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); await Promise.all( Array.from({ length: count }, (_, i) => dispatch({ type: "ADD_ENTRY", payload: { title: `E${i}`, username: "u", password: "p", url: "https://e.com", }, }) ) ); const list = await dispatch({ type: "GET_ENTRIES" }); expect((list as { data: unknown[] }).data.length).toBe(count); }); } it("interleaved ADD + UPDATE keeps entry counts stable", async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); const r = await dispatch({ type: "ADD_ENTRY", payload: { title: "x", username: "u", password: "p", url: "https://e.com" }, }); const id = (r as { data: { id: string } }).data.id; await Promise.all([ dispatch({ type: "ADD_ENTRY", payload: { title: "y", username: "u", password: "p", url: "https://e.com" }, }), dispatch({ type: "UPDATE_ENTRY", payload: { id, patch: { username: "v" } } }), dispatch({ type: "ADD_ENTRY", payload: { title: "z", username: "u", password: "p", url: "https://e.com" }, }), ]); const list = await dispatch({ type: "GET_ENTRIES" }); expect((list as { data: unknown[] }).data.length).toBe(3); }); }); // ── Storage corruption ────────────────────────────────────────────────── describe("storage corruption", () => { async function setupVaultInStorage() { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); vault.lock(); } it("non-base64 slot A: unlock fails gracefully", async () => { await setupVaultInStorage(); storage.peach_vault_a = "not-a-valid-stored-vault" as unknown; const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(false); }); it("missing slot A: unlock fails", async () => { await setupVaultInStorage(); delete storage.peach_vault_a; const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(false); }); it("missing slot B: unlock still works from slot A", async () => { await setupVaultInStorage(); delete storage.peach_vault_b; const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(true); }); it("corrupt ciphertext in slot A: unlock fails", async () => { await setupVaultInStorage(); const a = storage.peach_vault_a as { ciphertext: string }; storage.peach_vault_a = { ...a, ciphertext: "AAAA" + a.ciphertext.slice(4) }; const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(false); }); it("corrupt salt: unlock fails", async () => { await setupVaultInStorage(); const a = storage.peach_vault_a as { salt: string }; storage.peach_vault_a = { ...a, salt: "00".repeat(16) }; const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(false); }); it("corrupt nonce: unlock fails", async () => { await setupVaultInStorage(); const a = storage.peach_vault_a as { nonce: string }; storage.peach_vault_a = { ...a, nonce: "ff".repeat(12) }; const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(false); }); it("corrupt tag: unlock fails", async () => { await setupVaultInStorage(); const a = storage.peach_vault_a as { tag: string }; storage.peach_vault_a = { ...a, tag: "00".repeat(16) }; const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(false); }); it("truncated ciphertext: unlock fails", async () => { await setupVaultInStorage(); const a = storage.peach_vault_a as { ciphertext: string }; storage.peach_vault_a = { ...a, ciphertext: a.ciphertext.slice(0, 20) }; const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(false); }); it("empty ciphertext: unlock fails", async () => { await setupVaultInStorage(); const a = storage.peach_vault_a as { ciphertext: string }; storage.peach_vault_a = { ...a, ciphertext: "" }; const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(false); }); for (const flipPos of [0, 4, 8, 16, 32, 64]) { it(`bit flip in ciphertext at offset ${flipPos}: unlock fails`, async () => { await setupVaultInStorage(); const a = storage.peach_vault_a as { ciphertext: string }; if (a.ciphertext.length <= flipPos + 1) return; const flipped = a.ciphertext.slice(0, flipPos) + (a.ciphertext[flipPos] === "0" ? "f" : "0") + a.ciphertext.slice(flipPos + 1); storage.peach_vault_a = { ...a, ciphertext: flipped }; const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(false); }); } it("after corruption, original storage record still exists (no wipe)", async () => { await setupVaultInStorage(); const a = storage.peach_vault_a as { ciphertext: string }; storage.peach_vault_a = { ...a, ciphertext: "00".repeat(64) }; await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(storage.peach_vault_a).toBeDefined(); }); }); // ── Unlock attempts / response shape ──────────────────────────────────── describe("repeated wrong unlock attempts", () => { async function setupLocked() { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); vault.lock(); } for (let attempt = 1; attempt <= 10; attempt++) { it(`attempt ${attempt}: wrong password returns ok=false`, async () => { await setupLocked(); for (let i = 0; i < attempt - 1; i++) { await dispatch({ type: "UNLOCK", payload: { password: "wrong" } }); } const r = await dispatch({ type: "UNLOCK", payload: { password: "wrong" }, }); expect(r.ok).toBe(false); }); } it( "vault data is preserved across 25 wrong attempts", async () => { await setupLocked(); for (let i = 0; i < 25; i++) { await dispatch({ type: "UNLOCK", payload: { password: "wrong" } }); } expect(storage.peach_vault_a).toBeTruthy(); // Correct password still works. const ok = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(ok.ok).toBe(true); }, 60_000, ); it( "vault data is preserved across 50 wrong attempts", async () => { await setupLocked(); for (let i = 0; i < 50; i++) { await dispatch({ type: "UNLOCK", payload: { password: "wrong" } }); } expect(storage.peach_vault_a).toBeTruthy(); }, 120_000, ); it("successful unlock after wrong attempts produces unlocked state", async () => { await setupLocked(); for (let i = 0; i < 5; i++) { await dispatch({ type: "UNLOCK", payload: { password: "wrong" } }); } const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(true); const status = await dispatch({ type: "IS_UNLOCKED" }); expect((status as { data: boolean }).data).toBe(true); }); it("incorrect-then-correct: state is unlocked", async () => { await setupLocked(); await dispatch({ type: "UNLOCK", payload: { password: "wrong" } }); const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(true); }); }); // ── Generated random password set ─────────────────────────────────────── describe("generated random password fuzz round-trips", () => { // 50 deterministic randomly-shaped passwords; each must round-trip. const seeded: string[] = []; let rng = 0xdeadbeef; function next() { rng = (rng * 1103515245 + 12345) & 0x7fffffff; return rng; } for (let i = 0; i < 50; i++) { const len = (next() % 30) + 4; let s = ""; for (let j = 0; j < len; j++) { s += String.fromCharCode((next() % 94) + 33); } seeded.push(s); } for (const [i, password] of seeded.entries()) { it(`fuzz password #${i} round-trips`, async () => { const vault = await import("./vault"); await vault.createVault(password); vault.lock(); const r = await dispatch({ type: "UNLOCK", payload: { password } }); expect(r.ok).toBe(true); }); it(`fuzz password #${i} wrong rejects`, async () => { const vault = await import("./vault"); await vault.createVault(password); vault.lock(); const r = await dispatch({ type: "UNLOCK", payload: { password: password.slice(0, -1) + "Z" }, }); expect(r.ok).toBe(false); }); } }); // ── ADD/DELETE/UPDATE high-volume parameterized ───────────────────────── describe("entry lifecycle parameterized", () => { for (const n of [1, 2, 3, 5, 10, 25, 50]) { it(`add ${n} entries then delete all leaves no live entries`, async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); const ids: string[] = []; for (let i = 0; i < n; i++) { const r = await dispatch({ type: "ADD_ENTRY", payload: { title: `E${i}`, username: "u", password: "p", url: "https://e.com", }, }); ids.push((r as { data: { id: string } }).data.id); } for (const id of ids) { await dispatch({ type: "DELETE_ENTRY", payload: { id } }); } const list = await dispatch({ type: "GET_ENTRIES" }); expect((list as { data: unknown[] }).data.length).toBe(0); }); it(`add ${n} entries then update each title`, async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); const ids: string[] = []; for (let i = 0; i < n; i++) { const r = await dispatch({ type: "ADD_ENTRY", payload: { title: `E${i}`, username: "u", password: "p", url: "https://e.com", }, }); ids.push((r as { data: { id: string } }).data.id); } for (const [i, id] of ids.entries()) { await dispatch({ type: "UPDATE_ENTRY", payload: { id, patch: { title: `Updated${i}` } }, }); } const list = await dispatch({ type: "GET_ENTRIES" }); const titles = ( list as { data: Array<{ title: string }> } ).data.map((e) => e.title); for (let i = 0; i < n; i++) { expect(titles).toContain(`Updated${i}`); } }); } }); // ── lock/unlock churn ─────────────────────────────────────────────────── describe("lock/unlock churn", () => { for (const cycles of [1, 2, 3, 5, 10]) { it(`${cycles} lock/unlock cycles preserve entries`, async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); await vault.addEntry({ title: "Persistent", username: "u", password: "p", url: "https://e.com", type: "login", }); for (let i = 0; i < cycles; i++) { vault.lock(); const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(true); } const list = await dispatch({ type: "GET_ENTRIES" }); expect((list as { data: unknown[] }).data.length).toBe(1); }); } }); // ── Storage shape adversarial — every slot field corrupted ────────────── describe("per-field corruption matrix", () => { async function setup() { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); vault.lock(); } const fields = [ "salt", "nonce", "ciphertext", "tag", "version", ]; const corruptions = [ { name: "empty", value: "" }, { name: "garbage hex", value: "zzzz" }, { name: "wrong length 1", value: "ab" }, { name: "wrong length long", value: "ab".repeat(200) }, { name: "non-string number", value: 42 as unknown as string }, { name: "non-string null", value: null as unknown as string }, ]; for (const field of fields) { for (const { name, value } of corruptions) { it(`field=${field} corruption=${name}: unlock fails or response defined`, async () => { await setup(); const a = storage.peach_vault_a as Record; storage.peach_vault_a = { ...a, [field]: value }; const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r).toBeDefined(); // Cannot unlock unless the field was already correct. if ( field !== "version" || (typeof value !== "string" && value !== null) ) { expect(r.ok).toBe(false); } }); } } }); // ── Corruption distinguished from wrong password (M7/COR-007) ─────────── describe("vault corruption surfaces vault_corrupted error", () => { async function setup() { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); vault.lock(); } it("schema-invalid slot (version=42) returns vault_corrupted with canRestoreFromHistory", async () => { await setup(); const a = storage.peach_vault_a as Record; storage.peach_vault_a = { ...a, version: 42 }; const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(false); expect((r as { error?: string }).error).toBe("vault_corrupted"); expect((r as { canRestoreFromHistory?: boolean }).canRestoreFromHistory).toBe(true); }); it("wrong password (correct schema) does NOT return vault_corrupted", async () => { await setup(); const r = await dispatch({ type: "UNLOCK", payload: { password: "wrong-password" }, }); expect(r.ok).toBe(false); expect((r as { error?: string }).error).not.toBe("vault_corrupted"); }); }); // ── Many "add then lock then unlock" sequences ────────────────────────── describe("add-then-relock churn", () => { for (let seed = 0; seed < 20; seed++) { it(`seed ${seed}: add 3 entries with seeded titles survives lock/unlock`, async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); for (let i = 0; i < 3; i++) { await vault.addEntry({ title: `seed-${seed}-${i}`, username: `u${i}`, password: `p${i}`, url: `https://e${seed}.com`, type: "login", }); } vault.lock(); const r = await dispatch({ type: "UNLOCK", payload: { password: "test-pass-12345" }, }); expect(r.ok).toBe(true); const list = await dispatch({ type: "GET_ENTRIES" }); expect((list as { data: unknown[] }).data.length).toBe(3); }); } }); // ── Sanity / no-crash checks across many invocations ──────────────────── describe("UNLOCK no-crash matrix", () => { const adversarialPayloads = [ { name: "null password", payload: { password: null as unknown as string } }, { name: "undefined password", payload: { password: undefined as unknown as string } }, { name: "number password", payload: { password: 12345 as unknown as string } }, { name: "bool password", payload: { password: true as unknown as string } }, { name: "array password", payload: { password: ["a", "b"] as unknown as string } }, { name: "object password", payload: { password: { x: 1 } as unknown as string } }, { name: "missing password field", payload: {} as { password: string } }, { name: "empty password", payload: { password: "" } }, ]; for (const { name, payload } of adversarialPayloads) { it(`UNLOCK with ${name} resolves without crash`, async () => { const vault = await import("./vault"); await vault.createVault("test-pass-12345"); vault.lock(); const r = await dispatch({ type: "UNLOCK", payload }); expect(r).toBeDefined(); }); } }); });