12345678910111213141516const VERSION_BYTE = new Uint8Array([1]);1718function aadFor(aadTag?: string): Uint8Array {19 if (!aadTag) return VERSION_BYTE;20 return new TextEncoder().encode(aadTag);21}2223function hex(buf: ArrayBuffer | Uint8Array): string {24 const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);25 return Array.from(bytes)26 .map((byte) => byte.toString(16).padStart(2, "0"))27 .join("");28}2930function unhex(value: string): Uint8Array {31 const out = new Uint8Array(value.length / 2);32 for (let i = 0; i < value.length; i += 2) {33 out[i / 2] = parseInt(value.slice(i, i + 2), 16);34 }35 return out;36}3738export async function encrypt(39 keyBytes: Uint8Array,40 plaintext: string,41 aadTag?: string42): Promise<{ nonce: string; ciphertext: string; tag: string }> {43 const key = await crypto.subtle.importKey(44 "raw",45 keyBytes.buffer as ArrayBuffer,46 { name: "AES-GCM" },47 false,48 ["encrypt"]49 );5051 52 53 54 55 56 const nonce = crypto.getRandomValues(new Uint8Array(12));57 const encoded = new TextEncoder().encode(plaintext);58 const aad = aadFor(aadTag);59 const raw = await crypto.subtle.encrypt(60 {61 name: "AES-GCM",62 iv: nonce,63 additionalData: aad.buffer.slice(aad.byteOffset, aad.byteOffset + aad.byteLength) as ArrayBuffer,64 tagLength: 128,65 },66 key,67 encoded68 );6970 71 72 const full = new Uint8Array(raw);73 const ciphertext = full.slice(0, full.length - 16);74 const tag = full.slice(full.length - 16);7576 return {77 nonce: hex(nonce.buffer),78 ciphertext: hex(ciphertext.buffer),79 tag: hex(tag.buffer),80 };81}8283export async function decrypt(84 keyBytes: Uint8Array,85 nonce: string,86 ciphertext: string,87 tag: string,88 aadTag?: string89): Promise<string> {90 const tryDecrypt = async (aad: Uint8Array): Promise<string> =>91 new TextDecoder().decode(92 await decryptWithAadBytes(keyBytes, nonce, ciphertext, tag, aad),93 );9495 try {96 return await tryDecrypt(aadFor(aadTag));97 } catch (err) {98 99 100 101 102 if (aadTag) {103 return await tryDecrypt(VERSION_BYTE);104 }105 throw err;106 }107}108109async function decryptWithAadBytes(110 keyBytes: Uint8Array,111 nonce: string,112 ciphertext: string,113 tag: string,114 aad: Uint8Array,115): Promise<Uint8Array> {116 const key = await crypto.subtle.importKey(117 "raw",118 keyBytes.buffer as ArrayBuffer,119 { name: "AES-GCM" },120 false,121 ["decrypt"]122 );123124 const cipherBytes = unhex(ciphertext);125 const tagBytes = unhex(tag);126 const combined = new Uint8Array(cipherBytes.length + tagBytes.length);127 combined.set(cipherBytes);128 combined.set(tagBytes, cipherBytes.length);129130 const raw = await crypto.subtle.decrypt(131 {132 name: "AES-GCM",133 iv: unhex(nonce).buffer as ArrayBuffer,134 additionalData: aad.buffer.slice(135 aad.byteOffset,136 aad.byteOffset + aad.byteLength,137 ) as ArrayBuffer,138 tagLength: 128,139 },140 key,141 combined.buffer as ArrayBuffer,142 );143 return new Uint8Array(raw);144}145146export const _testSeam = { decryptWithAadBytes };147148export function zeroKey(keyBytes: Uint8Array): void {149 keyBytes.fill(0);150}