/** * @wrnexus/encryption — authenticated symmetric encryption (AES-256-GCM) via * WebCrypto, dependency-free. Use it to encrypt secrets, tokens, or database * fields at rest. * * const key = await generateKey(); // store this safely * const box = await encrypt("card #1234", key); // opaque base64 string * const plain = await decrypt(box, key); // "card #1234" * * A key derived from a password (PBKDF2) is also supported via `deriveKey`. */ const enc = new TextEncoder(); const dec = new TextDecoder(); const IV_BYTES = 12; function toB64(bytes: Uint8Array): string { let bin = ""; for (const b of bytes) bin += String.fromCharCode(b); return btoa(bin); } function fromB64(str: string): Uint8Array { const bin = atob(str); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; } function toHex(bytes: Uint8Array): string { let out = ""; for (const b of bytes) out += b.toString(16).padStart(2, "0"); return out; } /** SHA-256 hex digest of a string (e.g. content hashing, dedup keys). */ export async function sha256(data: string): Promise { const digest = await crypto.subtle.digest("SHA-256", enc.encode(data) as BufferSource); return toHex(new Uint8Array(digest)); } /** HMAC-SHA256 hex signature of `data` with `secret` (e.g. signing webhooks). */ export async function hmacSign(data: string, secret: string): Promise { const key = await crypto.subtle.importKey( "raw", enc.encode(secret) as BufferSource, { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const sig = await crypto.subtle.sign("HMAC", key, enc.encode(data) as BufferSource); return toHex(new Uint8Array(sig)); } /** Constant-time verify of an HMAC-SHA256 signature. */ export async function hmacVerify( data: string, secret: string, signature: string, ): Promise { const expected = await hmacSign(data, secret); if (expected.length !== signature.length) return false; let diff = 0; for (let i = 0; i < expected.length; i++) diff |= expected.charCodeAt(i) ^ signature.charCodeAt(i); return diff === 0; } /** Generate a random 256-bit key, base64-encoded. Store it as a secret. */ export async function generateKey(): Promise { const bytes = new Uint8Array(32); crypto.getRandomValues(bytes); return toB64(bytes); } async function importAesKey(key: string): Promise { const raw = fromB64(key); if (raw.length !== 32) throw new Error("Encryption key must be a base64 256-bit key"); return crypto.subtle.importKey("raw", raw as BufferSource, "AES-GCM", false, [ "encrypt", "decrypt", ]); } /** * Encrypt a string. Output is base64 of `iv(12) || ciphertext+tag`, safe to * store or transmit. Each call uses a fresh random IV. */ export async function encrypt(plaintext: string, key: string): Promise { const cryptoKey = await importAesKey(key); const iv = new Uint8Array(IV_BYTES); crypto.getRandomValues(iv); const ciphertext = new Uint8Array( await crypto.subtle.encrypt( { name: "AES-GCM", iv: iv as BufferSource }, cryptoKey, enc.encode(plaintext) as BufferSource, ), ); const packed = new Uint8Array(iv.length + ciphertext.length); packed.set(iv, 0); packed.set(ciphertext, iv.length); return toB64(packed); } /** Decrypt a value produced by `encrypt`. Throws if the key is wrong or data tampered. */ export async function decrypt(payload: string, key: string): Promise { const cryptoKey = await importAesKey(key); const packed = fromB64(payload); if (packed.length <= IV_BYTES) throw new Error("Invalid ciphertext"); const iv = packed.slice(0, IV_BYTES); const ciphertext = packed.slice(IV_BYTES); const plain = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv as BufferSource }, cryptoKey, ciphertext as BufferSource, ); return dec.decode(plain); } /** Derive a base64 AES key from a password + salt (PBKDF2, 100k iterations). */ export async function deriveKey(password: string, salt: string): Promise { const baseKey = await crypto.subtle.importKey( "raw", enc.encode(password) as BufferSource, "PBKDF2", false, ["deriveBits"], ); const bits = await crypto.subtle.deriveBits( { name: "PBKDF2", salt: enc.encode(salt) as BufferSource, iterations: 100_000, hash: "SHA-256", }, baseKey, 256, ); return toB64(new Uint8Array(bits)); }