1const VERSION_BYTE = new Uint8Array([1]);23function hex(buf: ArrayBuffer | Uint8Array): string {4 const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);5 return Array.from(bytes)6 .map((byte) => byte.toString(16).padStart(2, "0"))7 .join("");8}910function unhex(value: string): Uint8Array {11 const out = new Uint8Array(value.length / 2);12 for (let i = 0; i < value.length; i += 2) {13 out[i / 2] = parseInt(value.slice(i, i + 2), 16);14 }15 return out;16}1718export async function encrypt(19 keyBytes: Uint8Array,20 plaintext: string21): Promise<{ nonce: string; ciphertext: string; tag: string }> {22 const key = await crypto.subtle.importKey(23 "raw",24 keyBytes.buffer as ArrayBuffer,25 { name: "AES-GCM" },26 false,27 ["encrypt"]28 );2930 const nonce = crypto.getRandomValues(new Uint8Array(12));31 const encoded = new TextEncoder().encode(plaintext);32 const raw = await crypto.subtle.encrypt(33 {34 name: "AES-GCM",35 iv: nonce,36 additionalData: VERSION_BYTE.buffer as ArrayBuffer,37 tagLength: 128,38 },39 key,40 encoded41 );4243 const full = new Uint8Array(raw);44 const ciphertext = full.slice(0, full.length - 16);45 const tag = full.slice(full.length - 16);4647 return {48 nonce: hex(nonce.buffer),49 ciphertext: hex(ciphertext.buffer),50 tag: hex(tag.buffer),51 };52}5354export async function decrypt(55 keyBytes: Uint8Array,56 nonce: string,57 ciphertext: string,58 tag: string59): Promise<string> {60 const key = await crypto.subtle.importKey(61 "raw",62 keyBytes.buffer as ArrayBuffer,63 { name: "AES-GCM" },64 false,65 ["decrypt"]66 );6768 const cipherBytes = unhex(ciphertext);69 const tagBytes = unhex(tag);70 const combined = new Uint8Array(cipherBytes.length + tagBytes.length);71 combined.set(cipherBytes);72 combined.set(tagBytes, cipherBytes.length);7374 const raw = await crypto.subtle.decrypt(75 {76 name: "AES-GCM",77 iv: unhex(nonce).buffer as ArrayBuffer,78 additionalData: VERSION_BYTE.buffer as ArrayBuffer,79 tagLength: 128,80 },81 key,82 combined.buffer as ArrayBuffer83 );8485 return new TextDecoder().decode(raw);86}8788export function zeroKey(keyBytes: Uint8Array): void {89 keyBytes.fill(0);90}