Spaces:
Sleeping
Sleeping
| 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 | |
| }; | |