const VERSION_BYTE = new Uint8Array([1]); function hex(buf: ArrayBuffer | Uint8Array): string { const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf); return Array.from(bytes) .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); } function unhex(value: string): Uint8Array { const out = new Uint8Array(value.length / 2); for (let i = 0; i < value.length; i += 2) { out[i / 2] = parseInt(value.slice(i, i + 2), 16); } return out; } export async function encrypt( keyBytes: Uint8Array, plaintext: string ): Promise<{ nonce: string; ciphertext: string; tag: string }> { const key = await crypto.subtle.importKey( "raw", keyBytes.buffer as ArrayBuffer, { name: "AES-GCM" }, false, ["encrypt"] ); const nonce = crypto.getRandomValues(new Uint8Array(12)); const encoded = new TextEncoder().encode(plaintext); const raw = await crypto.subtle.encrypt( { name: "AES-GCM", iv: nonce, additionalData: VERSION_BYTE.buffer as ArrayBuffer, tagLength: 128, }, key, encoded ); const full = new Uint8Array(raw); const ciphertext = full.slice(0, full.length - 16); const tag = full.slice(full.length - 16); return { nonce: hex(nonce.buffer), ciphertext: hex(ciphertext.buffer), tag: hex(tag.buffer), }; } export async function decrypt( keyBytes: Uint8Array, nonce: string, ciphertext: string, tag: string ): Promise { const key = await crypto.subtle.importKey( "raw", keyBytes.buffer as ArrayBuffer, { name: "AES-GCM" }, false, ["decrypt"] ); const cipherBytes = unhex(ciphertext); const tagBytes = unhex(tag); const combined = new Uint8Array(cipherBytes.length + tagBytes.length); combined.set(cipherBytes); combined.set(tagBytes, cipherBytes.length); const raw = await crypto.subtle.decrypt( { name: "AES-GCM", iv: unhex(nonce).buffer as ArrayBuffer, additionalData: VERSION_BYTE.buffer as ArrayBuffer, tagLength: 128, }, key, combined.buffer as ArrayBuffer ); return new TextDecoder().decode(raw); } export function zeroKey(keyBytes: Uint8Array): void { keyBytes.fill(0); }