// ============================================================ // AES-256-GCM helpers for application-layer encryption of PHI // ============================================================ // Reads DATA_ENCRYPTION_KEY from env (hex, 32 bytes / 64 hex chars). // Generate one with: openssl rand -hex 32 // ============================================================ var crypto = require('crypto'); var KEY = null; var raw = process.env.DATA_ENCRYPTION_KEY; if (raw) { if (raw.length === 64 && /^[0-9a-fA-F]+$/.test(raw)) { KEY = Buffer.from(raw, 'hex'); } else if (raw.length >= 32) { // Fallback: derive a 32-byte key from any string via SHA-256. KEY = crypto.createHash('sha256').update(raw).digest(); console.warn('[crypto] DATA_ENCRYPTION_KEY is not 64 hex chars; derived via SHA-256.'); } } if (!KEY && (process.env.NODE_ENV === 'production' || process.env.APP_URL)) { console.error('[FATAL] DATA_ENCRYPTION_KEY is not set. Refusing to run in production.'); process.exit(1); } // Ciphertext format: "enc1:" + base64( iv(12) || authTag(16) || ciphertext ) var PREFIX = 'enc1:'; function isEncrypted(value) { return typeof value === 'string' && value.indexOf(PREFIX) === 0; } function encryptString(plaintext) { if (plaintext == null) return plaintext; if (!KEY) return plaintext; // dev mode: passthrough var iv = crypto.randomBytes(12); var cipher = crypto.createCipheriv('aes-256-gcm', KEY, iv); var ct = Buffer.concat([cipher.update(String(plaintext), 'utf8'), cipher.final()]); var tag = cipher.getAuthTag(); return PREFIX + Buffer.concat([iv, tag, ct]).toString('base64'); } function decryptString(value) { if (value == null) return value; if (!isEncrypted(value)) return value; // plaintext (legacy row) if (!KEY) throw new Error('Cannot decrypt: DATA_ENCRYPTION_KEY not set'); var buf = Buffer.from(String(value).slice(PREFIX.length), 'base64'); var iv = buf.subarray(0, 12); var tag = buf.subarray(12, 28); var ct = buf.subarray(28); var decipher = crypto.createDecipheriv('aes-256-gcm', KEY, iv); decipher.setAuthTag(tag); return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8'); } function encryptBuffer(buf) { if (!buf) return buf; if (!KEY) return buf; var iv = crypto.randomBytes(12); var cipher = crypto.createCipheriv('aes-256-gcm', KEY, iv); var ct = Buffer.concat([cipher.update(buf), cipher.final()]); var tag = cipher.getAuthTag(); // Version byte (0x01) + iv(12) + tag(16) + ciphertext return Buffer.concat([Buffer.from([0x01]), iv, tag, ct]); } function isEncryptedBuffer(buf) { return Buffer.isBuffer(buf) && buf.length > 29 && buf[0] === 0x01; } function decryptBuffer(buf) { if (!Buffer.isBuffer(buf)) return buf; if (!isEncryptedBuffer(buf)) return buf; // legacy (gzip without enc wrapper) if (!KEY) throw new Error('Cannot decrypt buffer: DATA_ENCRYPTION_KEY not set'); var iv = buf.subarray(1, 13); var tag = buf.subarray(13, 29); var ct = buf.subarray(29); var decipher = crypto.createDecipheriv('aes-256-gcm', KEY, iv); decipher.setAuthTag(tag); return Buffer.concat([decipher.update(ct), decipher.final()]); } module.exports = { encryptString: encryptString, decryptString: decryptString, isEncrypted: isEncrypted, encryptBuffer: encryptBuffer, decryptBuffer: decryptBuffer, isEncryptedBuffer: isEncryptedBuffer, hasKey: function() { return !!KEY; } };