/** * AES-256-GCM for IMAP passwords. ENCRYPTION_KEY = 64 hex chars (32 bytes). * Legacy: if decrypt fails or key missing, value treated as plaintext. */ import crypto from 'crypto' const ALGO = 'aes-256-gcm' const IV_LEN = 16 const AUTH_TAG_LEN = 16 function getKeyBuffer() { const hex = process.env.ENCRYPTION_KEY || '' if (hex.length !== 64) { throw new Error('ENCRYPTION_KEY must be 64 hex characters (32 bytes). Generate: openssl rand -hex 32') } return Buffer.from(hex, 'hex') } export function encrypt(text) { if (text == null || text === '') return '' const key = getKeyBuffer() const iv = crypto.randomBytes(IV_LEN) const cipher = crypto.createCipheriv(ALGO, key, iv, { authTagLength: AUTH_TAG_LEN }) const enc = Buffer.concat([cipher.update(String(text), 'utf8'), cipher.final()]) const authTag = cipher.getAuthTag() const combined = Buffer.concat([iv, authTag, enc]) return combined.toString('base64url') } export function decrypt(encoded) { if (!encoded) return '' const buf = Buffer.from(String(encoded), 'base64url') if (buf.length < IV_LEN + AUTH_TAG_LEN + 1) { throw new Error('invalid ciphertext') } const key = getKeyBuffer() const iv = buf.subarray(0, IV_LEN) const authTag = buf.subarray(IV_LEN, IV_LEN + AUTH_TAG_LEN) const data = buf.subarray(IV_LEN + AUTH_TAG_LEN) const decipher = crypto.createDecipheriv(ALGO, key, iv, { authTagLength: AUTH_TAG_LEN }) decipher.setAuthTag(authTag) return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8') } /** Encrypt IMAP password when ENCRYPTION_KEY is set; otherwise store plaintext. */ export function encryptImapSecret(plain) { if (plain == null || plain === '') return '' if (!process.env.ENCRYPTION_KEY) return String(plain) try { return encrypt(plain) } catch (e) { logWarnOnce('encryptImapSecret', e.message) return String(plain) } } /** Decrypt IMAP secret; on failure return as plaintext (legacy). */ export function decryptImapSecret(stored) { if (stored == null || stored === '') return '' if (!process.env.ENCRYPTION_KEY) return String(stored) try { return decrypt(stored) } catch { return String(stored) } } let warnedEncrypt = false function logWarnOnce(tag, msg) { if (warnedEncrypt) return warnedEncrypt = true console.warn(`[crypto] ${tag}: ${msg}`) }