const crypto = require("crypto"); /** * crypto_utils.js * Hybrid Encryption: AES-256-GCM + RSA-OAEP + SHA256withRSA * * Functions Exposed: * encryptString(text) * decryptString(encryptedText) * * Required ENV variables: * PRIVATE_KEY (PKCS8 private key) * PUBLIC_CERT (X.509 certificate) */ /* ------------------------------------------------------- PEM NORMALIZATION HELPERS (for raw Base64 input) -------------------------------------------------------- */ function wrapPrivateKey(key) { if (!key) throw new Error("Missing PRIVATE_KEY"); if (key.includes("BEGIN")) return key; // Already PEM const clean = key.replace(/\s+/g, ""); return `-----BEGIN PRIVATE KEY----- ${clean.match(/.{1,64}/g).join("\n")} -----END PRIVATE KEY-----`; } function wrapCertificate(cert) { if (!cert) throw new Error("Missing PUBLIC_CERT"); if (cert.includes("BEGIN")) return cert; // Already PEM const clean = cert.replace(/\s+/g, ""); return `-----BEGIN CERTIFICATE----- ${clean.match(/.{1,64}/g).join("\n")} -----END CERTIFICATE-----`; } /* ------------------------------------------------------- LOAD KEYS -------------------------------------------------------- */ const PRIVATE_KEY = wrapPrivateKey(process.env.PRIVATE_KEY); const PUBLIC_CERT = wrapCertificate(process.env.PUBLIC_CERT); /* ------------------------------------------------------- AES-256-GCM IMPLEMENTATION -------------------------------------------------------- */ function aesEncrypt(plainText, key, iv) { const cipher = crypto.createCipheriv("aes-256-gcm", key, iv); const encrypted = Buffer.concat([ cipher.update(plainText, "utf8"), cipher.final() ]); const tag = cipher.getAuthTag(); return Buffer.concat([encrypted, tag]).toString("base64"); } function aesDecrypt(cipherBase64, key, iv) { const buffer = Buffer.from(cipherBase64, "base64"); const tag = buffer.slice(buffer.length - 16); const ciphertext = buffer.slice(0, buffer.length - 16); const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv); decipher.setAuthTag(tag); const decrypted = Buffer.concat([ decipher.update(ciphertext), decipher.final() ]); return decrypted.toString("utf8"); } /* ------------------------------------------------------- RSA HELPERS -------------------------------------------------------- */ function rsaEncryptBase64(str) { return crypto .publicEncrypt( { key: PUBLIC_CERT, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(str, "utf8") ) .toString("base64"); } function rsaDecryptToString(b64) { return crypto .privateDecrypt( { key: PRIVATE_KEY, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(b64, "base64") ) .toString("utf8"); } /* ------------------------------------------------------- SIGN / VERIFY HELPERS -------------------------------------------------------- */ function signData(data) { const sign = crypto.createSign("RSA-SHA256"); sign.update(data); sign.end(); return sign.sign(PRIVATE_KEY).toString("base64"); } function verifyData(data, signatureB64) { const verify = crypto.createVerify("RSA-SHA256"); verify.update(data); verify.end(); return verify.verify(PUBLIC_CERT, Buffer.from(signatureB64, "base64")); } /* ------------------------------------------------------- PUBLIC FUNCTIONS -------------------------------------------------------- */ /** * Encrypt a plain string using: * AES-256-GCM + RSA-OAEP + SHA256withRSA */ function encryptStringB2C(text) { try { const aesKey = crypto.randomBytes(32); // 256-bit const iv = crypto.randomBytes(16); // 128-bit // AES encrypt const aesCipher = aesEncrypt(text, aesKey, iv); // Signature over AES ciphertext (Base64) const signature = signData(aesCipher); // RSA encrypted AES key const encryptedKey = rsaEncryptBase64(aesKey.toString("base64")); // Assemble: header : iv : cipher : signature const payload = [ encryptedKey, iv.toString("base64"), aesCipher, signature ].join(":"); // Final base64 wrapper return Buffer.from(payload, "utf8").toString("base64"); } catch (err) { console.error("Encryption error:", err.message); throw new Error("Failed to encrypt data"); } } /** * Decrypt the hybrid AES/RSA encrypted Base64 payload */ function decryptStringB2C(encryptedText) { try { const decoded = Buffer.from(encryptedText, "base64").toString("utf8"); const parts = decoded.split(":"); if (parts.length < 4) throw new Error("Invalid encrypted payload"); const [encryptedKey, iv64, cipher64, signature64] = parts; // RSA decrypt AES key const aesKeyBase64 = rsaDecryptToString(encryptedKey); const aesKey = Buffer.from(aesKeyBase64, "base64"); // Verify signature if (!verifyData(cipher64, signature64)) { throw new Error("Signature verification failed"); } const iv = Buffer.from(iv64, "base64"); // AES decrypt return aesDecrypt(cipher64, aesKey, iv); } catch (err) { console.error("Decryption error:", err.message); throw new Error("Failed to decrypt data"); } } /* ------------------------------------------------------- EXPORTS -------------------------------------------------------- */ module.exports = { encryptStringB2C, decryptStringB2C };