import crypto from "node:crypto"; const ALGO = "aes-256-gcm"; const IV_LEN = 12; const TAG_LEN = 16; function readKey(envName: string): Buffer { const hex = process.env[envName]; if (!hex) throw new Error(`Missing env ${envName}`); if (hex.length !== 64) { throw new Error(`${envName} must be 64 hex chars (32 bytes)`); } return Buffer.from(hex, "hex"); } function getEncKey(): Buffer { return readKey("KYC_ENCRYPTION_KEY"); } function getNikPepper(): Buffer { return readKey("KYC_NIK_PEPPER"); } /** Encrypt a Buffer with AES-256-GCM. Output layout: [iv(12) | tag(16) | ciphertext]. */ export function encryptBuffer(plain: Buffer): Buffer { const iv = crypto.randomBytes(IV_LEN); const cipher = crypto.createCipheriv(ALGO, getEncKey(), iv); const ct = Buffer.concat([cipher.update(plain), cipher.final()]); const tag = cipher.getAuthTag(); return Buffer.concat([iv, tag, ct]); } export function decryptBuffer(blob: Buffer): Buffer { if (blob.length < IV_LEN + TAG_LEN) throw new Error("Ciphertext too short"); const iv = blob.subarray(0, IV_LEN); const tag = blob.subarray(IV_LEN, IV_LEN + TAG_LEN); const ct = blob.subarray(IV_LEN + TAG_LEN); const decipher = crypto.createDecipheriv(ALGO, getEncKey(), iv); decipher.setAuthTag(tag); return Buffer.concat([decipher.update(ct), decipher.final()]); } /** Encrypt UTF-8 string -> base64 string. Used for short PII like NIK. */ export function encryptString(plain: string): string { return encryptBuffer(Buffer.from(plain, "utf8")).toString("base64"); } export function decryptString(b64: string): string { return decryptBuffer(Buffer.from(b64, "base64")).toString("utf8"); } /** Deterministic HMAC-SHA256 of a normalized value, hex-encoded. Used for unique-lookup of NIK without storing plaintext. */ export function hmacHex(value: string): string { return crypto.createHmac("sha256", getNikPepper()).update(value).digest("hex"); }