// AES-256-CBC encryption with a PBKDF2-derived key, using the WebCrypto API // (crypto.subtle) which is available both in Bun (server) and the browser // (client), so encryption/decryption is defined once for both sides. // Wire format: salt[16] | iv[16] | ciphertext. async function deriveKey( password: string, salt: Uint8Array, usage: KeyUsage[], ): Promise { const material = await crypto.subtle.importKey( "raw", new TextEncoder().encode(password), { name: "PBKDF2" }, false, ["deriveKey"], ); return crypto.subtle.deriveKey( { name: "PBKDF2", salt, iterations: 100000, hash: "SHA-512" }, material, { name: "AES-CBC", length: 256 }, false, usage, ); } export async function encrypt( content: Uint8Array, password: string, ): Promise { const iv = crypto.getRandomValues(new Uint8Array(16)); const salt = crypto.getRandomValues(new Uint8Array(16)); const key = await deriveKey(password, salt, ["encrypt"]); const ciphertext = await crypto.subtle.encrypt( { name: "AES-CBC", iv }, key, content, ); return new Uint8Array([...salt, ...iv, ...new Uint8Array(ciphertext)]); } export async function decrypt( data: Uint8Array, password: string, ): Promise { const salt = data.slice(0, 16); const iv = data.slice(16, 32); const key = await deriveKey(password, salt, ["decrypt"]); const plaintext = await crypto.subtle.decrypt( { name: "AES-CBC", iv }, key, data.slice(32), ); return new Uint8Array(plaintext); }