1234567891011const VERSION_BYTE = new Uint8Array([1]);1213function hex(buf: ArrayBuffer | Uint8Array): string {14 const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);15 return Array.from(bytes)16 .map((byte) => byte.toString(16).padStart(2, "0"))17 .join("");18}1920function unhex(value: string): Uint8Array {21 const out = new Uint8Array(value.length / 2);22 for (let i = 0; i < value.length; i += 2) {23 out[i / 2] = parseInt(value.slice(i, i + 2), 16);24 }25 return out;26}2728export async function encrypt(29 keyBytes: Uint8Array,30 plaintext: string31): Promise<{ nonce: string; ciphertext: string; tag: string }> {32 const key = await crypto.subtle.importKey(33 "raw",34 keyBytes.buffer as ArrayBuffer,35 { name: "AES-GCM" },36 false,37 ["encrypt"]38 );3940 41 42 43 44 45 const nonce = crypto.getRandomValues(new Uint8Array(12));46 const encoded = new TextEncoder().encode(plaintext);47 const raw = await crypto.subtle.encrypt(48 {49 name: "AES-GCM",50 iv: nonce,51 additionalData: VERSION_BYTE.buffer as ArrayBuffer,52 tagLength: 128,53 },54 key,55 encoded56 );5758 59 60 const full = new Uint8Array(raw);61 const ciphertext = full.slice(0, full.length - 16);62 const tag = full.slice(full.length - 16);6364 return {65 nonce: hex(nonce.buffer),66 ciphertext: hex(ciphertext.buffer),67 tag: hex(tag.buffer),68 };69}7071export async function decrypt(72 keyBytes: Uint8Array,73 nonce: string,74 ciphertext: string,75 tag: string76): Promise<string> {77 const key = await crypto.subtle.importKey(78 "raw",79 keyBytes.buffer as ArrayBuffer,80 { name: "AES-GCM" },81 false,82 ["decrypt"]83 );8485 const cipherBytes = unhex(ciphertext);86 const tagBytes = unhex(tag);87 const combined = new Uint8Array(cipherBytes.length + tagBytes.length);88 combined.set(cipherBytes);89 combined.set(tagBytes, cipherBytes.length);9091 const raw = await crypto.subtle.decrypt(92 {93 name: "AES-GCM",94 iv: unhex(nonce).buffer as ArrayBuffer,95 additionalData: VERSION_BYTE.buffer as ArrayBuffer,96 tagLength: 128,97 },98 key,99 combined.buffer as ArrayBuffer100 );101102 return new TextDecoder().decode(raw);103}104105export function zeroKey(keyBytes: Uint8Array): void {106 keyBytes.fill(0);107}