import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto' /** * AES-256-GCM helpers for the Gmail refresh token at rest. The key is * HQ_SECRET_KEY (64 hex chars = 32 bytes); payload format is * `iv.tag.ciphertext` hex-joined by `.` so it stores in one TEXT column. * Never log the plaintext or the key. */ function keyFromHex(keyHex: string): Buffer { const key = Buffer.from(keyHex, 'hex') if (key.length !== 32 || keyHex.length !== 64) { throw new Error('Encryption key must be 64 hex chars (32 bytes)') } return key } export function encrypt(plain: string, keyHex: string): string { const iv = randomBytes(12) // 96-bit nonce, the GCM-recommended size const cipher = createCipheriv('aes-256-gcm', keyFromHex(keyHex), iv) const ciphertext = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]) return [iv.toString('hex'), cipher.getAuthTag().toString('hex'), ciphertext.toString('hex')].join('.') } export function decrypt(payload: string, keyHex: string): string { const [ivHex, tagHex, ctHex] = payload.split('.') if (ivHex === undefined || tagHex === undefined || ctHex === undefined) { throw new Error('Malformed encrypted payload (expected iv.tag.ciphertext)') } const decipher = createDecipheriv('aes-256-gcm', keyFromHex(keyHex), Buffer.from(ivHex, 'hex')) decipher.setAuthTag(Buffer.from(tagHex, 'hex')) return Buffer.concat([decipher.update(Buffer.from(ctHex, 'hex')), decipher.final()]).toString('utf8') }