12345678910111213const VERSION_BYTE = new Uint8Array([1]);1415function aadFor(aadTag?: string): Uint8Array {16 if (!aadTag) return VERSION_BYTE;17 return new TextEncoder().encode(aadTag);18}1920function hex(buf: ArrayBuffer | Uint8Array): string {21 const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);22 return Array.from(bytes)23 .map((byte) => byte.toString(16).padStart(2, "0"))24 .join("");25}2627function unhex(value: string): Uint8Array {28 const out = new Uint8Array(value.length / 2);29 for (let i = 0; i < value.length; i += 2) {30 out[i / 2] = parseInt(value.slice(i, i + 2), 16);31 }32 return out;33}3435export async function encrypt(36 keyBytes: Uint8Array,37 plaintext: string,38 aadTag?: string39): Promise<{ nonce: string; ciphertext: string; tag: string }> {40 const key = await crypto.subtle.importKey(41 "raw",42 keyBytes.buffer as ArrayBuffer,43 { name: "AES-GCM" },44 false,45 ["encrypt"]46 );4748 49 50 51 52 53 const nonce = crypto.getRandomValues(new Uint8Array(12));54 const encoded = new TextEncoder().encode(plaintext);55 const aad = aadFor(aadTag);56 const raw = await crypto.subtle.encrypt(57 {58 name: "AES-GCM",59 iv: nonce,60 additionalData: aad.buffer.slice(aad.byteOffset, aad.byteOffset + aad.byteLength) as ArrayBuffer,61 tagLength: 128,62 },63 key,64 encoded65 );6667 68 69 const full = new Uint8Array(raw);70 const ciphertext = full.slice(0, full.length - 16);71 const tag = full.slice(full.length - 16);7273 return {74 nonce: hex(nonce.buffer),75 ciphertext: hex(ciphertext.buffer),76 tag: hex(tag.buffer),77 };78}7980export async function decrypt(81 keyBytes: Uint8Array,82 nonce: string,83 ciphertext: string,84 tag: string,85 aadTag?: string86): Promise<string> {87 const tryDecrypt = async (aad: Uint8Array): Promise<string> =>88 new TextDecoder().decode(89 await decryptWithAadBytes(keyBytes, nonce, ciphertext, tag, aad),90 );9192 return await tryDecrypt(aadFor(aadTag));93}9495async function decryptWithAadBytes(96 keyBytes: Uint8Array,97 nonce: string,98 ciphertext: string,99 tag: string,100 aad: Uint8Array,101): Promise<Uint8Array> {102 const key = await crypto.subtle.importKey(103 "raw",104 keyBytes.buffer as ArrayBuffer,105 { name: "AES-GCM" },106 false,107 ["decrypt"]108 );109110 const cipherBytes = unhex(ciphertext);111 const tagBytes = unhex(tag);112 const combined = new Uint8Array(cipherBytes.length + tagBytes.length);113 combined.set(cipherBytes);114 combined.set(tagBytes, cipherBytes.length);115116 const raw = await crypto.subtle.decrypt(117 {118 name: "AES-GCM",119 iv: unhex(nonce).buffer as ArrayBuffer,120 additionalData: aad.buffer.slice(121 aad.byteOffset,122 aad.byteOffset + aad.byteLength,123 ) as ArrayBuffer,124 tagLength: 128,125 },126 key,127 combined.buffer as ArrayBuffer,128 );129 return new Uint8Array(raw);130}131132export const _testSeam = { decryptWithAadBytes };133134export function zeroKey(keyBytes: Uint8Array): void {135 keyBytes.fill(0);136}