export async function deriveKey(passphrase, saltBytes) { const enc = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey("raw", enc.encode(passphrase), { name: "PBKDF2" }, false, ["deriveKey"]); return crypto.subtle.deriveKey( { name: "PBKDF2", salt: saltBytes, iterations: 120_000, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"] ); } export async function encryptString(plaintext, passphrase) { const enc = new TextEncoder(); const salt = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(12)); const key = await deriveKey(passphrase, salt); const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, enc.encode(plaintext)); const version = new Uint8Array([1]); const out = new Uint8Array(1 + 16 + 12 + ct.byteLength); out.set(version, 0); out.set(salt, 1); out.set(iv, 17); out.set(new Uint8Array(ct), 29); return out; } export async function decryptToString(payload, passphrase) { const dec = new TextDecoder(); if (!(payload instanceof Uint8Array)) payload = new Uint8Array(payload); if (payload.length < 29) throw new Error("ciphertext too short"); if (payload[0] !== 1) throw new Error("unknown version"); const salt = payload.slice(1, 17), iv = payload.slice(17, 29), ct = payload.slice(29); const key = await deriveKey(passphrase, salt); const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct); return dec.decode(pt); } export function toBlob(data) { if (data instanceof Uint8Array) return new Blob([data], { type: "application/octet-stream" }); return new Blob([data], { type: "application/json;charset=utf-8" }); }