1234567891011121314151617const KAT_PLAINTEXT = "peach self-test vector v1";18const KAT_PASSWORD = "kat-fixed-password-do-not-use";1920const KAT_SALT_HEX = "1719d355fb0e9b33cc95bca2298a00c9";2122const KAT_NONCE_HEX = "6b65792d6e6f6e63652d3031";232425262728const KAT_EXPECTED_CIPHERTEXT_HEX =29 "fb79bd9b46fbdc39e359ec9cac91d054b89d393e36801f9e0511adbe07b06a06273b45a8135a59a839";3031function hexToBytes(hex: string): Uint8Array<ArrayBuffer> {32 const buf = new ArrayBuffer(hex.length / 2);33 const out = new Uint8Array(buf);34 for (let i = 0; i < out.length; i++) {35 out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);36 }37 return out;38}3940function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {41 if (a.length !== b.length) return false;42 let diff = 0;43 for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!;44 return diff === 0;45}4647async function deriveKatKey(password: string, salt: Uint8Array<ArrayBuffer>): Promise<CryptoKey> {48 const enc = new TextEncoder();49 const keyMaterial = await crypto.subtle.importKey(50 "raw",51 enc.encode(password),52 "PBKDF2",53 false,54 ["deriveKey"]55 );56 return crypto.subtle.deriveKey(57 { name: "PBKDF2", salt, iterations: 1000, hash: "SHA-256" },58 keyMaterial,59 { name: "AES-GCM", length: 256 },60 false,61 ["encrypt", "decrypt"]62 );63}6465export async function runSelfTest(): Promise<{ ok: boolean; error?: string }> {66 try {67 const salt = hexToBytes(KAT_SALT_HEX);68 const nonce = hexToBytes(KAT_NONCE_HEX);69 const enc = new TextEncoder();70 const plaintext = enc.encode(KAT_PLAINTEXT);7172 const key = await deriveKatKey(KAT_PASSWORD, salt);7374 const ciphertext = await crypto.subtle.encrypt(75 { name: "AES-GCM", iv: nonce },76 key,77 plaintext78 );7980 81 82 83 const expected = hexToBytes(KAT_EXPECTED_CIPHERTEXT_HEX);84 const produced = new Uint8Array(ciphertext);85 if (!constantTimeEqual(produced, expected)) {86 return {87 ok: false,88 error: "KAT FAIL: ciphertext does not match the pinned test vector",89 };90 }9192 const decrypted = await crypto.subtle.decrypt(93 { name: "AES-GCM", iv: nonce },94 key,95 ciphertext96 );9798 const restored = new TextDecoder().decode(decrypted);99 if (restored !== KAT_PLAINTEXT) {100 return {101 ok: false,102 error: `KAT FAIL: expected "${KAT_PLAINTEXT}", got "${restored}"`,103 };104 }105106 return { ok: true };107 } catch (err) {108 return { ok: false, error: `KAT ERROR: ${String(err)}` };109 }110}