import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto' const GCM_IV_LENGTH = 12 // 96 bits const GCM_TAG_LENGTH = 16 // 128 bits const ALGORITHM = 'aes-256-gcm' /** * AES-256-GCM encrypt. * * Output format (before Base64): [IV (12 bytes)] + [ciphertext] + [auth tag (16 bytes)] * * @param plaintext - UTF-8 string to encrypt * @param key - 32-byte AES key * @returns Base64-encoded encrypted data */ export const aesGcmEncrypt = (plaintext: string, key: Buffer): string => { const iv = randomBytes(GCM_IV_LENGTH) const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: GCM_TAG_LENGTH }) const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]) const tag = cipher.getAuthTag() // Layout: IV + ciphertext + tag const combined = Buffer.concat([iv, encrypted, tag]) return combined.toString('base64') } /** * AES-256-GCM decrypt. * * Input format (after Base64 decode): [IV (12 bytes)] + [ciphertext] + [auth tag (16 bytes)] * * @param encryptedBase64 - Base64-encoded encrypted data * @param key - 32-byte AES key * @returns Decrypted UTF-8 string */ export const aesGcmDecrypt = (encryptedBase64: string, key: Buffer): string => { const data = Buffer.from(encryptedBase64, 'base64') if (data.length < GCM_IV_LENGTH + GCM_TAG_LENGTH) { throw new Error('Encrypted data too short: must contain IV + tag at minimum') } const iv = data.subarray(0, GCM_IV_LENGTH) const tag = data.subarray(data.length - GCM_TAG_LENGTH) const ciphertext = data.subarray(GCM_IV_LENGTH, data.length - GCM_TAG_LENGTH) const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: GCM_TAG_LENGTH }) decipher.setAuthTag(tag) const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]) return decrypted.toString('utf-8') }