1234567891011121314151617181920import { describe, expect, it } from "vitest";21import {22 tryDecryptSlotResult,23 type TryDecryptResult,24} from "../../background/vault";25import type { StoredVault } from "../../background/vault";26272829303132333435async function aesGcmEncrypt(36 key: CryptoKey,37 nonce: Uint8Array,38 plaintext: string,39 aad?: string,40): Promise<{ ciphertext: string; tag: string }> {41 const enc = new TextEncoder();42 const combined = await crypto.subtle.encrypt(43 {44 name: "AES-GCM",45 iv: nonce,46 additionalData: aad ? enc.encode(aad) : undefined,47 },48 key,49 enc.encode(plaintext),50 );51 52 const combined8 = new Uint8Array(combined);53 const ciphertext = combined8.slice(0, -16);54 const tag = combined8.slice(-16);55 return {56 ciphertext: btoa(String.fromCharCode(...ciphertext)),57 tag: btoa(String.fromCharCode(...tag)),58 };59}6061function bytesToB64(b: Uint8Array): string {62 return btoa(String.fromCharCode(...b));63}6465async function buildValidSlot(66 key: CryptoKey,67 json: string,68 aadTag = "slot_a_v1",69): Promise<StoredVault> {70 const nonce = crypto.getRandomValues(new Uint8Array(12));71 const { ciphertext, tag } = await aesGcmEncrypt(key, nonce, json, aadTag);72 return {73 version: 1,74 salt: bytesToB64(crypto.getRandomValues(new Uint8Array(16))),75 nonce: bytesToB64(nonce),76 ciphertext,77 tag,78 } as unknown as StoredVault;79}808182838485describe("T-018: vault corruption vs wrong-password discrimination", () => {86 87 88 it("T-018a: correct key + valid JSON → status ok", async () => {89 const key = await crypto.subtle.generateKey(90 { name: "AES-GCM", length: 256 },91 false,92 ["encrypt", "decrypt"],93 );9495 const validPayload = JSON.stringify({96 entries: [],97 version: 1,98 99 });100 const slot = await buildValidSlot(key, validPayload);101 const rawKey = new Uint8Array(102 await crypto.subtle.exportKey("raw", key),103 );104105 const result = await tryDecryptSlotResult(slot, rawKey, "slot_a_v1");106 expect(result.status).toBe("ok");107 });108109 110 111 it("T-018b: wrong key → status wrong_key", async () => {112 const correctKey = await crypto.subtle.generateKey(113 { name: "AES-GCM", length: 256 },114 false,115 ["encrypt", "decrypt"],116 );117 const wrongKey = await crypto.subtle.generateKey(118 { name: "AES-GCM", length: 256 },119 true,120 ["encrypt", "decrypt"],121 );122123 const slot = await buildValidSlot(correctKey, JSON.stringify({ entries: [] }));124 const rawWrongKey = new Uint8Array(await crypto.subtle.exportKey("raw", wrongKey));125126 const result = await tryDecryptSlotResult(slot, rawWrongKey, "slot_a_v1");127 expect(result.status).toBe("wrong_key");128 });129130 131 132 133 134 135 136 137 138 it("T-018c: correct key + non-JSON plaintext → status corrupted, reason invalid_payload_json", async () => {139 const key = await crypto.subtle.generateKey(140 { name: "AES-GCM", length: 256 },141 true,142 ["encrypt", "decrypt"],143 );144 const rawKey = new Uint8Array(await crypto.subtle.exportKey("raw", key));145146 const slot = await buildValidSlot(key, "THIS IS NOT JSON {{{");147148 const result = await tryDecryptSlotResult(slot, rawKey, "slot_a_v1");149 expect(result.status).toBe("corrupted");150 if (result.status === "corrupted") {151 expect(result.reason).toBe("invalid_payload_json");152 }153 });154155 156 157 158 159 it("T-018d: correct key + valid JSON but non-array entries → status corrupted, reason invalid_payload_shape", async () => {160 const key = await crypto.subtle.generateKey(161 { name: "AES-GCM", length: 256 },162 true,163 ["encrypt", "decrypt"],164 );165 const rawKey = new Uint8Array(await crypto.subtle.exportKey("raw", key));166167 const slot = await buildValidSlot(key, JSON.stringify({ entries: "NOT_AN_ARRAY" }));168169 const result = await tryDecryptSlotResult(slot, rawKey, "slot_a_v1");170 expect(result.status).toBe("corrupted");171 if (result.status === "corrupted") {172 expect(result.reason).toBe("invalid_payload_shape");173 }174 });175176 177 178 179 180 181 182 it("T-018e: StoredVault missing required field → status corrupted, reason invalid_slot_schema", async () => {183 const badSlot = {184 version: 1,185 salt: "dGVzdA==",186 nonce: "dGVzdA==",187 ciphertext: "dGVzdA==",188 189 } as unknown as StoredVault;190191 const key = new Uint8Array(32).fill(0x42);192 const result = await tryDecryptSlotResult(badSlot, key, "slot_a_v1");193 expect(result.status).toBe("corrupted");194 if (result.status === "corrupted") {195 expect(result.reason).toBe("invalid_slot_schema");196 }197 });198199 200 201 202 203 204 205 206 it.todo(207 "T-018f: consumeLastUnlockFailureReason non-null after corrupted unlock, null after consumed (requires chrome mock + vault setup)",208 );209});