const IV_LENGTH = 12 function toBase64(bytes: Uint8Array): string { let binary = '' bytes.forEach((b) => { binary += String.fromCharCode(b) }) return btoa(binary) } function fromBase64(value: string): Uint8Array { const binary = atob(value) const bytes = new Uint8Array(binary.length) for (let i = 0; i < binary.length; i += 1) { bytes[i] = binary.charCodeAt(i) } return bytes } async function importKey(secret: string): Promise { const encoder = new TextEncoder() const keyMaterial = encoder.encode(secret) const hash = await crypto.subtle.digest('SHA-256', keyMaterial) return crypto.subtle.importKey('raw', hash, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']) } export interface EncryptedPayload { iv: string ciphertext: string } export async function encryptPayload(secret: string, value: unknown): Promise { const key = await importKey(secret) const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)) const encoder = new TextEncoder() const encoded = encoder.encode(JSON.stringify(value)) const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded) return { iv: toBase64(iv), ciphertext: toBase64(new Uint8Array(ciphertext)), } } export async function decryptPayload(secret: string, payload: EncryptedPayload): Promise { const key = await importKey(secret) const ivBytes = fromBase64(payload.iv) const cipherBytes = fromBase64(payload.ciphertext) const ivBuffer = ivBytes.buffer as ArrayBuffer const cipherBuffer = cipherBytes.buffer as ArrayBuffer const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: ivBuffer }, key, cipherBuffer) const decoder = new TextDecoder() return JSON.parse(decoder.decode(decrypted)) as T }