self-test.ts

2.6 KB · raw

1/**2 * self-test.ts — Known-Answer Test (KAT) for the Peach crypto stack.3 *4 * Runs on every vault unlock. Catches runtime environments where WebCrypto5 * has been silently broken (corrupted extension, browser bug, bad update).6 * Uses PBKDF2 + AES-GCM directly rather than Argon2id so it completes in7 * < 5 ms — the same encrypt/decrypt path the vault uses, verified with a8 * fixed test vector that must never change.9 *10 * If the KAT fails, the vault is locked and an error is surfaced. A user11 * who sees "self_test_failed" has a broken crypto environment and should12 * reinstall the extension.13 */1415// Fixed KAT vector — generated once, never changes.16// To regenerate: run `scripts/generate-kat.mjs` and replace these constants.17const KAT_PLAINTEXT = "peach self-test vector v1";18const KAT_PASSWORD = "kat-fixed-password-do-not-use";19// 16-byte salt as hex20const KAT_SALT_HEX = "1719d355fb0e9b33cc95bca2298a00c9";21// 12-byte nonce as hex ("key-nonce-01")22const KAT_NONCE_HEX = "6b65792d6e6f6e63652d3031";2324function hexToBytes(hex: string): Uint8Array<ArrayBuffer> {25  const buf = new ArrayBuffer(hex.length / 2);26  const out = new Uint8Array(buf);27  for (let i = 0; i < out.length; i++) {28    out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);29  }30  return out;31}3233async function deriveKatKey(password: string, salt: Uint8Array<ArrayBuffer>): Promise<CryptoKey> {34  const enc = new TextEncoder();35  const keyMaterial = await crypto.subtle.importKey(36    "raw",37    enc.encode(password),38    "PBKDF2",39    false,40    ["deriveKey"]41  );42  return crypto.subtle.deriveKey(43    { name: "PBKDF2", salt, iterations: 1000, hash: "SHA-256" },44    keyMaterial,45    { name: "AES-GCM", length: 256 },46    false,47    ["encrypt", "decrypt"]48  );49}5051export async function runSelfTest(): Promise<{ ok: boolean; error?: string }> {52  try {53    const salt = hexToBytes(KAT_SALT_HEX);54    const nonce = hexToBytes(KAT_NONCE_HEX);55    const enc = new TextEncoder();56    const plaintext = enc.encode(KAT_PLAINTEXT);5758    const key = await deriveKatKey(KAT_PASSWORD, salt);5960    const ciphertext = await crypto.subtle.encrypt(61      { name: "AES-GCM", iv: nonce },62      key,63      plaintext64    );6566    const decrypted = await crypto.subtle.decrypt(67      { name: "AES-GCM", iv: nonce },68      key,69      ciphertext70    );7172    const restored = new TextDecoder().decode(decrypted);73    if (restored !== KAT_PLAINTEXT) {74      return {75        ok: false,76        error: `KAT FAIL: expected "${KAT_PLAINTEXT}", got "${restored}"`,77      };78    }7980    return { ok: true };81  } catch (err) {82    return { ok: false, error: `KAT ERROR: ${String(err)}` };83  }84}