const crypto = require("crypto"); const { env } = require("../config/env"); const ALGORITHM = "aes-256-gcm"; const IV_LENGTH = 12; // 96-bit IV recommended for GCM function getKey() { const key = Buffer.from(env.fieldEncryptionKey || "", "hex"); if (key.length !== 32) { throw new Error( "FIELD_ENCRYPTION_KEY must be a 32-byte value encoded as 64 hex characters. " + "Generate one with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"" ); } return key; } /** * Encrypts a plaintext string for storage at rest (e.g. bank wire * reference numbers, Payoneer transaction IDs). Returns a single * base64 payload containing iv + authTag + ciphertext so it can be * stored as one string field in MongoDB. */ function encryptField(plaintext) { if (plaintext === undefined || plaintext === null || plaintext === "") return null; const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv(ALGORITHM, getKey(), iv); const ciphertext = Buffer.concat([cipher.update(String(plaintext), "utf8"), cipher.final()]); const authTag = cipher.getAuthTag(); return Buffer.concat([iv, authTag, ciphertext]).toString("base64"); } /** * Decrypts a payload produced by encryptField. Returns null if the * input is empty rather than throwing, so callers can treat "no * payment reference yet" gracefully. */ function decryptField(payload) { if (!payload) return null; const raw = Buffer.from(payload, "base64"); const iv = raw.subarray(0, IV_LENGTH); const authTag = raw.subarray(IV_LENGTH, IV_LENGTH + 16); const ciphertext = raw.subarray(IV_LENGTH + 16); const decipher = crypto.createDecipheriv(ALGORITHM, getKey(), iv); decipher.setAuthTag(authTag); const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]); return plaintext.toString("utf8"); } /** One-way hash used for equality lookups on encrypted fields (e.g. duplicate detection) without decrypting. */ function hmacIndex(value) { if (!value) return null; return crypto.createHmac("sha256", getKey()).update(String(value)).digest("hex"); } module.exports = { encryptField, decryptField, hmacIndex };