UNPKG

cybersource-rest-client

Version:

Node.js SDK for the CyberSource REST API

479 lines (403 loc) 19.5 kB
'use strict' var ApiException = require('./ApiException'); var Constants = require('./Constants'); var fs = require('fs'); var forge = require('node-forge'); function jwkToPem(jwk, { private: isPrivate }) { const crypto = require('crypto'); try { if (isPrivate) { const keyObject = crypto.createPrivateKey({ key: jwk, format: 'jwk' }); return keyObject.export({ type: 'pkcs8', format: 'pem' }).toString(); } else { const keyObject = crypto.createPublicKey({ key: jwk, format: 'jwk' }); return keyObject.export({ type: 'spki', format: 'pem' }).toString(); } } catch (error) { console.error('Error converting JWK to PEM:', error); throw new Error('Failed to convert JWK to PEM'); } } exports.getResponseCodeMessage = function (responseCode) { var tempResponseCode = responseCode.toString(); switch (tempResponseCode) { case "200": tempResponseCode = Constants.STATUS200; break; case "201": tempResponseCode = Constants.STATUS200; break; case "400": tempResponseCode = Constants.STATUS400; break; case "401": tempResponseCode = Constants.STATUS401; break; case "403": tempResponseCode = Constants.STATUS403; break; case "404": tempResponseCode = Constants.STATUS404; break; case "500": tempResponseCode = Constants.STATUS500; break; case "502": tempResponseCode = Constants.STATUS502; break; case "503": tempResponseCode = Constants.STATUS503; break; case "504": tempResponseCode = Constants.STATUS504; break; default: tempResponseCode = null; } return tempResponseCode; } exports.isJsonString = function(jsonString){ try { JSON.parse(jsonString); return true; } catch (e) { return false; } } exports.loadPemCertificates = function (pemCertificatePath) { if (pemCertificatePath === null || pemCertificatePath === undefined) { return null; } const certs = pemCertificatePath.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/gm); return certs; } exports.findCertificateByAlias = function (certs, keyAlias) { if (certs === null || certs === undefined || keyAlias === null || keyAlias === undefined) { return null; } if (!Array.isArray(certs)) { ApiException.AuthException("Invalid certificate format. Expected an array of certificates."); } const forge = require('node-forge'); try { // Iterate through each certificate for (const cert of certs) { try { // Create an X509 certificate object const x509 = forge.pki.certificateFromPem(cert); // Extract the Common Name (CN) from the subject let commonName = null; const lowerKeyAlias = keyAlias.toLowerCase(); // In node-forge, subject is an object with attributes if (x509.subject && x509.subject.attributes) { for (const attr of x509.subject.attributes) { if (attr.name === 'commonName' || attr.shortName === 'CN') { commonName = attr.value; break; } } } if (commonName) { if (commonName.toLowerCase() === lowerKeyAlias) { return cert; } } } catch (e) { // Skip invalid certificates continue; } } // If we get here, no matching certificate was found ApiException.AuthException("Certificate with alias " + keyAlias + " not found in the provided PEM certificates."); } catch (e) { ApiException.AuthException("Error processing certificates: " + e.message); } } /** * Parses the MLE configuration string and returns an object indicating requestMLE and responseMLE flags. * @param {string} configString - The MLE configuration string in the format 'requestMLE::responseMLE' or 'requestMLE'. * @param {object} logger - Logger object for logging errors. * @returns {object} An object with requestMLE and optionally responseMLE boolean properties. * @throws Will throw an error if the configString format is invalid. */ exports.ParseMLEConfigString = function (configString, logger) { if (!configString?.trim()) { ApiException.ApiException("Unsupported empty. Expected format: 'requestMLE::responseMLE' or 'requestMLE' as true/false.", logger); } else if (configString.indexOf('::') != -1) { const parts = configString.split('::'); if (parts.length !== 2) { ApiException.ApiException("Invalid MLE control map value format. Expected format: true/false for 'requestMLE::responseMLE' but got: '" + configString + "'", logger); } const requestMLEPart = parts[0].trim(); const responseMLEPart = parts[1].trim(); if (requestMLEPart !== "" && ((requestMLEPart !== 'true' && requestMLEPart !== 'false'))) { ApiException.ApiException("Invalid MLE control map value format. Expected format: true/false for 'requestMLE::responseMLE' but got: '" + configString + "'", logger); } if (responseMLEPart !== "" && ((responseMLEPart !== 'true' && responseMLEPart !== 'false'))) { ApiException.ApiException("Invalid MLE control map value format. Expected format: true/false for 'requestMLE::responseMLE' but got: '" + configString + "'", logger); } // Create the result object const result = {}; // Only set requestMLE if requestMLEPart is not empty if (requestMLEPart !== "") { result.requestMLE = (requestMLEPart === 'true'); } // Only set responseMLE if responseMLEPart is not empty if (responseMLEPart !== "") { result.responseMLE = (responseMLEPart === 'true'); } return result; } else { if (configString === 'true' || configString === 'false') { const result = { requestMLE: configString === 'true' }; return result; } else { ApiException.ApiException("Invalid MLE control map value format: '" + configString + "'. Expected format: true/false for 'requestMLE' but got: '" + configString + "'", logger); } } } /** * Parses a P12 file and returns the pkcs12 object * @param {string} filePath - Path to the P12 file * @param {string} password - Password for the P12 file * @param {object} logger - Logger object for logging messages * @returns {object} - Parsed pkcs12 object * @throws Will throw an error if file reading or parsing fails */ exports.parseP12File = function(filePath, password, logger) { logger.debug(`Parsing P12 file: ${filePath}`); if (!fs.existsSync(filePath)) { logger.error(`File not found: ${filePath}`); throw new Error(Constants.FILE_NOT_FOUND + filePath); } var p12Buffer = fs.readFileSync(filePath); var p12Der = forge.util.binary.raw.encode(new Uint8Array(p12Buffer)); var p12Asn1 = forge.asn1.fromDer(p12Der); var p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, password); logger.debug(`Successfully parsed P12 file`); return p12; } /** * Reads a private key from a P12 file * @param {string} filePath - Path to the P12 file * @param {string} password - Password for the P12 file * @param {object} logger - Logger object for logging messages * @returns {string} - Private key in PEM format */ exports.readPrivateKeyFromP12 = function(filePath, password, logger) { try { logger.debug(`Reading private key from P12 file: ${filePath}`); var p12 = exports.parseP12File(filePath, password, logger); // Extract the private key var keyBags = p12.getBags({ bagType: forge.pki.oids.keyBag }); var bag = keyBags[forge.pki.oids.keyBag][0]; if (keyBags[forge.pki.oids.keyBag] === undefined || keyBags[forge.pki.oids.keyBag].length == 0) { logger.debug(`No key bag found, trying pkcs8ShroudedKeyBag`); keyBags = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag }); bag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag][0]; } var privateKey = bag.key; var rsaPrivateKey = forge.pki.privateKeyToPem(privateKey); logger.debug(`Successfully extracted private key from P12 file`); return rsaPrivateKey; } catch (error) { logger.error(`Error reading private key from P12 file: ${filePath}: ${error.message}`); ApiException.AuthException(`Error reading private key from P12 file: ${filePath}: ${error.message}. ${Constants.INCORRECT_KEY_PASS}`); } }; /** * Loads a private key from a PEM file * @param {string} filePath - Path to the PEM file * @param {string} password - Password for the encrypted PEM file (optional) * @param {object} logger - Logger object for logging messages * @returns {string} - Private key in PEM format */ exports.readPrivateKeyFromPemFile = function(filePath, password, logger) { try { logger.debug(`Reading private key from PEM file: ${filePath}`); if (!fs.existsSync(filePath)) { logger.error(`File not found: ${filePath}`); ApiException.AuthException(Constants.FILE_NOT_FOUND + filePath); } // Read the PEM file var pemData = fs.readFileSync(filePath, 'utf8'); logger.debug(`Successfully read PEM file`); // Check if the private key is encrypted var isEncrypted = pemData.includes('ENCRYPTED'); logger.debug(`PEM file contains ${isEncrypted ? 'an encrypted' : 'an unencrypted'} private key`); if (isEncrypted && (!password || password.trim() === '')) { logger.error(`Password is required for encrypted private key: ${filePath}`); ApiException.AuthException(`Password is required for encrypted private key: ${filePath}`); } try { var privateKey; if (isEncrypted) { logger.debug(`Decrypting private key using provided password`); // Decrypt the private key using the provided password privateKey = forge.pki.decryptRsaPrivateKey(pemData, password); } else { logger.debug(`Parsing unencrypted private key`); // Parse the unencrypted private key privateKey = forge.pki.privateKeyFromPem(pemData); } if (!privateKey) { logger.error(`Failed to parse private key from PEM file: ${filePath}`); ApiException.AuthException(`Failed to parse private key from PEM file: ${filePath}`); } logger.debug(`Successfully extracted private key from PEM file`); return forge.pki.privateKeyToPem(privateKey); } catch (error) { if (isEncrypted) { logger.error(`Error decrypting private key from ${filePath}: ${error.message}. This may be due to an incorrect password.`); ApiException.AuthException(`Error decrypting private key from ${filePath}: ${error.message}. ${Constants.INCORRECT_KEY_PASS}`); } else { logger.error(`Error parsing private key from ${filePath}: ${error.message}`); ApiException.AuthException(`Error parsing private key from ${filePath}: ${error.message}`); } } } catch (error) { logger.error(`Error loading private key from PEM file: ${filePath}: ${error.message}`); ApiException.AuthException(`Error loading private key from PEM file: ${filePath}: ${error.message}`); } }; exports.parseAndReturnPem = function(key, logger, password, passwordPropertyName) { logger.debug(`Parsing private key to PEM format synchronously, key type: ${typeof key}`); if (typeof key === 'string') { logger.debug('Processing string key as potential PEM private key'); // Check if the key is encrypted const isEncrypted = key.includes('ENCRYPTED'); if (isEncrypted) { logger.debug('Detected encrypted private key'); // Check if password is provided for encrypted key if (!password || password.trim() === '') { const propertyHint = passwordPropertyName ? ` Please set the '${passwordPropertyName}' property in your configuration.` : ''; const errorMessage = `Password is required for encrypted private key.${propertyHint}`; logger.error(errorMessage); throw new Error(errorMessage); } try { // Decrypt the private key using the provided password logger.debug('Attempting to decrypt private key with provided password'); const privateKey = forge.pki.decryptRsaPrivateKey(key, password); if (!privateKey) { logger.error('Failed to decrypt private key. Incorrect password or invalid key format.'); throw new Error('Failed to decrypt private key. Incorrect password or invalid key format.'); } // Convert the decrypted key back to PEM format const pemKey = forge.pki.privateKeyToPem(privateKey); logger.debug('Successfully decrypted and converted private key to PEM format'); return pemKey; } catch (error) { logger.error(`Error decrypting private key: ${error.message}`); throw new Error(`Error decrypting private key: ${error.message}`); } } else { // Not encrypted, proceed with normal validation try { // Validate it's a valid private key PEM forge.pki.privateKeyFromPem(key); logger.debug('Successfully validated private key PEM format'); return key; } catch (error) { logger.error(`Invalid private key PEM format: ${error.message}`); throw new Error('Invalid private key PEM format'); } } } else if (typeof key === 'object' && key !== null) { logger.debug('Processing object key as potential JWK private key'); try { // Check if it has the 'd' property which indicates a private key if (!key.d) { logger.error('JWK object is not a private key (missing d parameter)'); throw new Error('JWK object is not a private key'); } // Convert JWK to PEM (private key) logger.debug('Converting JWK to private key PEM'); const pem = jwkToPem(key, { private: true }); logger.debug('Successfully converted JWK to private key PEM format'); return pem; } catch (error) { logger.error(`Invalid JWK private key object: ${error.message}`); throw new Error('Invalid JWK private key object'); } } else { logger.error(`Unsupported key format: ${typeof key}`); throw new Error('Unsupported key format'); } } /** * Checks if a P12 file is generated by CyberSource * Validates that the P12 contains a certificate with CN="CyberSource_SJC_US" and only one private key * @param {string} filePath - Path to the P12 file * @param {string} password - Password for the P12 file * @param {object} logger - Logger object for logging messages * @returns {boolean} - True if the P12 file is generated by CyberSource, false otherwise */ exports.isCybersourceP12 = function(filePath, password, logger) { try { logger.debug(`Checking if P12 file is generated by CyberSource: ${filePath}`); // Use cached P12 object instead of parsing directly for better performance const Cache = require('./Cache'); const p12 = Cache.fetchCachedP12FromFile(filePath, password, logger); const certBags = p12.getBags({ bagType: forge.pki.oids.certBag }); const certs = certBags[forge.pki.oids.certBag]; // Early return if no certificates found if (!certs) { logger.debug('No certificates found in P12 file'); return false; } logger.debug(`Found ${certs.length} certificate(s) in P12 file`); // Check for CyberSource certificate using modern iteration const hasCybersourceCert = certs.some(({ cert }) => { if (!cert?.subject?.attributes) return false; const cnAttr = cert.subject.attributes.find( attr => attr.name === 'commonName' || attr.shortName === 'CN' ); if (cnAttr) { logger.debug(`Found certificate with CN: ${cnAttr.value}`); if (cnAttr.value === Constants.DEFAULT_MLE_ALIAS_FOR_CERT) { logger.debug(`Found CyberSource certificate (CN=${Constants.DEFAULT_MLE_ALIAS_FOR_CERT})`); return true; } } return false; }); if (!hasCybersourceCert) { logger.debug(`P12 file does not contain CyberSource certificate (CN=${Constants.DEFAULT_MLE_ALIAS_FOR_CERT})`); return false; } // Count private keys from both bag types // const bagTypes = [ // { oid: forge.pki.oids.keyBag, name: 'keyBag' }, // { oid: forge.pki.oids.pkcs8ShroudedKeyBag, name: 'pkcs8ShroudedKeyBag' } // ]; // let privateKeyCount = 0; // for (const { oid, name } of bagTypes) { // const bags = p12.getBags({ bagType: oid }); // const count = bags[oid]?.length || 0; // if (count > 0) { // privateKeyCount += count; // logger.debug(`Found ${count} ${name} private key(s)`); // } // } // logger.debug(`Total private keys found: ${privateKeyCount}`); // // Verify exactly one private key // if (privateKeyCount !== 1) { // logger.debug(`P12 file does not contain exactly one private key (found ${privateKeyCount})`); // return false; // } logger.debug('P12 file is generated by CyberSource: contains CyberSource certificate'); return true; } catch (error) { logger.error(`Error checking if P12 file is generated by CyberSource: ${error.message}`); return false; } };