From 1e0a388d77a2edf443e8480d40698cd58ee65f5b Mon Sep 17 00:00:00 2001 From: monkumar Date: Mon, 15 Sep 2025 15:24:49 +0530 Subject: [PATCH 01/30] added config options for message leveencryption for response --- src/authentication/core/MerchantConfig.js | 115 +++++++++++++++++++++- src/authentication/util/Utility.js | 53 ++++++++++ 2 files changed, 166 insertions(+), 2 deletions(-) diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index 77462c3d..7d2bec8b 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -8,6 +8,7 @@ var path = require('path'); var fs = require('fs'); var path = require('path'); var fs = require('fs'); +var Utility = require('../util/Utility'); /** * This function has all the merchentConfig properties getters and setters methods @@ -80,18 +81,81 @@ function MerchantConfig(result) { /* Default Custom Headers */ this.defaultHeaders = result.defaultHeaders; - /* MLE Feature */ + //MLE Params for Request Body + /** + * Deprecated flag to enable MLE for request. This flag is now known as "enableRequestMLEForOptionalApisGlobally" + */ this.useMLEGlobally = result.useMLEGlobally; + + /** + * Flag to enable MLE (Message Level Encryption) for request body to all APIs in SDK which have optional support for MLE. + * This means the API can send both non-encrypted and encrypted requests. + * Older flag "useMLEGlobally" is deprecated and will be used as alias/another name for enableRequestMLEForOptionalApisGlobally. + */ this.enableRequestMLEForOptionalApisGlobally = result.enableRequestMLEForOptionalApisGlobally !== undefined ? result.enableRequestMLEForOptionalApisGlobally : this.useMLEGlobally; + + /** + * Flag to disable MLE (Message Level Encryption) for request body to APIs in SDK which have mandatory MLE requirement when sending calls. + */ this.disableRequestMLEForMandatoryApisGlobally = result.disableRequestMLEForMandatoryApisGlobally !== undefined ? result.disableRequestMLEForMandatoryApisGlobally : false; + /** + * Assigns internal maps to control MLE for request and response per API function, + * based on the mapToControlMLEonAPI property. + */ + //both fields used for internal purpose only not exposed for merchants to set. Both sets from mapToControlMLEonAPI internally. + this.internalMapToControlRequestMLEonAPI = new Map(); + this.internalMapToControlResponseMLEonAPI = new Map(); + this.mapToControlMLEonAPI = result.mapToControlMLEonAPI; - this.mleKeyAlias = result.mleKeyAlias; //mleKeyAlias is optional parameter, default value is "CyberSource_SJC_US". + + /** + * Optional parameter. User can pass a custom requestMleKeyAlias to fetch from the certificate. + * Older flag "mleKeyAlias" is deprecated and will be used as alias/another name for requestMleKeyAlias. + */ + this.requestmleKeyAlias = Constants.DEFAULT_MLE_ALIAS_FOR_CERT; + + /** + * Parameter to pass the request MLE public certificate path. + */ this.mleForRequestPublicCertPath = result.mleForRequestPublicCertPath; + /** + * Flag to enable MLE (Message Level Encryption) for response body for all APIs in SDK to get MLE Response(encrypted response) if supported by API. + */ + this.enableResponseMleGlobally = result.enableResponseMleGlobally !== undefined ? result.enableResponseMleGlobally : false; + + /** + * Parameter to pass the KID value for the MLE response public certificate. This value will be provided in the merchant portal when retrieving the MLE response certificate. + */ + this.responseMleKID = result.responseMleKID; + + /** + * Path to the private key file used for Response MLE decryption by the SDK. + * Supported formats: .p12, .key, .pem, etc. + */ + this.responseMlePrivateKeyFilePath = result.responseMlePrivateKeyFilePath; + + /** + * Password for the private key file used in Response MLE decryption by the SDK. + * Required for .p12 files or encrypted private keys. + */ + this.responseMlePrivateKeyFilePassword = result.responseMlePrivateKeyFilePassword; + + /** + * PrivateKey instance used for Response MLE decryption by the SDK. + * Optional — either provide this object directly or specify the private key file path via configuration. + */ + this.responseMlePrivateKey = result.responseMlePrivateKey; + + + this.mapToControlMLEonAPI = result.mapToControlMLEonAPI; /* Fallback logic*/ this.defaultPropValues(); + if (this.mapToControlMLEonAPI != null) { + validateAndSetMapToControlMLEonAPI.call(this, this.mapToControlMLEonAPI); + } } MerchantConfig.prototype.getAuthenticationType = function getAuthenticationType() { @@ -743,4 +807,51 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { } } + +function validateAndSetMapToControlMLEonAPI(mapFromConfig) { + let tempMap; + var logger = Logger.getLogger(this, 'MerchantConfig'); + + // Validating only type of keys and values in the map. + if (mapFromConfig === null) { + ApiException.ApiException("Unsupported null value to mapToControlMLEonAPI in merchantConfig. Expected map which corresponds to <'apiFunctionName','flagForRequestMLE::flagForResponseMLE'> as dataType for field.", logger); + } + if (typeof (mapFromConfig) !== "map" && typeof (mapFromConfig) !== "object") { + ApiException.ApiException("Unsupported datatype for field mapToControlMLEonAPI. Expected Map which corresponds to <'apiFunctionName','flagForRequestMLE::flagForResponseMLE'> as dataType for field but got: " + typeof (mapFromConfig), logger); + } + if (typeof (mapFromConfig) === "object") { + for (const[_, value] of Object.entries(mapFromConfig)) { + if ((typeof (value) !== "string" && typeof (value) !== "boolean")) { + ApiException.ApiException("Unsupported datatype for field mapToControlMLEonAPI. Expected Map which corresponds to <'apiFunctionName','flagForRequestMLE::flagForResponseMLE'> as dataType for field but got: " + typeof (value), logger); + } + } + tempMap = new Map(Object.entries(mapFromConfig)); + } else { + mapFromConfig.forEach((value, key) => { + if (typeof (key) !== "string" || (typeof (value) !== "string" && typeof (value) !== "boolean")) { + ApiException.ApiException("Unsupported datatype for field mapToControlMLEonAPI. Expected Map which corresponds to <'apiFunctionName','flagForRequestMLE::flagForResponseMLE'> as dataType for field but got: " + typeof (value), logger); + } + }); + tempMap = mapFromConfig; + } + + + // Validating actual values in the map and setting internal maps for request and response MLE control. + this.internalMapToControlRequestMLEonAPI = new Map(); + this.internalMapToControlResponseMLEonAPI = new Map(); + + for (const[apiFunctionName, configString] of tempMap) { + var config = Utility.ParseMLEConfigString(configString, logger); + logger.debug(`For apiFunctionName: ${apiFunctionName}, parsed config is: `, config); + if (config.requestMLE !== undefined) { + this.internalMapToControlRequestMLEonAPI.set(apiFunctionName, config.requestMLE); + } + if (config.responseMLE !== undefined) { + this.internalMapToControlResponseMLEonAPI.set(apiFunctionName, config.responseMLE); + } + } +} + + + module.exports = MerchantConfig; diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index a813bfd3..0398fc35 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -115,3 +115,56 @@ exports.findCertificateByAlias = function (certs, keyAlias) { 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 === null || configString === undefined || configString.trim() === "") { + ApiException.ApiException("Unsupported empty or non-string configString. 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 for key '" + configString + "'. Expected format: true/false for 'requestMLE' but got: '" + configString + "'", logger); + } + } +} From d54855dd34b49468c8d6a500da9ba068b22843b1 Mon Sep 17 00:00:00 2001 From: monkumar Date: Wed, 17 Sep 2025 15:47:37 +0530 Subject: [PATCH 02/30] added utility for reading private key and changes for decrypting encrypted response --- src/authentication/core/Authorization.js | 4 +- src/authentication/core/MerchantConfig.js | 137 ++++++++++++++++++++-- src/authentication/jwt/JWTSigToken.js | 77 +++++++----- src/authentication/util/Cache.js | 39 ++++++ src/authentication/util/Constants.js | 1 + src/authentication/util/MLEUtility.js | 76 +++++++++++- src/authentication/util/Utility.js | 115 +++++++++++++++++- 7 files changed, 400 insertions(+), 49 deletions(-) diff --git a/src/authentication/core/Authorization.js b/src/authentication/core/Authorization.js index ccdec269..4bfa9967 100644 --- a/src/authentication/core/Authorization.js +++ b/src/authentication/core/Authorization.js @@ -10,7 +10,7 @@ var ApiException = require('../util/ApiException'); * This function calls for the generation of Signature message depending on the authentication type. * */ -exports.getToken = function(merchantConfig, logger){ +exports.getToken = function(merchantConfig, isResponseMLEForApi, logger){ var authenticationType = merchantConfig.getAuthenticationType().toLowerCase(); var httpSigToken; @@ -22,7 +22,7 @@ exports.getToken = function(merchantConfig, logger){ return httpSigToken; } else if(authenticationType === Constants.JWT) { - jwtSingToken = JWTSigToken.getToken(merchantConfig, logger); + jwtSingToken = JWTSigToken.getToken(merchantConfig, isResponseMLEForApi, logger); return jwtSingToken; } else if(authenticationType === Constants.OAUTH) { diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index 7d2bec8b..9f5e581e 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -151,11 +151,11 @@ function MerchantConfig(result) { this.mapToControlMLEonAPI = result.mapToControlMLEonAPI; /* Fallback logic*/ - this.defaultPropValues(); if (this.mapToControlMLEonAPI != null) { validateAndSetMapToControlMLEonAPI.call(this, this.mapToControlMLEonAPI); } + this.defaultPropValues(); } MerchantConfig.prototype.getAuthenticationType = function getAuthenticationType() { @@ -525,6 +525,54 @@ MerchantConfig.prototype.setMleForRequestPublicCertPath = function setMleForRequ this.mleForRequestPublicCertPath = mleForRequestPublicCertPath; } +MerchantConfig.prototype.getEnableResponseMleGlobally = function getEnableResponseMleGlobally() { + return this.enableResponseMleGlobally; +} + +MerchantConfig.prototype.setEnableResponseMleGlobally = function setEnableResponseMleGlobally(enableResponseMleGlobally) { + this.enableResponseMleGlobally = enableResponseMleGlobally; +} + +MerchantConfig.prototype.getResponseMleKID = function getResponseMleKID() { + return this.responseMleKID; +} + +MerchantConfig.prototype.setResponseMleKID = function setResponseMleKID(responseMleKID) { + this.responseMleKID = responseMleKID; +} + +MerchantConfig.prototype.getResponseMlePrivateKeyFilePath = function getResponseMlePrivateKeyFilePath() { + return this.responseMlePrivateKeyFilePath; +} + +MerchantConfig.prototype.setResponseMlePrivateKeyFilePath = function setResponseMlePrivateKeyFilePath(responseMlePrivateKeyFilePath) { + this.responseMlePrivateKeyFilePath = responseMlePrivateKeyFilePath; +} + +MerchantConfig.prototype.getResponseMlePrivateKeyFilePassword = function getResponseMlePrivateKeyFilePassword() { + return this.responseMlePrivateKeyFilePassword; +} + +MerchantConfig.prototype.setResponseMlePrivateKeyFilePassword = function setResponseMlePrivateKeyFilePassword(responseMlePrivateKeyFilePassword) { + this.responseMlePrivateKeyFilePassword = responseMlePrivateKeyFilePassword; +} + +MerchantConfig.prototype.getResponseMlePrivateKey = function getResponseMlePrivateKey() { + return this.responseMlePrivateKey; +} + +MerchantConfig.prototype.setResponseMlePrivateKey = function setResponseMlePrivateKey(responseMlePrivateKey) { + this.responseMlePrivateKey = responseMlePrivateKey; +} + +MerchantConfig.prototype.getInternalMapToControlResponseMLEonAPI = function getInternalMapToControlResponseMLEonAPI() { + return this.internalMapToControlResponseMLEonAPI; +} + +MerchantConfig.prototype.getInternalMapToControlRequestMLEonAPI = function getInternalMapToControlRequestMLEonAPI() { + return this.internalMapToControlRequestMLEonAPI; +} + MerchantConfig.prototype.getP12FilePath = function getP12FilePath() { return path.resolve(path.join(this.getKeysDirectory(), this.getKeyFileName() + '.p12')); } @@ -548,6 +596,8 @@ MerchantConfig.prototype.runEnvironmentCheck = function runEnvironmentCheck(logg } } + + //This method is for fallback MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { @@ -745,18 +795,18 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { ApiException.ApiException("mapToControlMLEonAPI in merchantConfig should be key value pair", logger); } - if (this.mapToControlMLEonAPI != null && Object.keys(this.mapToControlMLEonAPI).length !== 0) { - var hasTrueValue = false; - for (const[key, value] of Object.entries(this.mapToControlMLEonAPI)) { - if (value === true) { - hasTrueValue = true; - break; - } - } - if (hasTrueValue && this.authenticationType.toLowerCase() !== Constants.JWT) { - ApiException.ApiException("Request MLE is only supported in JWT auth type", logger); - } - } + // if (this.mapToControlMLEonAPI != null && Object.keys(this.mapToControlMLEonAPI).length !== 0) { + // var hasTrueValue = false; + // for (const[_, value] of Object.entries(this.mapToControlMLEonAPI)) { + // if (value === true) { + // hasTrueValue = true; + // break; + // } + // } + // if (hasTrueValue && this.authenticationType.toLowerCase() !== Constants.JWT) { + // ApiException.ApiException("Request MLE is only supported in JWT auth type", logger); + // } + // } } if (this.mleForRequestPublicCertPath) { // First check if the file exists and is readable @@ -780,6 +830,67 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { } } + + var responseMleConfigured = this.enableResponseMleGlobally; + if (this.internalMapToControlResponseMLEonAPI?.size > 0) { + responseMleConfigured = [...this.internalMapToControlResponseMLEonAPI.values()].includes(true); + } + + /** + * Validates Response Message Level Encryption (MLE) configuration + */ + if (responseMleConfigured) { + const logger = Logger.getLogger(this, 'MerchantConfig'); + + // Check authentication type + if (this.authenticationType?.toLowerCase() !== Constants.JWT) { + throw new ApiException.ApiException("Response MLE is only supported in JWT auth type", logger); + } + + // Check if either private key or valid file path is provided + const hasPrivateKey = !!this.responseMlePrivateKey; + const hasValidFilePath = this.responseMlePrivateKeyFilePath?.trim?.() !== ""; + + if (!hasPrivateKey && !hasValidFilePath) { + throw new ApiException.ApiException( + "Response MLE is enabled but no private key provided. Either set responseMlePrivateKey object or provide responseMlePrivateKeyFilePath.", + logger + ); + } + + // Ensure only one private key method is provided + if (hasPrivateKey && hasValidFilePath) { + throw new ApiException.ApiException( + "Both responseMlePrivateKey object and responseMlePrivateKeyFilePath are provided. Please provide only one of them for response mle private key.", + logger + ); + } + + // Validate file path accessibility if provided + if (hasValidFilePath) { + try { + fs.accessSync(this.responseMlePrivateKeyFilePath, fs.constants.R_OK); + const ext = path.extname(this.responseMlePrivateKeyFilePath).toLowerCase(); + if (!['.p12', '.pfx', '.pem', '.key', '.p8'].includes(ext)) { + throw new ApiException.ApiException( + `Unsupported Response MLE Private Key file format: ${ext}. Supported extensions are: .p12, .pfx, .pem, .key, .p8`, + logger + ); + } + } catch (err) { + const errorType = err.code === 'ENOENT' ? 'does not exist' : 'is not readable'; + throw new ApiException.ApiException( + `Invalid responseMlePrivateKeyFilePath ${errorType}: ${this.responseMlePrivateKeyFilePath} (${err.message})`, + logger + ); + } + } + + // Validate KID + if (typeof this.responseMleKID !== "string" || !this.responseMleKID?.trim()) { + throw new ApiException.ApiException("responseMleKID is required when response MLE is enabled.", logger); + } + } /** * This method is to log all merchantConfic properties * excluding HideMerchantConfigProperies defined in Constants diff --git a/src/authentication/jwt/JWTSigToken.js b/src/authentication/jwt/JWTSigToken.js index dad1ad2b..dd5e5bad 100644 --- a/src/authentication/jwt/JWTSigToken.js +++ b/src/authentication/jwt/JWTSigToken.js @@ -1,54 +1,75 @@ 'use strict'; -var Jwt = require('jwt-simple'); +const Jwt = require('jwt-simple'); const Constants = require('../util/Constants'); -var KeyCertificate = require('./KeyCertificateGenerator'); -var DigestGenerator = require('../payloadDigest/DigestGenerator'); -var ApiException = require('../util/ApiException'); +const KeyCertificate = require('./KeyCertificateGenerator'); +const DigestGenerator = require('../payloadDigest/DigestGenerator'); +const ApiException = require('../util/ApiException'); -/* JWTSigToken return jwtToken. -* jwtToken contains jwtBody encoded with JWT using RS256 algoritham. -* In POST method only we need to add digest in the jwtBody -*/ +// Constants for algorithms +const JWT_ALGORITHM = 'RS256'; +const DIGEST_ALGORITHM = 'SHA-256'; -exports.getToken = function (merchantConfig, logger) { +/** + * JWTSigToken module generates JWT tokens for API authentication. + * + * The JWT token contains a claim set encoded with JWT using RS256 algorithm. + * For POST, PUT, and PATCH methods, a digest of the request payload is added to the claim set. + * For MLE-enabled APIs, a response MLE key ID is added to the claim set. + * + * @module authentication/jwt/JWTSigToken + */ +/** + * Generates a JWT token for authentication + * + * @param {Object} merchantConfig - Configuration containing merchant details + * @param {boolean} isResponseMLEForApi - Flag indicating if MLE is enabled for this API + * @param {Object} logger - Logger instance + * @returns {string} The generated JWT token + * @throws {Error} If token generation fails + */ +exports.getToken = function (merchantConfig, isResponseMLEForApi, logger) { try { - var claimSet = ''; // date format is 'Mon, 09 Apr 2018 10:18:57 GMT' - var date = new Date(Date.now()).toUTCString(); - var rsaPrivateKey = KeyCertificate.getRSAPrivateKey(merchantConfig, logger); - var certificate = KeyCertificate.getX509CertificateInBase64(merchantConfig, logger, merchantConfig.getKeyAlias()); - var requestType = merchantConfig.getRequestType().toLowerCase(); + const date = new Date(Date.now()).toUTCString(); + const rsaPrivateKey = KeyCertificate.getRSAPrivateKey(merchantConfig, logger); + const certificate = KeyCertificate.getX509CertificateInBase64(merchantConfig, logger, merchantConfig.getKeyAlias()); + const requestType = merchantConfig.getRequestType().toLowerCase(); + // Create claim set as a regular JavaScript object + const claimSetJson = {}; + if (requestType === Constants.GET || requestType === Constants.DELETE) { - claimSet = "{\"iat\":\"" + date + "\"}"; + claimSetJson.iat = date; } else if (requestType === Constants.POST || requestType === Constants.PUT || requestType === Constants.PATCH) { - var digest = DigestGenerator.generateDigest(merchantConfig, logger); - claimSet = "{\"digest\":\"" - + digest + "\",\"digestAlgorithm\":\"SHA-256\",\"iat\":\"" - + date + "\"}"; + const digest = DigestGenerator.generateDigest(merchantConfig, logger); + claimSetJson.digest = digest; + claimSetJson.digestAlgorithm = DIGEST_ALGORITHM; + claimSetJson.iat = date; } - else { - ApiException.ApiException(Constants.INVALID_REQUEST_TYPE_METHOD, logger); + + // Add MLE key ID if MLE is enabled + if (isResponseMLEForApi === true) { + // Using bracket notation for property name with hyphens + claimSetJson["v-c-response-mle-kid"] = merchantConfig.getResponseMleKeyId(); } - var x5CList = [certificate]; - var customHeader = { + const customHeader = { 'header': { 'v-c-merchant-id': merchantConfig.getMerchantID(), - 'x5c': x5CList + 'x5c': [certificate] } }; - var claimSetObj = JSON.parse(claimSet); - //Generating JWToken - var jwtToken = Jwt.encode(claimSetObj, rsaPrivateKey, 'RS256', customHeader); + // Generating JWToken using the claimSetJson object directly + const jwtToken = Jwt.encode(claimSetJson, rsaPrivateKey, JWT_ALGORITHM, customHeader); return jwtToken; } catch (err) { - throw err; + logger.error(`JWT token generation failed: ${err.message}`); + throw new Error(`Failed to generate JWT token: ${err.message}`); } }; diff --git a/src/authentication/util/Cache.js b/src/authentication/util/Cache.js index f18c4c42..bca9c556 100644 --- a/src/authentication/util/Cache.js +++ b/src/authentication/util/Cache.js @@ -242,3 +242,42 @@ function validateCertificateExpiry(certificate, keyAlias, cacheKey, merchantConf } } }; + +exports.getMleResponsePrivateKeyFromFilePath = function(merchantConfig) { + const logger = Logger.getLogger(merchantConfig, 'Cache'); + const merchantId = merchantConfig.getMerchantID(); + const cacheKey = merchantId + Constants.MLE_CACHE_KEY_IDENTIFIER_FOR_RESPONSE_PRIVATE_KEY; + const certificatePath = merchantConfig.getResponseMlePrivateKeyFilePath(); + + const cachedEntry = cache.get(cacheKey); + + logger.debug("Fetching MLE response private key from cache with key: " + cacheKey); + if (cachedEntry == undefined || cachedEntry == null || cachedEntry.fileLastModifiedTime !== fs.statSync(certificatePath).mtimeMs) { + logger.debug("MLE response private key not found in cache or has been modified. Loading from file: " + certificatePath); + putMLEResponsePrivateKeyInCache(merchantConfig, cacheKey, certificatePath); + } + return cache.get(cacheKey).privateKey; +} + +function putMLEResponsePrivateKeyInCache(merchantConfig, cacheKey, privateKeyPath) { + const logger = Logger.getLogger(merchantConfig, 'Cache'); + const fileExtension = path.extname(privateKeyPath).toLowerCase(); + const keyPass = merchantConfig.getResponseMlePrivateKeyFilePassword(); + const fileLastModifiedTime = fs.statSync(privateKeyPath).mtimeMs; + var privateKey = null; + try { + if (['.p12', '.pfx'].includes(fileExtension)) { + privateKey = Utility.readPrivateKeyFromP12(privateKeyPath, keyPass, logger); + } else if (['.pem', '.key', '.p8'].includes(fileExtension)) { + privateKey = Utility.readPrivateKeyFromPemFile(privateKeyPath, keyPass, logger); + } + } catch (error) { + logger.error("Error reading private key from file: " + error.message); + throw error; + } + const cacheEntry = { + privateKey: privateKey, + fileLastModifiedTime: fileLastModifiedTime + }; + cache.put(cacheKey, cacheEntry); +} diff --git a/src/authentication/util/Constants.js b/src/authentication/util/Constants.js index 46957584..4db52141 100644 --- a/src/authentication/util/Constants.js +++ b/src/authentication/util/Constants.js @@ -97,6 +97,7 @@ module.exports = { DEFAULT_LOG_FILENAME : "cybs", DEFAULT_MAX_LOG_FILES : "10d", DEFAULT_LOGGING_LEVEL : "error", + MLE_CACHE_KEY_IDENTIFIER_FOR_RESPONSE_PRIVATE_KEY : "_mleResponsePrivateKeyFromFile", STATUS200 : "Transaction Successful", STATUS400 : "Bad Request", diff --git a/src/authentication/util/MLEUtility.js b/src/authentication/util/MLEUtility.js index 26a9bd16..e8d8405f 100644 --- a/src/authentication/util/MLEUtility.js +++ b/src/authentication/util/MLEUtility.js @@ -6,6 +6,7 @@ const Logger= require('../logging/Logger'); const ApiException= require('./ApiException'); const Constants = require('./Constants'); const Cache = require('./Cache'); +const JWEUtility = require('./JWEUtility'); exports.checkIsMLEForAPI = function (merchantConfig, inboundMLEStatus, operationId) { //isMLE for an api is false by default @@ -27,12 +28,12 @@ exports.checkIsMLEForAPI = function (merchantConfig, inboundMLEStatus, operation } //Control the MLE only from map - if (merchantConfig.mapToControlMLEonAPI != null && operationId in merchantConfig.mapToControlMLEonAPI) { - if (merchantConfig.mapToControlMLEonAPI[operationId] === true) { + if (merchantConfig.internalMapToControlRequestMLEonAPI != null && operationId in Object.keys(merchantConfig.internalMapToControlRequestMLEonAPI)) { + if (merchantConfig.internalMapToControlRequestMLEonAPI[operationId] === true) { isMLEForAPI = true; } - if (merchantConfig.mapToControlMLEonAPI[operationId] === false) { + if (merchantConfig.internalMapToControlRequestMLEonAPI[operationId] === false) { isMLEForAPI = false; } } @@ -40,6 +41,75 @@ exports.checkIsMLEForAPI = function (merchantConfig, inboundMLEStatus, operation return isMLEForAPI; } +/** + * Determines if Message Level Encryption (MLE) should be applied to the API response. + * @param {Object} merchantConfig - Merchant configuration object + * @param {array} operationIds - Array of operation IDs + * @returns {boolean} Whether MLE should be applied + */ +exports.checkIsResponseMLEForAPI = function (merchantConfig, operationIds) { + let isResponseMLEForAPI = merchantConfig.getEnableResponseMleGlobally(); + const responseMLEMap = merchantConfig.getInternalMapToControlResponseMLEonAPI(); + + if (responseMLEMap && operationIds) { + operationIds.forEach(opId => { + const trimmedId = opId.trim(); + if (trimmedId in responseMLEMap) { + isResponseMLEForAPI = responseMLEMap[trimmedId]; + } + }); + } + + return isResponseMLEForAPI; +} + +exports.checkAndDecryptEncryptedResponse = function (responseBody, merchantConfig) { + const logger = Logger.getLogger(merchantConfig, 'MLEUtility'); + logger.debug('Checking if response body requires decryption'); + + if ( + !responseBody || + typeof responseBody !== 'object' || + Object.keys(responseBody).length !== 1 || + !responseBody.encryptedResponse + ) { + logger.debug('Response body is not an encrypted response, returning as is'); + return Promise.resolve(responseBody); + } + + logger.debug('Response body contains encrypted data, attempting to decrypt'); + + try { + const privateKey = merchantConfig.getResponseMlePrivateKey() || + Cache.getMleResponsePrivateKeyFromFilePath(merchantConfig); + + if (!privateKey) { + const errorMsg = 'Failed to retrieve MLE response private key'; + logger.error(errorMsg); + return Promise.reject(new Error(errorMsg)); + } + + logger.debug('Successfully retrieved private key for decryption'); + + return JWEUtility.decryptJWEUsingPrivateKey(privateKey, responseBody.encryptedResponse) + .then(decryptedData => { + logger.debug('Successfully decrypted MLE response'); + return decryptedData; + }) + .catch(error => { + const errorMsg = `Error decrypting MLE response: ${error.message}`; + logger.error(errorMsg); + // Create a more descriptive error + return Promise.reject(new Error(errorMsg)); + }); + } catch (error) { + const errorMsg = `Error preparing for MLE response decryption: ${error.message}`; + logger.error(errorMsg); + // Create a more descriptive error + return Promise.reject(new Error(errorMsg)); + } +} + exports.encryptRequestPayload = function(merchantConfig, requestBody) { if (requestBody == null) { return Promise.resolve(requestBody); diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index 0398fc35..260d34dc 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -1,7 +1,9 @@ 'use strict' var ApiException = require('./ApiException'); -var Constants = require('./Constants') +var Constants = require('./Constants'); +var fs = require('fs'); +var forge = require('node-forge'); exports.getResponseCodeMessage = function (responseCode) { @@ -124,8 +126,8 @@ exports.findCertificateByAlias = function (certs, keyAlias) { * @throws Will throw an error if the configString format is invalid. */ exports.ParseMLEConfigString = function (configString, logger) { - if (configString === null || configString === undefined || configString.trim() === "") { - ApiException.ApiException("Unsupported empty or non-string configString. Expected format: 'requestMLE::responseMLE' or 'requestMLE' as true/false.", 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) { @@ -168,3 +170,110 @@ exports.ParseMLEConfigString = function (configString, logger) { } } } + +/** + * 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}`); + + if (!fs.existsSync(filePath)) { + logger.error(`File not found: ${filePath}`); + ApiException.AuthException(Constants.FILE_NOT_FOUND + filePath); + } + + // Read the P12 file and convert to ASN1 + 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 read P12 file and converted to ASN1`); + + // 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: ${error.message}`); + ApiException.AuthException(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`); + ApiException.AuthException("Password is required for encrypted private key"); + } + + 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`); + ApiException.AuthException("Failed to parse private key from PEM file"); + } + + logger.debug(`Successfully extracted private key from PEM file`); + + return forge.pki.privateKeyToPem(privateKey); + } catch (error) { + logger.error(`Error parsing private key: ${error.message}`); + ApiException.AuthException("Error parsing private key: " + error.message); + } + } catch (error) { + logger.error(`Error loading private key from PEM file: ${error.message}`); + ApiException.AuthException("Error loading private key from PEM file: " + error.message); + } +}; From e7a6b9dd6181d36390a28488d6b541d5393ecc62 Mon Sep 17 00:00:00 2001 From: monkumar Date: Wed, 17 Sep 2025 16:06:39 +0530 Subject: [PATCH 03/30] corrected request mle check --- src/authentication/core/MerchantConfig.js | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index 9f5e581e..8e55cf91 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -795,18 +795,18 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { ApiException.ApiException("mapToControlMLEonAPI in merchantConfig should be key value pair", logger); } - // if (this.mapToControlMLEonAPI != null && Object.keys(this.mapToControlMLEonAPI).length !== 0) { - // var hasTrueValue = false; - // for (const[_, value] of Object.entries(this.mapToControlMLEonAPI)) { - // if (value === true) { - // hasTrueValue = true; - // break; - // } - // } - // if (hasTrueValue && this.authenticationType.toLowerCase() !== Constants.JWT) { - // ApiException.ApiException("Request MLE is only supported in JWT auth type", logger); - // } - // } + if (this.getInternalMapToControlRequestMLEonAPI() != null && Object.keys(this.this.getInternalMapToControlRequestMLEonAPI()).length !== 0) { + var hasTrueValue = false; + for (const[_, value] of Object.entries(this.this.getInternalMapToControlRequestMLEonAPI())) { + if (value === true) { + hasTrueValue = true; + break; + } + } + if (hasTrueValue && this.authenticationType.toLowerCase() !== Constants.JWT) { + ApiException.ApiException("Request MLE is only supported in JWT auth type", logger); + } + } } if (this.mleForRequestPublicCertPath) { // First check if the file exists and is readable From 7b46d5cb53a81767b0b50bd71db2716b35d1dbfc Mon Sep 17 00:00:00 2001 From: monkumar Date: Thu, 18 Sep 2025 12:05:54 +0530 Subject: [PATCH 04/30] corrected check and validation of request mle --- src/authentication/core/MerchantConfig.js | 8 ++++---- src/authentication/util/MLEUtility.js | 11 +++-------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index 8e55cf91..6f7cd7a0 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -786,18 +786,18 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { } //useMLEGlobally check for auth Type - if (this.enableRequestMLEForOptionalApisGlobally === true || this.mapToControlMLEonAPI != null) { + if (this.enableRequestMLEForOptionalApisGlobally === true || this.internalMapToControlRequestMLEonAPI != null) { if (this.enableRequestMLEForOptionalApisGlobally === true && this.authenticationType.toLowerCase() !== Constants.JWT) { ApiException.ApiException("Request MLE is only supported in JWT auth type", logger); } - if (this.mapToControlMLEonAPI != null && typeof (this.mapToControlMLEonAPI) !== "object") { + if (this.internalMapToControlRequestMLEonAPI != null && typeof (this.internalMapToControlRequestMLEonAPI) !== "object") { ApiException.ApiException("mapToControlMLEonAPI in merchantConfig should be key value pair", logger); } - if (this.getInternalMapToControlRequestMLEonAPI() != null && Object.keys(this.this.getInternalMapToControlRequestMLEonAPI()).length !== 0) { + if (this.internalMapToControlRequestMLEonAPI != null && Object.keys(this.internalMapToControlRequestMLEonAPI).length !== 0) { var hasTrueValue = false; - for (const[_, value] of Object.entries(this.this.getInternalMapToControlRequestMLEonAPI())) { + for (const[_, value] of Object.entries(this.internalMapToControlRequestMLEonAPI)) { if (value === true) { hasTrueValue = true; break; diff --git a/src/authentication/util/MLEUtility.js b/src/authentication/util/MLEUtility.js index e8d8405f..a31479bd 100644 --- a/src/authentication/util/MLEUtility.js +++ b/src/authentication/util/MLEUtility.js @@ -28,14 +28,9 @@ exports.checkIsMLEForAPI = function (merchantConfig, inboundMLEStatus, operation } //Control the MLE only from map - if (merchantConfig.internalMapToControlRequestMLEonAPI != null && operationId in Object.keys(merchantConfig.internalMapToControlRequestMLEonAPI)) { - if (merchantConfig.internalMapToControlRequestMLEonAPI[operationId] === true) { - isMLEForAPI = true; - } - - if (merchantConfig.internalMapToControlRequestMLEonAPI[operationId] === false) { - isMLEForAPI = false; - } + const mleControlMap = merchantConfig.getInternalMapToControlRequestMLEonAPI(); + if (mleControlMap && mleControlMap.has(operationId)) { + isMLEForAPI = mleControlMap.get(operationId); } return isMLEForAPI; From 90e575408f2bb3df46f6993f695263182fcfcd19 Mon Sep 17 00:00:00 2001 From: monkumar Date: Thu, 18 Sep 2025 14:09:25 +0530 Subject: [PATCH 05/30] minor fixs --- src/authentication/jwt/JWTSigToken.js | 2 +- src/authentication/util/MLEUtility.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/authentication/jwt/JWTSigToken.js b/src/authentication/jwt/JWTSigToken.js index dd5e5bad..15aaa388 100644 --- a/src/authentication/jwt/JWTSigToken.js +++ b/src/authentication/jwt/JWTSigToken.js @@ -53,7 +53,7 @@ exports.getToken = function (merchantConfig, isResponseMLEForApi, logger) { // Add MLE key ID if MLE is enabled if (isResponseMLEForApi === true) { // Using bracket notation for property name with hyphens - claimSetJson["v-c-response-mle-kid"] = merchantConfig.getResponseMleKeyId(); + claimSetJson["v-c-response-mle-kid"] = merchantConfig.getResponseMleKID(); } const customHeader = { diff --git a/src/authentication/util/MLEUtility.js b/src/authentication/util/MLEUtility.js index a31479bd..be3aaeb1 100644 --- a/src/authentication/util/MLEUtility.js +++ b/src/authentication/util/MLEUtility.js @@ -49,8 +49,8 @@ exports.checkIsResponseMLEForAPI = function (merchantConfig, operationIds) { if (responseMLEMap && operationIds) { operationIds.forEach(opId => { const trimmedId = opId.trim(); - if (trimmedId in responseMLEMap) { - isResponseMLEForAPI = responseMLEMap[trimmedId]; + if (responseMLEMap.has(trimmedId)) { + isResponseMLEForAPI = responseMLEMap.get(trimmedId); } }); } @@ -73,6 +73,7 @@ exports.checkAndDecryptEncryptedResponse = function (responseBody, merchantConfi } logger.debug('Response body contains encrypted data, attempting to decrypt'); + logger.debug('LOG_NETWORK_RESPONSE_BEFORE_MLE_DECRYPTION: ' + JSON.stringify(responseBody)); try { const privateKey = merchantConfig.getResponseMlePrivateKey() || @@ -88,7 +89,7 @@ exports.checkAndDecryptEncryptedResponse = function (responseBody, merchantConfi return JWEUtility.decryptJWEUsingPrivateKey(privateKey, responseBody.encryptedResponse) .then(decryptedData => { - logger.debug('Successfully decrypted MLE response'); + logger.debug('LOG_NETWORK_RESPONSE_AFTER_MLE_DECRYPTION: ' + JSON.stringify(decryptedData)); return decryptedData; }) .catch(error => { From a6106ec2a602c51f9eebd6180048ec11daa4fe4d Mon Sep 17 00:00:00 2001 From: monkumar Date: Thu, 18 Sep 2025 16:59:25 +0530 Subject: [PATCH 06/30] minor fix --- src/authentication/core/MerchantConfig.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index 6f7cd7a0..549602fd 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -831,15 +831,15 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { } - var responseMleConfigured = this.enableResponseMleGlobally; - if (this.internalMapToControlResponseMLEonAPI?.size > 0) { - responseMleConfigured = [...this.internalMapToControlResponseMLEonAPI.values()].includes(true); - } + const isResponseMleConfigured = this.enableResponseMleGlobally || + (this.internalMapToControlResponseMLEonAPI?.size > 0 && + Array.from(this.internalMapToControlResponseMLEonAPI.values()).some(value => value === true)); + /** * Validates Response Message Level Encryption (MLE) configuration */ - if (responseMleConfigured) { + if (isResponseMleConfigured) { const logger = Logger.getLogger(this, 'MerchantConfig'); // Check authentication type From 4fae8dec118e273a6e814ef4fb1ebb46c7ac1ac9 Mon Sep 17 00:00:00 2001 From: monkumar Date: Fri, 19 Sep 2025 16:56:09 +0530 Subject: [PATCH 07/30] corrected file path check and renamed mleKeyAlias to requestMleKeyAlias --- MLE.md | 12 ++++++------ src/authentication/core/MerchantConfig.js | 17 +++++++++-------- src/authentication/util/Cache.js | 8 ++++---- src/authentication/util/MLEUtility.js | 6 +++--- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/MLE.md b/MLE.md index 27616f4d..97ce9518 100644 --- a/MLE.md +++ b/MLE.md @@ -25,9 +25,9 @@ Optionally, you can control the MLE feature at the API level using the `mapToCon ### MLE Key Alias -Another optional parameter for MLE is `mleKeyAlias`, which specifies the key alias used to retrieve the MLE certificate from the JWT P12 file. +Another optional parameter for MLE is `requestmleKeyAlias` (formerly known as `mleKeyAlias`), which specifies the key alias used to retrieve the MLE certificate from the JWT P12 file. -- **Variable**: `mleKeyAlias` +- **Variable**: `requestmleKeyAlias` - **Type**: `string` - **Default**: `CyberSource_SJC_US` - **Description**: By default, CyberSource uses the `CyberSource_SJC_US` public certificate to encrypt the payload. However, users can override this default value by setting their own key alias. @@ -35,7 +35,7 @@ Another optional parameter for MLE is `mleKeyAlias`, which specifies the key ali ## Notes - If `useMLEGlobally` is set to true, it will enable MLE for all API calls that support MLE by CyberSource, unless overridden by mapToControlMLEonAPI. - If `mapToControlMLEonAPI` is not provided or does not contain a specific API function name, the global useMLEGlobally setting will be applied. -- The `mleKeyAlias` parameter is optional and defaults to CyberSource_SJC_US if not specified by the user. Users can override this default value by setting their own key alias. +- The `requestmleKeyAlias` parameter is optional and defaults to CyberSource_SJC_US if not specified by the user. Users can override this default value by setting their own key alias. ## Example Configuration @@ -56,7 +56,7 @@ Or "apiFunctionName1": false, //if want to disable the particular api from list of MLE supported APIs "apiFunctionName2": true //if want to enable MLE on API which is not in the list of supported MLE APIs for used version of Rest SDK }, - "mleKeyAlias": "Custom_Key_Alias" //optional if any custom value provided by Cybs + "requestmleKeyAlias": "Custom_Key_Alias" //optional if any custom value provided by Cybs } } ``` @@ -70,7 +70,7 @@ Or "apiFunctionName1": true, //if want to enable MLE for API1 "apiFunctionName2": true //if want to enable MLE for API2 }, - "mleKeyAlias": "Custom_Key_Alias" //optional if any custom value provided by Cybs + "requestmleKeyAlias": "Custom_Key_Alias" //optional if any custom value provided by Cybs } } ``` @@ -79,7 +79,7 @@ In the above examples: - MLE is enabled/disabled globally (`useMLEGlobally` is true/false). - `apiFunctionName1` will have MLE disabled/enabled based on value provided. - `apiFunctionName2` will have MLE enabled. -- `mleKeyAlias` is set to `Custom_Key_Alias`, overriding the default value. +- `requestmleKeyAlias` is set to `Custom_Key_Alias`, overriding the default value. Please refer given link for sample codes with MLE: https://github.com/CyberSource/cybersource-rest-samples-node/tree/master/Samples/MLEFeature diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index 549602fd..f9caea8b 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -113,7 +113,8 @@ function MerchantConfig(result) { * Optional parameter. User can pass a custom requestMleKeyAlias to fetch from the certificate. * Older flag "mleKeyAlias" is deprecated and will be used as alias/another name for requestMleKeyAlias. */ - this.requestmleKeyAlias = Constants.DEFAULT_MLE_ALIAS_FOR_CERT; + this.requestmleKeyAlias = result.requestmleKeyAlias !== undefined && typeof result.requestmleKeyAlias == "string" ? result.requestmleKeyAlias : + (result.mleKeyAlias !== undefined && typeof result.mleKeyAlias == "string" ? result.mleKeyAlias : Constants.DEFAULT_MLE_ALIAS_FOR_CERT); /** * Parameter to pass the request MLE public certificate path. @@ -509,12 +510,12 @@ MerchantConfig.prototype.setMapToControlMLEonAPI = function setMapToControlMLEon this.mapToControlMLEonAPI = mapToControlMLEonAPI; } -MerchantConfig.prototype.getMleKeyAlias = function getMleKeyAlias() { - return this.mleKeyAlias; +MerchantConfig.prototype.getRequestmleKeyAlias = function getRequestmleKeyAlias() { + return this.requestmleKeyAlias; } -MerchantConfig.prototype.setMleKeyAlias = function setMleKeyAlias(mleKeyAlias) { - this.mleKeyAlias = mleKeyAlias; +MerchantConfig.prototype.setRequestmleKeyAlias = function setRequestmleKeyAlias(requestmleKeyAlias) { + this.requestmleKeyAlias = requestmleKeyAlias; } MerchantConfig.prototype.getMleForRequestPublicCertPath = function getMleForRequestPublicCertPath() { @@ -773,8 +774,8 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { } //set the MLE key alias either from merchant config or default value - if (!this.mleKeyAlias || !this.mleKeyAlias.trim()) { - this.mleKeyAlias = Constants.DEFAULT_MLE_ALIAS_FOR_CERT; + if (!this.requestmleKeyAlias || !this.requestmleKeyAlias.trim()) { + this.requestmleKeyAlias = Constants.DEFAULT_MLE_ALIAS_FOR_CERT; } if ( @@ -849,7 +850,7 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { // Check if either private key or valid file path is provided const hasPrivateKey = !!this.responseMlePrivateKey; - const hasValidFilePath = this.responseMlePrivateKeyFilePath?.trim?.() !== ""; + const hasValidFilePath = typeof this.responseMlePrivateKeyFilePath === "string" && this.responseMlePrivateKeyFilePath.trim() !== ""; if (!hasPrivateKey && !hasValidFilePath) { throw new ApiException.ApiException( diff --git a/src/authentication/util/Cache.js b/src/authentication/util/Cache.js index bca9c556..cefe77be 100644 --- a/src/authentication/util/Cache.js +++ b/src/authentication/util/Cache.js @@ -130,7 +130,7 @@ function setupMLECache(merchantConfig, cacheKey, certificateSourcePath) { mleCert: mleCert, fileLastModifiedTime: fileLastModifiedTime }); - validateCertificateExpiry(mleCert, merchantConfig.getMleKeyAlias(), cacheKey, merchantConfig); + validateCertificateExpiry(mleCert, merchantConfig.getRequestmleKeyAlias(), cacheKey, merchantConfig); } @@ -153,7 +153,7 @@ function loadCertificateFromP12(merchantConfig, certificatePath) { } // Try to find the certificate by alias among all certificates - var mleCert = Utility.findCertificateByAlias(certs, merchantConfig.getMleKeyAlias()); + var mleCert = Utility.findCertificateByAlias(certs, merchantConfig.getRequestmleKeyAlias()); return forge.pki.certificateFromPem(mleCert); } else { throw new Error("No certificate found in P12 file"); @@ -173,10 +173,10 @@ function loadCertificateFromPem(merchantConfig, mleCertPath) { throw new Error("No valid PEM certificates found in the provided path : " + mleCertPath); } try { - mleCert = Utility.findCertificateByAlias(certs, merchantConfig.getMleKeyAlias()); + mleCert = Utility.findCertificateByAlias(certs, merchantConfig.getRequestmleKeyAlias()); } catch (error) { - logger.warn("No certificate found for the specified mleKeyAlias '" + merchantConfig.getMleKeyAlias() + "'. Using the first certificate from file " + mleCertPath + " as the MLE request certificate."); + logger.warn("No certificate found for the specified requestmleKeyAlias '" + merchantConfig.getRequestmleKeyAlias() + "'. Using the first certificate from file " + mleCertPath + " as the MLE request certificate."); mleCert = certs[0]; } // Use node forge to parse the PEM certificate diff --git a/src/authentication/util/MLEUtility.js b/src/authentication/util/MLEUtility.js index be3aaeb1..963cf8cb 100644 --- a/src/authentication/util/MLEUtility.js +++ b/src/authentication/util/MLEUtility.js @@ -120,9 +120,9 @@ exports.encryptRequestPayload = function(merchantConfig, requestBody) { logger.debug("Currently, MLE for requests using HTTP Signature as authentication is not supported by Cybersource. By default, the SDK will fall back to non-encrypted requests."); return Promise.resolve(requestBody); } - // let isCertExpired = KeyCertificate.verifyIsCertificateExpired(cert, merchantConfig.getMleKeyAlias(), logger); + // let isCertExpired = KeyCertificate.verifyIsCertificateExpired(cert, merchantConfig.getRequestmleKeyAlias(), logger); // if (isCertExpired === true) { - // ApiException.ApiException("Certificate for MLE with alias " + merchantConfig.getMleKeyAlias() + " is expired in " + merchantConfig.getKeyFileName() + ".p12", logger); + // ApiException.ApiException("Certificate for MLE with alias " + merchantConfig.getRequestmleKeyAlias() + " is expired in " + merchantConfig.getKeyFileName() + ".p12", logger); // } const customHeaders = { @@ -176,7 +176,7 @@ function getSerialNumberFromCert(cert, merchantConfig, logger) { if (serialNumberAttr) { return serialNumberAttr.value; } else { - logger.warn("Serial number not found in MLE certificate for alias " + merchantConfig.getMleKeyAlias() + " in " + merchantConfig.getKeyFileName() + ".p12"); + logger.warn("Serial number not found in MLE certificate for alias " + merchantConfig.getRequestmleKeyAlias() + " in " + merchantConfig.getKeyFileName() + ".p12"); return cert.serialNumber; } } From e3fde6c13a7887c2a42471be23008f81517b2ffa Mon Sep 17 00:00:00 2001 From: monkumar Date: Fri, 19 Sep 2025 21:17:42 +0530 Subject: [PATCH 08/30] decrypting response in case of encrypted response --- src/ApiClient.js | 56 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/src/ApiClient.js b/src/ApiClient.js index b874c7a3..252f6446 100644 --- a/src/ApiClient.js +++ b/src/ApiClient.js @@ -18,18 +18,18 @@ (function(root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. - define(['axios', 'axios-cookiejar-support', 'https-proxy-agent', 'https', 'querystring', 'Authentication/MerchantConfig', 'Authentication/Logger', 'Authentication/Constants', 'Authentication/Authorization', 'Authentication/PayloadDigest'], factory); + define(['axios', 'axios-cookiejar-support', 'https-proxy-agent', 'https', 'querystring', 'Authentication/MerchantConfig', 'Authentication/Logger', 'Authentication/Constants', 'Authentication/Authorization', 'Authentication/PayloadDigest', 'Authentication/MLEUtility'], factory); } else if (typeof module === 'object' && module.exports) { // CommonJS-like environments that support module.exports, like Node. - module.exports = factory(require('axios'), require('axios-cookiejar-support'), require('https-proxy-agent'), require('https'), require('querystring'), require('./authentication/core/MerchantConfig'), require('./authentication/logging/Logger'), require('./authentication/util/Constants'), require('./authentication/core/Authorization'), require('./authentication/payloadDigest/DigestGenerator')); + module.exports = factory(require('axios'), require('axios-cookiejar-support'), require('https-proxy-agent'), require('https'), require('querystring'), require('./authentication/core/MerchantConfig'), require('./authentication/logging/Logger'), require('./authentication/util/Constants'), require('./authentication/core/Authorization'), require('./authentication/payloadDigest/DigestGenerator'), require('./authentication/util/MLEUtility')); } else { // Browser globals (root is window) if (!root.CyberSource) { root.CyberSource = {}; } - root.CyberSource.ApiClient = factory(root.axios, root.axiosCookieJar, root.httpsProxyAgent, root.https, root.querystring, root.Authentication.MerchantConfig, root.Authentication.Logger, root.Authentication.Constants, root.Authentication.Authorization, root.Authentication.PayloadDigest); + root.CyberSource.ApiClient = factory(root.axios, root.axiosCookieJar, root.httpsProxyAgent, root.https, root.querystring, root.Authentication.MerchantConfig, root.Authentication.Logger, root.Authentication.Constants, root.Authentication.Authorization, root.Authentication.PayloadDigest, root.Authentication.MLEUtility); } -}(this, function(axios, axiosCookieJar, { HttpsProxyAgent }, https, querystring, MerchantConfig, Logger, Constants, Authorization, PayloadDigest) { +}(this, function(axios, axiosCookieJar, { HttpsProxyAgent }, https, querystring, MerchantConfig, Logger, Constants, Authorization, PayloadDigest, MLEUtility) { /** * @module ApiClient * @version 0.0.1 @@ -472,8 +472,9 @@ * @param {String} httpMethod * @param {String} requestTarget * @param {String} requestBody + * @param {Boolean} isResponseMLEForApi */ - exports.prototype.callAuthenticationHeader = function (httpMethod, requestTarget, requestBody, headerParams) { + exports.prototype.callAuthenticationHeader = function (httpMethod, requestTarget, requestBody, headerParams, isResponseMLEForApi) { this.merchantConfig.setRequestTarget(requestTarget); this.merchantConfig.setRequestType(httpMethod) @@ -482,7 +483,7 @@ this.logger.info('Authentication Type : ' + this.merchantConfig.getAuthenticationType()); this.logger.info(this.constants.REQUEST_TYPE + ' : ' + httpMethod.toUpperCase()); - var token = Authorization.getToken(this.merchantConfig, this.logger); + var token = Authorization.getToken(this.merchantConfig, isResponseMLEForApi, this.logger); var clientId = getClientId(); @@ -557,13 +558,14 @@ * @param {Array.} contentTypes An array of request MIME types. * @param {Array.} accepts An array of acceptable response MIME types. * @param {(String|Array|ObjectFunction)} returnType The required type to return; can be a string for simple types or the + * @param {Boolean} isResponseMLEForApi - Flag indicating if MLE is enabled for this API * constructor for a complex type. * @param {module:ApiClient~callApiCallback} callback The callback function. * @returns {Object} The SuperAgent request object. */ exports.prototype.callApi = function callApi(path, httpMethod, pathParams, queryParams, headerParams, formParams, bodyParam, authNames, contentTypes, accepts, - returnType, callback) { + returnType, isResponseMLEForApi, callback) { var _this = this; var url = this.buildUrl(path, pathParams); @@ -654,7 +656,7 @@ if (this.merchantConfig.getAuthenticationType().toLowerCase() !== this.constants.MUTUAL_AUTH) { - headerParams = this.callAuthenticationHeader(httpMethod, requestTarget, bodyParam, headerParams); + headerParams = this.callAuthenticationHeader(httpMethod, requestTarget, bodyParam, headerParams, isResponseMLEForApi); } if(this.merchantConfig.getDefaultHeaders()) { @@ -755,12 +757,38 @@ axios.request(axiosConfig).then(function(response) { - if (callback) { - var data = _this.deserialize(response, returnType); - response = _this.translateResponse(response); - - callback(null, data, response); - } + // Properly wait for the decryption to complete before proceeding + return MLEUtility.checkAndDecryptEncryptedResponse(response.data, _this.merchantConfig) + .then(function(decryptedData) { + response.data = decryptedData; + + if (callback) { + var data = _this.deserialize(response, returnType); + _this.logger.debug(`Response data: ${JSON.stringify(data)}`); + + response = _this.translateResponse(response); + + callback(null, data, response); + } + + // Return data for Promise-based usage + return { + data: data, + response: response + }; + }) + .catch(function(error) { + + // Create a simple error object with descriptive message + const errorMsg = `Failed to decrypt response: ${error.message}`; + + if (callback) { + callback(new Error(errorMsg), null, null); + } + + // Reject the promise for Promise-based usage + return Promise.reject(new Error(errorMsg)); + }); }).catch(function(error, response) { source.cancel('Stream ended.'); var userError = {}; From 45eb1a05cec3daa5015c4e241dab59f05aef7e99 Mon Sep 17 00:00:00 2001 From: monkumar Date: Mon, 22 Sep 2025 16:19:08 +0530 Subject: [PATCH 09/30] corrected log message --- src/authentication/util/Utility.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index 260d34dc..966ee97f 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -166,7 +166,7 @@ exports.ParseMLEConfigString = function (configString, logger) { }; return result; } else { - ApiException.ApiException("Invalid MLE control map value format for key '" + configString + "'. Expected format: true/false for 'requestMLE' but got: '" + configString + "'", logger); + ApiException.ApiException("Invalid MLE control map value format: '" + configString + "'. Expected format: true/false for 'requestMLE' but got: '" + configString + "'", logger); } } } From 00c3e6c649e378e14c965b1ec38e0dceb5ab72f7 Mon Sep 17 00:00:00 2001 From: monkumar Date: Mon, 22 Sep 2025 17:35:45 +0530 Subject: [PATCH 10/30] mustache field changes --- .../ApiClient.mustache | 73 +++++++++++++------ .../api.mustache | 5 +- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/generator/cybersource-javascript-template/ApiClient.mustache b/generator/cybersource-javascript-template/ApiClient.mustache index f74fc0d2..75e184b9 100644 --- a/generator/cybersource-javascript-template/ApiClient.mustache +++ b/generator/cybersource-javascript-template/ApiClient.mustache @@ -4,18 +4,18 @@ (function(root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. - define(['axios', 'axios-cookiejar-support', 'https-proxy-agent', 'https', 'querystring', 'Authentication/MerchantConfig', 'Authentication/Logger', 'Authentication/Constants', 'Authentication/Authorization', 'Authentication/PayloadDigest'], factory); + define(['axios', 'axios-cookiejar-support', 'https-proxy-agent', 'https', 'querystring', 'Authentication/MerchantConfig', 'Authentication/Logger', 'Authentication/Constants', 'Authentication/Authorization', 'Authentication/PayloadDigest', 'Authentication/MLEUtility'], factory); } else if (typeof module === 'object' && module.exports) { // CommonJS-like environments that support module.exports, like Node. - module.exports = factory(require('axios'), require('axios-cookiejar-support'), require('https-proxy-agent'), require('https'), require('querystring'), require('./authentication/core/MerchantConfig'), require('./authentication/logging/Logger'), require('./authentication/util/Constants'), require('./authentication/core/Authorization'), require('./authentication/payloadDigest/DigestGenerator')); + module.exports = factory(require('axios'), require('axios-cookiejar-support'), require('https-proxy-agent'), require('https'), require('querystring'), require('./authentication/core/MerchantConfig'), require('./authentication/logging/Logger'), require('./authentication/util/Constants'), require('./authentication/core/Authorization'), require('./authentication/payloadDigest/DigestGenerator'), require('./authentication/util/MLEUtility')); } else { // Browser globals (root is window) if (!root.{{moduleName}}) { root.{{moduleName}} = {}; } - root.{{moduleName}}.ApiClient = factory(root.axios, root.axiosCookieJar, root.httpsProxyAgent, root.https, root.querystring, root.Authentication.MerchantConfig, root.Authentication.Logger, root.Authentication.Constants, root.Authentication.Authorization, root.Authentication.PayloadDigest); + root.{{moduleName}}.ApiClient = factory(root.axios, root.axiosCookieJar, root.httpsProxyAgent, root.https, root.querystring, root.Authentication.MerchantConfig, root.Authentication.Logger, root.Authentication.Constants, root.Authentication.Authorization, root.Authentication.PayloadDigest, root.Authentication.MLEUtility); } -}(this, function(axios, axiosCookieJar, { HttpsProxyAgent }, https, querystring, MerchantConfig, Logger, Constants, Authorization, PayloadDigest) { +}(this, function(axios, axiosCookieJar, { HttpsProxyAgent }, https, querystring, MerchantConfig, Logger, Constants, Authorization, PayloadDigest, MLEUtility) { {{#emitJSDoc}} /** * @module {{#invokerPackage}}{{invokerPackage}}/{{/invokerPackage}}ApiClient * @version {{projectVersion}} @@ -470,8 +470,9 @@ * @param {String} httpMethod * @param {String} requestTarget * @param {String} requestBody + * @param {Boolean} isResponseMLEForApi */ - exports.prototype.callAuthenticationHeader = function (httpMethod, requestTarget, requestBody, headerParams) { + exports.prototype.callAuthenticationHeader = function (httpMethod, requestTarget, requestBody, headerParams, isResponseMLEForApi) { this.merchantConfig.setRequestTarget(requestTarget); this.merchantConfig.setRequestType(httpMethod) @@ -480,7 +481,7 @@ this.logger.info('Authentication Type : ' + this.merchantConfig.getAuthenticationType()); this.logger.info(this.constants.REQUEST_TYPE + ' : ' + httpMethod.toUpperCase()); - var token = Authorization.getToken(this.merchantConfig, this.logger); + var token = Authorization.getToken(this.merchantConfig, isResponseMLEForApi, this.logger); var clientId = getClientId(); @@ -555,13 +556,14 @@ * @param {Array.} contentTypes An array of request MIME types. * @param {Array.} accepts An array of acceptable response MIME types. * @param {(String|Array|ObjectFunction)} returnType The required type to return; can be a string for simple types or the + * @param {Boolean} isResponseMLEForApi - Flag indicating if MLE is enabled for this API * constructor for a complex type.{{^usePromises}} * @param {module:{{#invokerPackage}}{{invokerPackage}}/{{/invokerPackage}}ApiClient~callApiCallback} callback The callback function.{{/usePromises}} * @returns {{#usePromises}}{Promise} A {@link https://www.promisejs.org/|Promise} object{{/usePromises}}{{^usePromises}}{Object} The SuperAgent request object{{/usePromises}}. */ {{/emitJSDoc}} exports.prototype.callApi = function callApi(path, httpMethod, pathParams, queryParams, headerParams, formParams, bodyParam, authNames, contentTypes, accepts, - returnType{{^usePromises}}, callback{{/usePromises}}) { + returnType, isResponseMLEForApi{{^usePromises}}, callback{{/usePromises}}) { var _this = this; var url = this.buildUrl(path, pathParams); @@ -652,7 +654,7 @@ if (this.merchantConfig.getAuthenticationType().toLowerCase() !== this.constants.MUTUAL_AUTH) { - headerParams = this.callAuthenticationHeader(httpMethod, requestTarget, bodyParam, headerParams); + headerParams = this.callAuthenticationHeader(httpMethod, requestTarget, bodyParam, headerParams, isResponseMLEForApi); } if(this.merchantConfig.getDefaultHeaders()) { @@ -752,14 +754,27 @@ axiosConfig.url = requestTarget; {{#usePromises}} return axios.request(axiosConfig).then(function(response) { - try { - var data = _this.deserialize(response, returnType); - response = _this.translateResponse(response); - - resolve({data: data, response: response}); - } catch(err) { - reject(err); - } + // Properly wait for the decryption to complete before proceeding + return MLEUtility.checkAndDecryptEncryptedResponse(response.data, _this.merchantConfig) + .then(function(decryptedData) { + response.data = decryptedData; + + try { + var data = _this.deserialize(response, returnType); + response = _this.translateResponse(response); + + resolve({data: data, response: response}); + } catch(err) { + reject(err); + } + }) + .catch(function(error) { + // Create a simple error object with descriptive message + const errorMsg = `Failed to decrypt response: ${error.message}`; + + // Reject the promise for Promise-based usage + return Promise.reject(new Error(errorMsg)); + }); }).catch(function(error, response) { source.cancel('Stream ended.'); var userError = {}; @@ -778,12 +793,26 @@ reject(userError); });{{/usePromises}} {{^usePromises}} axios.request(axiosConfig).then(function(response) { - if (callback) { - var data = _this.deserialize(response, returnType); - response = _this.translateResponse(response); - - callback(null, data, response); - } + // Properly wait for the decryption to complete before proceeding + return MLEUtility.checkAndDecryptEncryptedResponse(response.data, _this.merchantConfig) + .then(function(decryptedData) { + response.data = decryptedData; + + if (callback) { + var data = _this.deserialize(response, returnType); + response = _this.translateResponse(response); + + callback(null, data, response); + } + }) + .catch(function(error) { + // Create a simple error object with descriptive message + const errorMsg = `Failed to decrypt response: ${error.message}`; + + if (callback) { + callback(new Error(errorMsg), null, null); + } + }); }).catch(function(error, response) { source.cancel('Stream ended.'); var userError = {}; diff --git a/generator/cybersource-javascript-template/api.mustache b/generator/cybersource-javascript-template/api.mustache index f65c2d3f..81063d49 100644 --- a/generator/cybersource-javascript-template/api.mustache +++ b/generator/cybersource-javascript-template/api.mustache @@ -117,20 +117,21 @@ //check isMLE for an api method 'this.' var inboundMLEStatus = <#vendorExtensions.x-devcenter-metaData.mleForRequest>''<^vendorExtensions.x-devcenter-metaData.mleForRequest>'false'; var isMLEForApi = MLEUtility.checkIsMLEForAPI(this.apiClient.merchantConfig, inboundMLEStatus, ''); + const isResponseMLEForApi = MLEUtility.checkIsResponseMLEForAPI(this.apiClient.merchantConfig, ['']); if (isMLEForApi === true) { MLEUtility.encryptRequestPayload(this.apiClient.merchantConfig, postBody).then(postBody => { return this.apiClient.callApi( '<&path>', '', pathParams, queryParams, headerParams, formParams, postBody, - authNames, contentTypes, accepts, returnType<^usePromises>, callback + authNames, contentTypes, accepts, returnType, isResponseMLEForApi<^usePromises>, callback ); }); } else { return this.apiClient.callApi( '<&path>', '', pathParams, queryParams, headerParams, formParams, postBody, - authNames, contentTypes, accepts, returnType<^usePromises>, callback + authNames, contentTypes, accepts, returnType, isResponseMLEForApi<^usePromises>, callback ); } } From 0cafe70f32b0dcccdfd998948e92d4afe90fe86d Mon Sep 17 00:00:00 2001 From: monkumar Date: Tue, 23 Sep 2025 14:15:50 +0530 Subject: [PATCH 11/30] changing config val to string --- src/authentication/core/MerchantConfig.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index f9caea8b..99d302cf 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -952,7 +952,8 @@ function validateAndSetMapToControlMLEonAPI(mapFromConfig) { this.internalMapToControlRequestMLEonAPI = new Map(); this.internalMapToControlResponseMLEonAPI = new Map(); - for (const[apiFunctionName, configString] of tempMap) { + for (const[apiFunctionName, configValue] of tempMap) { + const configString = String(configValue); var config = Utility.ParseMLEConfigString(configString, logger); logger.debug(`For apiFunctionName: ${apiFunctionName}, parsed config is: `, config); if (config.requestMLE !== undefined) { From ccf235d67c2c14163515cbebef67dde621cde65f Mon Sep 17 00:00:00 2001 From: monkumar Date: Wed, 24 Sep 2025 11:30:39 +0530 Subject: [PATCH 12/30] changing JSON string to object --- src/authentication/util/MLEUtility.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/authentication/util/MLEUtility.js b/src/authentication/util/MLEUtility.js index 963cf8cb..bd32c562 100644 --- a/src/authentication/util/MLEUtility.js +++ b/src/authentication/util/MLEUtility.js @@ -90,7 +90,7 @@ exports.checkAndDecryptEncryptedResponse = function (responseBody, merchantConfi return JWEUtility.decryptJWEUsingPrivateKey(privateKey, responseBody.encryptedResponse) .then(decryptedData => { logger.debug('LOG_NETWORK_RESPONSE_AFTER_MLE_DECRYPTION: ' + JSON.stringify(decryptedData)); - return decryptedData; + return JSON.parse(decryptedData); }) .catch(error => { const errorMsg = `Error decrypting MLE response: ${error.message}`; From bd4d26917576e0302ce5468d20d5fe4847b724a2 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Wed, 24 Sep 2025 19:05:47 +0530 Subject: [PATCH 13/30] logging file name --- src/authentication/util/Utility.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index 966ee97f..9168d8e5 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -212,8 +212,8 @@ exports.readPrivateKeyFromP12 = function(filePath, password, logger) { return rsaPrivateKey; } catch (error) { - logger.error(`Error reading private key from P12 file: ${error.message}`); - ApiException.AuthException(error.message + ". " + Constants.INCORRECT_KEY_PASS); + 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}`); } }; @@ -244,8 +244,8 @@ exports.readPrivateKeyFromPemFile = function(filePath, password, logger) { 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`); - ApiException.AuthException("Password is required for encrypted private key"); + logger.error(`Password is required for encrypted private key: ${filePath}`); + ApiException.AuthException(`Password is required for encrypted private key: ${filePath}`); } try { @@ -261,19 +261,19 @@ exports.readPrivateKeyFromPemFile = function(filePath, password, logger) { } if (!privateKey) { - logger.error(`Failed to parse private key from PEM file`); - ApiException.AuthException("Failed to parse private key from PEM file"); + 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) { - logger.error(`Error parsing private key: ${error.message}`); - ApiException.AuthException("Error parsing private key: " + error.message); + 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: ${error.message}`); - ApiException.AuthException("Error loading private key from PEM file: " + error.message); + logger.error(`Error loading private key from PEM file: ${filePath}: ${error.message}`); + ApiException.AuthException(`Error loading private key from PEM file: ${filePath}: ${error.message}`); } }; From a8565ceb489b3e3ef7a1b91bee3c3be471b0c12a Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Wed, 24 Sep 2025 19:28:48 +0530 Subject: [PATCH 14/30] using a more descriptive message --- src/authentication/util/MLEUtility.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/authentication/util/MLEUtility.js b/src/authentication/util/MLEUtility.js index bd32c562..b80b0a25 100644 --- a/src/authentication/util/MLEUtility.js +++ b/src/authentication/util/MLEUtility.js @@ -93,7 +93,12 @@ exports.checkAndDecryptEncryptedResponse = function (responseBody, merchantConfi return JSON.parse(decryptedData); }) .catch(error => { - const errorMsg = `Error decrypting MLE response: ${error.message}`; + let errorMsg; + if (error.message.includes('no key found') || error.message.includes('key not found')) { + errorMsg = 'Decryption failed: unable to find a suitable decryption key.'; + } else { + errorMsg = `Error decrypting MLE response: ${error.message}`; + } logger.error(errorMsg); // Create a more descriptive error return Promise.reject(new Error(errorMsg)); From 4414e13a86ae3c6cb5638cbb320489b1ee689d31 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Thu, 25 Sep 2025 14:58:11 +0530 Subject: [PATCH 15/30] updated MLE.md --- MLE.md | 450 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 401 insertions(+), 49 deletions(-) diff --git a/MLE.md b/MLE.md index 97ce9518..6ebb5e9e 100644 --- a/MLE.md +++ b/MLE.md @@ -4,95 +4,447 @@ This feature provides an implementation of Message Level Encryption (MLE) for APIs provided by CyberSource, integrated within our SDK. This feature ensures secure communication by encrypting messages at the application level before they are sent over the network. +MLE supports both **Request Encryption** (encrypting outgoing request payloads) and **Response Decryption** (decrypting incoming response payloads). + +## Authentication Requirements + +- **Request MLE**: Only supported with `JWT (JSON Web Token)` authentication type +- **Response MLE**: Only supported with `JWT (JSON Web Token)` authentication type + +
+ ## Configuration -### Global MLE Configuration +## 1. Request MLE Configuration -In the `merchantConfig` object, set the `useMLEGlobally` variable to enable or disable MLE for all supported APIs for the Rest SDK. +#### 1.1 Global Request MLE Configuration -- **Variable**: `useMLEGlobally` -- **Type**: `boolean` +Configure global settings for request MLE using these properties in your `merchantConfig`: + +##### (i) Primary Configuration + +- **Variable**: `enableRequestMLEForOptionalApisGlobally` +- **Type**: `Boolean` - **Default**: `false` -- **Description**: Enables MLE globally for all APIs when set to `true`. If set to `true`, it will enable MLE for all API calls that support MLE by CyberSource, unless overridden by `mapToControlMLEonAPI`. +- **Description**: Enables request MLE globally for all APIs that have optional MLE support when set to `true`. -### API-level MLE Control +--- -Optionally, you can control the MLE feature at the API level using the `mapToControlMLEonAPI` variable in the `merchantConfig` object. +##### (ii) Deprecated Configuration (Backward Compatibility) -- **Variable**: `mapToControlMLEonAPI` -- **Type**: `Map` -- **Description**: Overrides the global MLE setting for specific APIs. The key is the function name of the API in the SDK, and the value is a boolean indicating whether MLE should be enabled (`true`) or disabled (`false`) for that specific API call. +- **Variable**: `useMLEGlobally` ⚠️ **DEPRECATED** +- **Type**: `Boolean` +- **Default**: `false` +- **Description**: **DEPRECATED** - Use `enableRequestMLEForOptionalApisGlobally` instead. This field is maintained for backward compatibility and will be used as an alias for `enableRequestMLEForOptionalApisGlobally`. + +--- + +##### (iii) Advanced Configuration + +- **Variable**: `disableRequestMLEForMandatoryApisGlobally` +- **Type**: `Boolean` +- **Default**: `false` +- **Description**: Disables request MLE for APIs that have mandatory MLE requirement when set to `true`. + +--- + +#### 1.2 Request MLE Certificate Configuration [Optional Params] + +##### (i) Certificate File Path (Optional) -### MLE Key Alias +- **Variable**: `mleForRequestPublicCertPath` +- **Type**: `String` +- **Optional**: `true` +- **Description**: Path to the public certificate file used for request encryption. Supported formats: `.pem`, `.crt`. + - **Note**: This parameter is optional when using JWT authentication. If not provided, the request MLE certificate will be automatically fetched from the JWT authentication P12 file using the `requestMleKeyAlias`. -Another optional parameter for MLE is `requestmleKeyAlias` (formerly known as `mleKeyAlias`), which specifies the key alias used to retrieve the MLE certificate from the JWT P12 file. +--- -- **Variable**: `requestmleKeyAlias` -- **Type**: `string` +##### (ii) Key Alias Configuration (Optional) + +- **Variable**: `requestMleKeyAlias` +- **Type**: `String` +- **Optional**: `true` +- **Default**: `CyberSource_SJC_US` +- **Description**: Key alias used to retrieve the MLE certificate from the certificate file. When `mleForRequestPublicCertPath` is not provided, this alias is used to fetch the certificate from the JWT authentication P12 file. If not specified, the SDK will automatically use the default value `CyberSource_SJC_US`. + +--- + +##### (iii) Deprecated Key Alias (Backward Compatibility) (Optional) + +- **Variable**: `mleKeyAlias` ⚠️ **DEPRECATED** +- **Type**: `String` +- **Optional**: `true` - **Default**: `CyberSource_SJC_US` -- **Description**: By default, CyberSource uses the `CyberSource_SJC_US` public certificate to encrypt the payload. However, users can override this default value by setting their own key alias. +- **Description**: **DEPRECATED** - Use `requestMleKeyAlias` instead. This field is maintained for backward compatibility and will be used as an alias for `requestMleKeyAlias`. + +
+ +## 2. Response MLE Configuration + +#### 2.1 Global Response MLE Configuration + +- **Variable**: `enableResponseMleGlobally` +- **Type**: `Boolean` +- **Default**: `false` +- **Description**: Enables response MLE globally for all APIs that support MLE responses when set to `true`. + +---- + +#### 2.2 Response MLE Private Key Configuration + +##### (i) Option 1: Provide Private Key Object + +- **Variable**: `responseMlePrivateKey` +- **Type**: `PrivateKey` +- **Description**: Direct private key object for response decryption. **Note**: Only PEM format is supported for the private key object. + +--- -## Notes -- If `useMLEGlobally` is set to true, it will enable MLE for all API calls that support MLE by CyberSource, unless overridden by mapToControlMLEonAPI. -- If `mapToControlMLEonAPI` is not provided or does not contain a specific API function name, the global useMLEGlobally setting will be applied. -- The `requestmleKeyAlias` parameter is optional and defaults to CyberSource_SJC_US if not specified by the user. Users can override this default value by setting their own key alias. +##### (ii) Option 2: Provide Private Key File Path -## Example Configuration +- **Variable**: `responseMlePrivateKeyFilePath` +- **Type**: `String` +- **Description**: Path to the private key file. Supported formats: `.p12`, `.pfx`, `.pem`, `.key`, `.p8`. Recommendation use encrypted private Key (password protection) for MLE response. + +--- + +##### (iii) Private Key File Password + +- **Variable**: `responseMlePrivateKeyFilePassword` +- **Type**: `String` +- **Description**: Password for the private key file (required for `.p12/.pfx` files or encrypted private keys). +--- +#### 2.3 Response MLE Additional Configuration + +- **Variable**: `responseMleKID` +- **Type**: `String` +- **Required**: `true` (when response MLE is enabled) +- **Description**: Key ID value for the MLE response certificate (provided in merchant portal). + +
+ +## 3. API-level MLE Control for Request and Response MLE + +### Object Configuration + +- **Variable**: `mapToControlMLEonAPI` +- **Type**: `Object` or `Map` with string keys and string/boolean values +- **Description**: Overrides global MLE settings for specific APIs. The key is the API function name, and the value controls both request and response MLE. +- **Example**: `{ "apiFunctionName": "true::true" }` or `{ "apiFunctionName": true }` + +#### Structure of Values in Object: + +(i) **String format: "requestMLE::responseMLE"** - Control both request and response MLE + - `"true::true"` - Enable both request and response MLE + - `"false::false"` - Disable both request and response MLE + - `"true::false"` - Enable request MLE, disable response MLE + - `"false::true"` - Disable request MLE, enable response MLE + - `"::true"` - Use global setting for request, enable response MLE + - `"true::"` - Enable request MLE, use global setting for response + - `"::false"` - Use global setting for request, disable response MLE + - `"false::"` - Disable request MLE, use global setting for response + +(ii) **Boolean format** - Control request MLE only (response uses global setting) + - `true` - Enable request MLE + - `false` - Disable request MLE + +
+ +## 4. Example Configurations + +### (i) Minimal Request MLE Configuration + +```javascript +// Properties-based configuration - Uses defaults (most common scenario) +var merchantConfig = { + enableRequestMLEForOptionalApisGlobally: true + // Both mleForRequestPublicCertPath and requestMleKeyAlias are optional + // SDK will use JWT P12 file with default alias "CyberSource_SJC_US" +}; +``` + +### (ii) Request MLE with Deprecated Parameters (Backward Compatibility) + +```javascript +// Using deprecated parameters - still supported but not recommended +var merchantConfig = { + useMLEGlobally: true, // Deprecated - use enableRequestMLEForOptionalApisGlobally + mleKeyAlias: "Custom_Key_Alias" // Deprecated - use requestMleKeyAlias +}; +``` + +### (iii) Request MLE with Custom Key Alias + +```javascript +// Properties-based configuration - With custom key alias only +var merchantConfig = { + enableRequestMLEForOptionalApisGlobally: true, + requestMleKeyAlias: "Custom_Key_Alias" + // Will fetch from JWT P12 file using custom alias +}; +``` + +### (iv) Request MLE with Separate Certificate File + +```javascript +// Properties-based configuration - With separate MLE certificate file +var merchantConfig = { + enableRequestMLEForOptionalApisGlobally: true, + mleForRequestPublicCertPath: "/path/to/public/cert.pem", + requestMleKeyAlias: "Custom_Key_Alias", + + // API-specific control with boolean values + mapToControlMLEonAPI: { + "createPayment": true, // Enable request MLE for this API + "capturePayment": false // Disable request MLE for this API + } +}; +``` + +### (v) Response MLE Configuration with Private Key File + +```javascript +// Properties-based configuration +var merchantConfig = { + enableResponseMleGlobally: true, + responseMlePrivateKeyFilePath: "/path/to/private/key.p12", + responseMlePrivateKeyFilePassword: "password", + responseMleKID: "your-key-id", + + // API-specific control with string values + mapToControlMLEonAPI: { + "createPayment": "::true" // Enable response MLE only for this API + } +}; +``` + +### (vi) Response MLE Configuration with Private Key Object + +```javascript +// Load private key programmatically (PEM format only) +var privateKey = loadPrivateKeyFromSomewhere(); + +// Create merchantConfig with private key object +var merchantConfig = { + enableResponseMleGlobally: true, + responseMlePrivateKey: privateKey, // Must be in PEM format + responseMleKID: "your-key-id" +}; +``` + +### (vii) Both Request and Response MLE Configuration + +```javascript +// Properties-based configuration +var merchantConfig = { + // Request MLE settings (minimal - uses defaults) + enableRequestMLEForOptionalApisGlobally: true, + + // Response MLE settings + enableResponseMleGlobally: true, + responseMlePrivateKeyFilePath: "/path/to/private/key.p12", + responseMlePrivateKeyFilePassword: "password", + responseMleKID: "your-key-id", + + // API-specific control for both request and response + mapToControlMLEonAPI: { + "createPayment": "true::true", // Enable both request and response MLE for this API + "capturePayment": "false::true", // Disable request, enable response MLE for this API + "refundPayment": "true::false", // Enable request, disable response MLE for this API + "createCredit": "::true" // Use global request setting, enable response MLE for this API + } +}; +``` + +### (viii) Mixed Configuration (New and Deprecated Parameters) + +```javascript +// Example showing both new and deprecated parameters (deprecated will be used as aliases) +var merchantConfig = { + // If both are set with same value, it works fine + enableRequestMLEForOptionalApisGlobally: true, + useMLEGlobally: true, // Deprecated but same value + + // Key alias - new parameter takes precedence if both are provided + requestMleKeyAlias: "New_Alias", + mleKeyAlias: "Old_Alias" // This will be ignored +}; +``` + +
+ +## 5. JSON Configuration Examples + +### (i) Minimal Request MLE + +```json +{ + "merchantConfig": { + "enableRequestMLEForOptionalApisGlobally": true + } +} +``` + +### (ii) Request MLE with Deprecated Parameters + +```json +{ + "merchantConfig": { + "useMLEGlobally": true, + "mleKeyAlias": "Custom_Key_Alias" + } +} +``` + +### (iii) Request MLE with Custom Configuration ```json { "merchantConfig": { - "useMLEGlobally": true //globally MLE will be enabled for all MLE supported APIs + "enableRequestMLEForOptionalApisGlobally": true, + "mleForRequestPublicCertPath": "/path/to/public/cert.pem", + "requestMleKeyAlias": "Custom_Key_Alias", + "mapToControlMLEonAPI": { + "createPayment": "true", + "capturePayment": "false" + } } } ``` -Or + +### (iv) Response MLE Only ```json { "merchantConfig": { - "useMLEGlobally": true, //globally MLE will be enabled for all MLE supported APIs + "enableResponseMleGlobally": true, + "responseMlePrivateKeyFilePath": "/path/to/private/key.p12", + "responseMlePrivateKeyFilePassword": "password", + "responseMleKID": "your-key-id", "mapToControlMLEonAPI": { - "apiFunctionName1": false, //if want to disable the particular api from list of MLE supported APIs - "apiFunctionName2": true //if want to enable MLE on API which is not in the list of supported MLE APIs for used version of Rest SDK - }, - "requestmleKeyAlias": "Custom_Key_Alias" //optional if any custom value provided by Cybs + "createPayment": "::true" + } } } ``` -Or + +### (v) Both Request and Response MLE ```json { "merchantConfig": { - "useMLEGlobally": false, //globally MLE will be disabled for all APIs + "enableRequestMLEForOptionalApisGlobally": true, + "enableResponseMleGlobally": true, + "responseMlePrivateKeyFilePath": "/path/to/private/key.p12", + "responseMlePrivateKeyFilePassword": "password", + "responseMleKID": "your-key-id", "mapToControlMLEonAPI": { - "apiFunctionName1": true, //if want to enable MLE for API1 - "apiFunctionName2": true //if want to enable MLE for API2 - }, - "requestmleKeyAlias": "Custom_Key_Alias" //optional if any custom value provided by Cybs + "createPayment": "true::true", + "capturePayment": "false::true", + "refundPayment": "true::false", + "createCredit": "::true" + } } } ``` +
+ +## 6. Supported Private Key File Formats + +For Response MLE private key files, the following formats are supported: + +- **PKCS#12**: `.p12`, `.pfx` (requires password) +- **PEM**: `.pem`, `.key`, `.p8` (supports both encrypted and unencrypted) + +
+ +## 7. Important Notes + +### (i) Request MLE +- Both `mleForRequestPublicCertPath` and `requestMleKeyAlias` are **optional** parameters +- If `mleForRequestPublicCertPath` is not provided, the SDK will automatically fetch the MLE certificate from the JWT authentication P12 file +- If `requestMleKeyAlias` is not provided, the SDK will use the default value `CyberSource_SJC_US` +- The SDK provides flexible configuration options: you can use defaults, customize the key alias only, or provide a separate certificate file +- If `enableRequestMLEForOptionalApisGlobally` is set to `true`, it enables request MLE for all APIs that have optional MLE support +- APIs with mandatory MLE requirements are enabled by default unless `disableRequestMLEForMandatoryApisGlobally` is set to `true` +- If `mapToControlMLEonAPI` doesn't contain a specific API, the global setting applies +- For HTTP Signature authentication, request MLE will fall back to non-encrypted requests with a warning + +### (ii) Response MLE +- Response MLE requires either `responseMlePrivateKey` object OR `responseMlePrivateKeyFilePath` (not both) +- The `responseMlePrivateKey` object must be in PEM format +- The `responseMleKID` parameter is mandatory when response MLE is enabled +- If an API expects a mandatory MLE response but the map specifies non-MLE response, the API might return an error +- Both the private key object and file path approaches are mutually exclusive + +### (iii) Backward Compatibility +- `useMLEGlobally` is **deprecated** but still supported as an alias for `enableRequestMLEForOptionalApisGlobally` +- `mleKeyAlias` is **deprecated** but still supported as an alias for `requestMleKeyAlias` +- If both new and deprecated parameters are provided with the **same value**, it works fine +- If both new and deprecated parameters are provided with **different values**, it will cause a `ConfigException` +- When both new and deprecated parameters are provided, the **new parameter takes precedence** + +### (iv) API-level Control Validation +- The `mapToControlMLEonAPI` values are validated for proper format +- Invalid formats (empty values, multiple separators, non-boolean values) will cause configuration errors +- Empty string after `::` separator will use global defaults +- The object also supports backward compatibility with boolean values, which will be automatically converted to control request MLE only -In the above examples: -- MLE is enabled/disabled globally (`useMLEGlobally` is true/false). -- `apiFunctionName1` will have MLE disabled/enabled based on value provided. -- `apiFunctionName2` will have MLE enabled. -- `requestmleKeyAlias` is set to `Custom_Key_Alias`, overriding the default value. +### (v) Configuration Validation +- The SDK performs comprehensive validation of MLE configuration parameters +- Conflicting values between new and deprecated parameters will result in `ConfigException` +- File path validation is performed for certificate and private key files +- Invalid boolean values in `mapToControlMLEonAPI` will cause parsing errors -Please refer given link for sample codes with MLE: -https://github.com/CyberSource/cybersource-rest-samples-node/tree/master/Samples/MLEFeature +
+ +## 8. Error Handling + +The SDK provides specific error messages for common MLE issues: +- Invalid private key for response decryption +- Missing certificates for request encryption +- Invalid file formats or paths +- Authentication type mismatches +- Configuration validation errors +- Conflicting parameter values between new and deprecated fields +- Invalid format in `mapToControlMLEonAPI` values + +
+ +## 9. Sample Code Repository + +For comprehensive examples and sample implementations, please refer to: +[Cybersource Node.js Sample Code Repository (on GitHub)](https://github.com/CyberSource/cybersource-rest-samples-node/tree/master/Samples/MLEFeature) + +
+ +## 10. Additional Information + +### (i) API Support +- MLE is designed to support specific APIs that have been enabled for encryption +- Support can be extended to additional APIs based on requirements and updates + +### (ii) Using the SDK +To use the MLE feature in the SDK, configure the `merchantConfig` object as shown above and pass it to the SDK initialization. The SDK will automatically handle encryption and decryption based on your configuration. + +### (iii) Migration from Deprecated Parameters + +If you're currently using deprecated parameters, here's how to migrate: + +```javascript +// OLD (Deprecated) +merchantConfig.useMLEGlobally = true; +merchantConfig.mleKeyAlias = "Custom_Alias"; + +// NEW (Recommended) +merchantConfig.enableRequestMLEForOptionalApisGlobally = true; +merchantConfig.requestMleKeyAlias = "Custom_Alias"; +``` -## Additional Information +The deprecated parameters will continue to work but are not recommended for new implementations. -### API Support -- MLE is initially designed to support a few APIs. -- It can be extended to support more APIs in the future based on requirements and updates. -### Authentication Type -- MLE is only supported with `JWT (JSON Web Token)` authentication type within the SDK. -### Using the SDK -To use the MLE feature in the SDK, configure the `merchantConfig` object as shown above and pass it to the SDK initialization. +
-## Contact +## 11. Contact For any issues or further assistance, please open an issue on the GitHub repository or contact our support team. From b69c2fb4c7132bd920e6cfe5f965440e9d6c0b02 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Mon, 29 Sep 2025 11:33:50 +0530 Subject: [PATCH 16/30] accepting string/jwk object as mle repsone private key --- package.json | 3 +- src/authentication/core/MerchantConfig.js | 20 ++++++++++-- src/authentication/util/Utility.js | 39 +++++++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index be87daf5..870c199e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "promise": "^8.3.0", "winston": "^3.11.0", "winston-daily-rotate-file": "^4.7.1", - "node-jose": "^2.2.0" + "node-jose": "^2.2.0", + "jwk-to-pem": "^2.0.7" }, "keywords": [ "nodeJS" diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index 99d302cf..71777c61 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -147,7 +147,7 @@ function MerchantConfig(result) { * PrivateKey instance used for Response MLE decryption by the SDK. * Optional — either provide this object directly or specify the private key file path via configuration. */ - this.responseMlePrivateKey = result.responseMlePrivateKey; + this.setResponseMlePrivateKey(result.responseMlePrivateKey); this.mapToControlMLEonAPI = result.mapToControlMLEonAPI; @@ -563,7 +563,23 @@ MerchantConfig.prototype.getResponseMlePrivateKey = function getResponseMlePriva } MerchantConfig.prototype.setResponseMlePrivateKey = function setResponseMlePrivateKey(responseMlePrivateKey) { - this.responseMlePrivateKey = responseMlePrivateKey; + var logger = Logger.getLogger(this, 'MerchantConfig'); + + if (responseMlePrivateKey) { + logger.debug('Processing response MLE private key'); + + try { + // Use synchronous version of parseAndReturnPem + const pemKey = Utility.parseAndReturnPem(responseMlePrivateKey, logger); + logger.debug('Successfully parsed response MLE private key'); + this.responseMlePrivateKey = pemKey; + } catch (error) { + logger.error(`Error parsing response MLE private key: ${error.message}`); + throw new ApiException.ApiException(`Error parsing response MLE private key: ${error.message}`, logger); + } + } else { + this.responseMlePrivateKey = responseMlePrivateKey; + } } MerchantConfig.prototype.getInternalMapToControlResponseMLEonAPI = function getInternalMapToControlResponseMLEonAPI() { diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index 9168d8e5..c6b3b80a 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -4,6 +4,7 @@ var ApiException = require('./ApiException'); var Constants = require('./Constants'); var fs = require('fs'); var forge = require('node-forge'); +var jwkToPem = require('jwk-to-pem'); exports.getResponseCodeMessage = function (responseCode) { @@ -277,3 +278,41 @@ exports.readPrivateKeyFromPemFile = function(filePath, password, logger) { ApiException.AuthException(`Error loading private key from PEM file: ${filePath}: ${error.message}`); } }; + +exports.parseAndReturnPem = function(key, logger) { + 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'); + 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'); + } +} From ec681cde2d0011a62b5deaaa51ad67af1ad17703 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Tue, 30 Sep 2025 13:53:00 +0530 Subject: [PATCH 17/30] handelling encrypted pem string --- src/authentication/core/MerchantConfig.js | 7 ++- src/authentication/util/Utility.js | 52 +++++++++++++++++++---- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index 71777c61..76c76014 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -569,8 +569,11 @@ MerchantConfig.prototype.setResponseMlePrivateKey = function setResponseMlePriva logger.debug('Processing response MLE private key'); try { - // Use synchronous version of parseAndReturnPem - const pemKey = Utility.parseAndReturnPem(responseMlePrivateKey, logger); + const pemKey = Utility.parseAndReturnPem( + responseMlePrivateKey, + logger, + this.responseMlePrivateKeyFilePassword + ); logger.debug('Successfully parsed response MLE private key'); this.responseMlePrivateKey = pemKey; } catch (error) { diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index c6b3b80a..32698999 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -279,19 +279,53 @@ exports.readPrivateKeyFromPemFile = function(filePath, password, logger) { } }; -exports.parseAndReturnPem = function(key, logger) { +exports.parseAndReturnPem = function(key, logger, password) { 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'); - 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'); + + // 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() === '') { + logger.error('Password is required for encrypted private key'); + throw new Error('Password is required for encrypted private key'); + } + + 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'); From 9835ddb909a4be67cd2f22103ecd8d7483f227ea Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Tue, 30 Sep 2025 15:38:35 +0530 Subject: [PATCH 18/30] corrected err message --- src/authentication/util/Utility.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index 32698999..364852eb 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -270,8 +270,13 @@ exports.readPrivateKeyFromPemFile = function(filePath, password, logger) { return forge.pki.privateKeyToPem(privateKey); } catch (error) { - logger.error(`Error parsing private key from ${filePath}: ${error.message}`); - ApiException.AuthException(`Error parsing private key from ${filePath}: ${error.message}`); + 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}`); From 1c55d182cb24730afe1993bcbf605be230fcccd2 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Mon, 6 Oct 2025 16:53:35 +0530 Subject: [PATCH 19/30] addressing PR comments --- MLE.md | 28 ++++++++++++----------- src/authentication/core/MerchantConfig.js | 3 ++- src/authentication/util/MLEUtility.js | 1 + src/authentication/util/Utility.js | 8 ++++--- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/MLE.md b/MLE.md index 6ebb5e9e..094e7fe1 100644 --- a/MLE.md +++ b/MLE.md @@ -129,13 +129,13 @@ Configure global settings for request MLE using these properties in your `mercha ### Object Configuration - **Variable**: `mapToControlMLEonAPI` -- **Type**: `Object` or `Map` with string keys and string/boolean values +- **Type**: `Object` or `Map` with string keys and string values - **Description**: Overrides global MLE settings for specific APIs. The key is the API function name, and the value controls both request and response MLE. -- **Example**: `{ "apiFunctionName": "true::true" }` or `{ "apiFunctionName": true }` +- **Example**: `{ "apiFunctionName": "true::true" }` #### Structure of Values in Object: -(i) **String format: "requestMLE::responseMLE"** - Control both request and response MLE +(i) **"requestMLE::responseMLE"** - Control both request and response MLE - `"true::true"` - Enable both request and response MLE - `"false::false"` - Disable both request and response MLE - `"true::false"` - Enable request MLE, disable response MLE @@ -145,9 +145,10 @@ Configure global settings for request MLE using these properties in your `mercha - `"::false"` - Use global setting for request, disable response MLE - `"false::"` - Disable request MLE, use global setting for response -(ii) **Boolean format** - Control request MLE only (response uses global setting) - - `true` - Enable request MLE - - `false` - Disable request MLE +(ii) **"requestMLE"** - Control request MLE only (response uses global setting) + - `"true"` - Enable request MLE + - `"false"` - Disable request MLE +
@@ -194,10 +195,10 @@ var merchantConfig = { mleForRequestPublicCertPath: "/path/to/public/cert.pem", requestMleKeyAlias: "Custom_Key_Alias", - // API-specific control with boolean values + // API-specific control with string values mapToControlMLEonAPI: { - "createPayment": true, // Enable request MLE for this API - "capturePayment": false // Disable request MLE for this API + "createPayment": "true", // Enable request MLE for this API (simple format) + "capturePayment": "false::" // Disable request MLE for this API (full format) } }; ``` @@ -386,16 +387,17 @@ For Response MLE private key files, the following formats are supported: - When both new and deprecated parameters are provided, the **new parameter takes precedence** ### (iv) API-level Control Validation -- The `mapToControlMLEonAPI` values are validated for proper format -- Invalid formats (empty values, multiple separators, non-boolean values) will cause configuration errors +- The `mapToControlMLEonAPI` values are validated for proper format using string format +- Invalid formats (empty values, multiple separators) will cause configuration errors - Empty string after `::` separator will use global defaults -- The object also supports backward compatibility with boolean values, which will be automatically converted to control request MLE only +- **Note**: Boolean values are supported for backward compatibility but are deprecated. Use string format for new implementations ### (v) Configuration Validation - The SDK performs comprehensive validation of MLE configuration parameters - Conflicting values between new and deprecated parameters will result in `ConfigException` - File path validation is performed for certificate and private key files -- Invalid boolean values in `mapToControlMLEonAPI` will cause parsing errors +- Invalid string format values in `mapToControlMLEonAPI` will cause parsing errors +- **Note**: Boolean values in `mapToControlMLEonAPI` are deprecated but still supported for backward compatibility
diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index 76c76014..428c4f5d 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -572,7 +572,8 @@ MerchantConfig.prototype.setResponseMlePrivateKey = function setResponseMlePriva const pemKey = Utility.parseAndReturnPem( responseMlePrivateKey, logger, - this.responseMlePrivateKeyFilePassword + this.responseMlePrivateKeyFilePassword, + 'responseMlePrivateKeyFilePassword' ); logger.debug('Successfully parsed response MLE private key'); this.responseMlePrivateKey = pemKey; diff --git a/src/authentication/util/MLEUtility.js b/src/authentication/util/MLEUtility.js index b80b0a25..2dbdb8f9 100644 --- a/src/authentication/util/MLEUtility.js +++ b/src/authentication/util/MLEUtility.js @@ -76,6 +76,7 @@ exports.checkAndDecryptEncryptedResponse = function (responseBody, merchantConfi logger.debug('LOG_NETWORK_RESPONSE_BEFORE_MLE_DECRYPTION: ' + JSON.stringify(responseBody)); try { + // Private key from config will take precedence over file path. const privateKey = merchantConfig.getResponseMlePrivateKey() || Cache.getMleResponsePrivateKeyFromFilePath(merchantConfig); diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index 364852eb..92712a42 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -284,7 +284,7 @@ exports.readPrivateKeyFromPemFile = function(filePath, password, logger) { } }; -exports.parseAndReturnPem = function(key, logger, password) { +exports.parseAndReturnPem = function(key, logger, password, passwordPropertyName) { logger.debug(`Parsing private key to PEM format synchronously, key type: ${typeof key}`); if (typeof key === 'string') { @@ -298,8 +298,10 @@ exports.parseAndReturnPem = function(key, logger, password) { // Check if password is provided for encrypted key if (!password || password.trim() === '') { - logger.error('Password is required for encrypted private key'); - throw new Error('Password is required for encrypted private key'); + 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 { From 40e0d22d5d93eb97b6d85f383e0eb9faa44a76b3 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Mon, 6 Oct 2025 19:48:25 +0530 Subject: [PATCH 20/30] minor fix --- MLE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MLE.md b/MLE.md index 094e7fe1..fcc3f6c3 100644 --- a/MLE.md +++ b/MLE.md @@ -97,7 +97,7 @@ Configure global settings for request MLE using these properties in your `mercha - **Variable**: `responseMlePrivateKey` - **Type**: `PrivateKey` -- **Description**: Direct private key object for response decryption. **Note**: Only PEM format is supported for the private key object. +- **Description**: Direct private key object for response decryption. **Note**: Supports both PEM format private key objects and raw JWK (JSON Web Key) objects. When using JWK format, ensure the key contains the required cryptographic parameters for RSA private keys (n, e, d, p, q, dp, dq, qi). --- @@ -223,13 +223,13 @@ var merchantConfig = { ### (vi) Response MLE Configuration with Private Key Object ```javascript -// Load private key programmatically (PEM format only) +// Load private key programmatically (PEM format or JWK object) var privateKey = loadPrivateKeyFromSomewhere(); // Create merchantConfig with private key object var merchantConfig = { enableResponseMleGlobally: true, - responseMlePrivateKey: privateKey, // Must be in PEM format + responseMlePrivateKey: privateKey, // Supports PEM format or JWK object responseMleKID: "your-key-id" }; ``` @@ -374,7 +374,7 @@ For Response MLE private key files, the following formats are supported: ### (ii) Response MLE - Response MLE requires either `responseMlePrivateKey` object OR `responseMlePrivateKeyFilePath` (not both) -- The `responseMlePrivateKey` object must be in PEM format +- The `responseMlePrivateKey` object supports both PEM format and JWK (JSON Web Key) objects - The `responseMleKID` parameter is mandatory when response MLE is enabled - If an API expects a mandatory MLE response but the map specifies non-MLE response, the API might return an error - Both the private key object and file path approaches are mutually exclusive From 8877cc56f2a4f45b07ff75de326d268b9549ce09 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Thu, 6 Nov 2025 13:36:55 +0530 Subject: [PATCH 21/30] masked sensitive information in logs --- src/authentication/logging/SensitiveDataMasker.js | 9 ++++++++- src/authentication/logging/SensitiveDataTags.js | 2 +- src/authentication/util/Constants.js | 2 ++ src/authentication/util/MLEUtility.js | 4 ++-- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/authentication/logging/SensitiveDataMasker.js b/src/authentication/logging/SensitiveDataMasker.js index e0edbf8d..c38b4643 100644 --- a/src/authentication/logging/SensitiveDataMasker.js +++ b/src/authentication/logging/SensitiveDataMasker.js @@ -14,8 +14,15 @@ function maskSensitiveData(message) { return Constants.LOG_REQUEST_AFTER_MLE + maskSensitiveData(message.substring(Constants.LOG_REQUEST_AFTER_MLE.length)); } + if (typeof message === 'string' && message.startsWith(Constants.LOG_RESPONSE_AFTER_MLE)) { + return Constants.LOG_RESPONSE_AFTER_MLE + maskSensitiveData(message.substring(Constants.LOG_RESPONSE_AFTER_MLE.length)); + } + if (typeof message === 'string' && message.startsWith(Constants.LOG_RESPONSE_BEFORE_MLE)) { + return Constants.LOG_RESPONSE_BEFORE_MLE + maskSensitiveData(message.substring(Constants.LOG_RESPONSE_BEFORE_MLE.length)); + } + if (Utility.isJsonString(message)) { - jsonMsg = JSON.parse(message) + jsonMsg = JSON.parse(message); } else { jsonMsg = JSON.parse(JSON.stringify(message)); } diff --git a/src/authentication/logging/SensitiveDataTags.js b/src/authentication/logging/SensitiveDataTags.js index 37bcf326..64008b16 100644 --- a/src/authentication/logging/SensitiveDataTags.js +++ b/src/authentication/logging/SensitiveDataTags.js @@ -35,6 +35,6 @@ exports.getSensitiveDataTags = function () { tags.push("prefix"); tags.push("bin"); tags.push("encryptedRequest"); - + tags.push("encryptedResponse"); return tags; } \ No newline at end of file diff --git a/src/authentication/util/Constants.js b/src/authentication/util/Constants.js index 9a149d7d..b1ae6264 100644 --- a/src/authentication/util/Constants.js +++ b/src/authentication/util/Constants.js @@ -44,6 +44,8 @@ module.exports = { END_TRANSACTION : "************************ LOGGING END ************************", LOG_REQUEST_BEFORE_MLE : "Request before MLE: ", LOG_REQUEST_AFTER_MLE : "Request after MLE: ", + LOG_RESPONSE_BEFORE_MLE : "Response before MLE decryption: ", + LOG_RESPONSE_AFTER_MLE : "Response after MLE decryption: ", MERCHANTID : "MERCHANTID", MERCHANT_KEY_ID : "MERCHANT_KEY_ID", MERCHANT_SECERT_KEY : "MERCHANT_SECERT_KEY", diff --git a/src/authentication/util/MLEUtility.js b/src/authentication/util/MLEUtility.js index 2dbdb8f9..65da5e82 100644 --- a/src/authentication/util/MLEUtility.js +++ b/src/authentication/util/MLEUtility.js @@ -73,7 +73,7 @@ exports.checkAndDecryptEncryptedResponse = function (responseBody, merchantConfi } logger.debug('Response body contains encrypted data, attempting to decrypt'); - logger.debug('LOG_NETWORK_RESPONSE_BEFORE_MLE_DECRYPTION: ' + JSON.stringify(responseBody)); + logger.debug(Constants.LOG_RESPONSE_BEFORE_MLE + JSON.stringify(responseBody)); try { // Private key from config will take precedence over file path. @@ -90,7 +90,7 @@ exports.checkAndDecryptEncryptedResponse = function (responseBody, merchantConfi return JWEUtility.decryptJWEUsingPrivateKey(privateKey, responseBody.encryptedResponse) .then(decryptedData => { - logger.debug('LOG_NETWORK_RESPONSE_AFTER_MLE_DECRYPTION: ' + JSON.stringify(decryptedData)); + logger.debug(Constants.LOG_RESPONSE_AFTER_MLE + decryptedData); return JSON.parse(decryptedData); }) .catch(error => { From 032444185d9356c688a7c7f728d8732eb3fedb99 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Thu, 13 Nov 2025 11:52:21 +0530 Subject: [PATCH 22/30] using serial number as response mle kid for cybersource generated p12 --- src/authentication/core/MerchantConfig.js | 56 +++++- src/authentication/util/Constants.js | 1 + src/authentication/util/Utility.js | 205 ++++++++++++++++++++-- 3 files changed, 247 insertions(+), 15 deletions(-) diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index d12d28fe..48c93ff8 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -937,9 +937,18 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { } } - // Validate KID - if (typeof this.responseMleKID !== "string" || !this.responseMleKID?.trim()) { - throw new ApiException.ApiException("responseMleKID is required when response MLE is enabled.", logger); + // Validate KID - Auto-extract from CyberSource P12 if applicable, then validate + if (!this.responseMleKID || !this.responseMleKID?.trim()) { + tryAutoExtractResponseMleKid.call(this, logger); + + if (typeof this.responseMleKID !== "string" || !this.responseMleKID?.trim()) { + throw new ApiException.ApiException( + "responseMleKID is required when response MLE is enabled. " + + "For CyberSource-generated P12 certificates, this will be auto-extracted. " + + "For other certificate types, please provide it explicitly in your configuration.", + logger + ); + } } } /** @@ -970,6 +979,47 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { } +function tryAutoExtractResponseMleKid(logger) { + const hasValidFilePath = typeof this.responseMlePrivateKeyFilePath === "string" && this.responseMlePrivateKeyFilePath.trim() !== ""; + + if (!hasValidFilePath) { + return; + } + + const fileExtension = path.extname(this.responseMlePrivateKeyFilePath).toLowerCase(); + const isP12File = fileExtension === ".p12"; + + if (!isP12File) { + logger.debug('Private key file is not a P12 file, skipping auto-extraction of responseMleKID'); + return; + } + + const isCybersourceP12 = Utility.isCybersourceP12( + this.responseMlePrivateKeyFilePath, + this.responseMlePrivateKeyFilePassword, + logger + ); + + if (!isCybersourceP12) { + logger.debug('P12 file is not a CyberSource-generated certificate, skipping auto-extraction of responseMleKID'); + return; + } + + logger.debug('Detected CyberSource P12 file, attempting to auto-extract responseMleKID'); + try { + const extractedKid = Utility.extractResponseMleKid( + this.responseMlePrivateKeyFilePath, + this.responseMlePrivateKeyFilePassword, + this.merchantID, + logger + ); + this.setResponseMleKID(extractedKid); + logger.info('Successfully auto-extracted responseMleKID from CyberSource P12 certificate'); + } catch (error) { + logger.warn(`Failed to auto-extract responseMleKID from P12 file: ${error.message}. Please provide responseMleKID manually.`); + } +} + function validateAndSetMapToControlMLEonAPI(mapFromConfig) { let tempMap; var logger = Logger.getLogger(this, 'MerchantConfig'); diff --git a/src/authentication/util/Constants.js b/src/authentication/util/Constants.js index b1ae6264..00180bc7 100644 --- a/src/authentication/util/Constants.js +++ b/src/authentication/util/Constants.js @@ -29,6 +29,7 @@ module.exports = { CERTIFICATE_EXPIRY_DATE_WARNING_DAYS : 90, FACTOR_DAYS_TO_MILLISECONDS : 24 * 60 * 60 * 1000, DEFAULT_MLE_ALIAS_FOR_CERT : "CyberSource_SJC_US", + CYBERSOURCE_P12_CERT_ALIAS : "CyberSource_SJC_US", MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT : "_mleCertFromMerchantConfig", MLE_CACHE_IDENTIFIER_FOR_P12_CERT : "_mleCertFromP12", diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index 92712a42..bdcb074c 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -172,6 +172,32 @@ exports.ParseMLEConfigString = function (configString, logger) { } } +/** + * Internal helper: 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 + */ +function parseP12File(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 @@ -183,18 +209,7 @@ exports.readPrivateKeyFromP12 = function(filePath, password, logger) { try { logger.debug(`Reading private key from P12 file: ${filePath}`); - if (!fs.existsSync(filePath)) { - logger.error(`File not found: ${filePath}`); - ApiException.AuthException(Constants.FILE_NOT_FOUND + filePath); - } - - // Read the P12 file and convert to ASN1 - 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 read P12 file and converted to ASN1`); + var p12 = parseP12File(filePath, password, logger); // Extract the private key var keyBags = p12.getBags({ bagType: forge.pki.oids.keyBag }); @@ -357,3 +372,169 @@ exports.parseAndReturnPem = function(key, logger, password, passwordPropertyName 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}`); + + const p12 = parseP12File(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.CYBERSOURCE_P12_CERT_ALIAS) { + logger.debug(`Found CyberSource certificate (CN=${Constants.CYBERSOURCE_P12_CERT_ALIAS})`); + return true; + } + } + return false; + }); + + if (!hasCybersourceCert) { + logger.debug(`P12 file does not contain CyberSource certificate (CN=${Constants.CYBERSOURCE_P12_CERT_ALIAS})`); + 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 and exactly one private key'); + return true; + + } catch (error) { + logger.error(`Error checking if P12 file is generated by CyberSource: ${error.message}`); + return false; + } +}; + +/** + * Extracts the serial number (KID) from a certificate's subject in a P12 file where CN matches the merchantId + * @param {string} filePath - Path to the P12 file + * @param {string} password - Password for the P12 file + * @param {string} merchantId - The merchant ID to match against the CN in the certificate subject + * @param {object} logger - Logger object for logging messages + * @returns {string} - The serial number extracted from the certificate's subject attributes + * @throws Will throw an error if the certificate with matching CN is not found or serial number is missing + */ +exports.extractResponseMleKid = function(filePath, password, merchantId, logger) { + try { + logger.debug(`Extracting MLE KID from P12 file: ${filePath} for merchantId: ${merchantId}`); + + const p12 = parseP12File(filePath, password, logger); + + // Get certificate bags from P12 + const certBags = p12.getBags({ bagType: forge.pki.oids.certBag }); + const certs = certBags[forge.pki.oids.certBag]; + + if (!certs || certs.length === 0) { + logger.error(`No certificates found in P12 file: ${filePath}`); + ApiException.AuthException(`No certificates found in P12 file: ${filePath}`); + } + + logger.debug(`Found ${certs.length} certificate(s) in P12 file`); + + // Iterate through certificates to find one with matching CN + for (let i = 0; i < certs.length; i++) { + const certBag = certs[i]; + const cert = certBag.cert; + + if (!cert || !cert.subject || !cert.subject.attributes) { + logger.debug(`Certificate ${i + 1} has no subject attributes, skipping`); + continue; + } + + // Extract CN from certificate subject + let cn = null; + for (const attr of cert.subject.attributes) { + if (attr.name === 'commonName' || attr.shortName === 'CN') { + cn = attr.value; + break; + } + } + + if (!cn) { + logger.debug(`Certificate ${i + 1} has no CN in subject, skipping`); + continue; + } + + logger.debug(`Certificate ${i + 1} CN: ${cn}`); + + // Check if CN matches merchantId (case-insensitive) + if (cn.toLowerCase() === merchantId.toLowerCase()) { + logger.debug(`Found certificate with matching CN: ${cn}`); + + // Extract serial number from certificate subject + let serialNumber = null; + for (const attr of cert.subject.attributes) { + if (attr.name === 'serialNumber' || attr.shortName === 'serialNumber') { + serialNumber = attr.value; + break; + } + } + + if (!serialNumber) { + logger.debug(`Certificate with CN=${cn} has no serialNumber in subject, continuing search`); + continue; + } + + logger.debug(`Serial number (MLE KID) extracted from certificate subject: ${serialNumber}`); + + return serialNumber; + } + } + + // If we get here, no matching certificate was found + logger.error(`No certificate with CN matching merchantId (${merchantId}) and valid serialNumber found in P12 file: ${filePath}`); + ApiException.AuthException(`No certificate with CN matching merchantId (${merchantId}) found in P12 file: ${filePath}`); + + } catch (error) { + logger.error(`Error extracting MLE KID from P12 file: ${filePath}: ${error.message}`); + ApiException.AuthException(`Error extracting MLE KID from P12 file: ${filePath}: ${error.message}`); + } +}; From 106eeb736100df043f5bb2d58a81ab470020e509 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Thu, 13 Nov 2025 13:35:58 +0530 Subject: [PATCH 23/30] using cert.serial number in case of no serial no attribute --- src/authentication/util/Utility.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index bdcb074c..7017a52b 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -509,21 +509,17 @@ exports.extractResponseMleKid = function(filePath, password, merchantId, logger) if (cn.toLowerCase() === merchantId.toLowerCase()) { logger.debug(`Found certificate with matching CN: ${cn}`); - // Extract serial number from certificate subject - let serialNumber = null; - for (const attr of cert.subject.attributes) { - if (attr.name === 'serialNumber' || attr.shortName === 'serialNumber') { - serialNumber = attr.value; - break; - } - } + const serialNumberAttr = cert.subject.attributes.find(attr => attr.name === 'serialNumber'); + let serialNumber; - if (!serialNumber) { - logger.debug(`Certificate with CN=${cn} has no serialNumber in subject, continuing search`); - continue; + if (serialNumberAttr) { + serialNumber = serialNumberAttr.value; + } else { + logger.warn(`Serial number not found in certificate subject for merchantId ${merchantId}, using certificate serial number as fallback`); + serialNumber = cert.serialNumber; } - logger.debug(`Serial number (MLE KID) extracted from certificate subject: ${serialNumber}`); + logger.debug(`Serial number (MLE KID) extracted: ${serialNumber}`); return serialNumber; } From fda34988d2e31978cc80465f06040a25873ea15d Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Mon, 17 Nov 2025 15:26:57 +0530 Subject: [PATCH 24/30] moving response mle kid to Utility --- src/authentication/core/MerchantConfig.js | 56 ----------------- src/authentication/jwt/JWTSigToken.js | 4 +- src/authentication/util/Cache.js | 43 +++++++++++++ src/authentication/util/Constants.js | 1 + src/authentication/util/Utility.js | 73 ++++++++++++++++++++++- 5 files changed, 119 insertions(+), 58 deletions(-) diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index 48c93ff8..d5a85250 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -936,20 +936,6 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { ); } } - - // Validate KID - Auto-extract from CyberSource P12 if applicable, then validate - if (!this.responseMleKID || !this.responseMleKID?.trim()) { - tryAutoExtractResponseMleKid.call(this, logger); - - if (typeof this.responseMleKID !== "string" || !this.responseMleKID?.trim()) { - throw new ApiException.ApiException( - "responseMleKID is required when response MLE is enabled. " + - "For CyberSource-generated P12 certificates, this will be auto-extracted. " + - "For other certificate types, please provide it explicitly in your configuration.", - logger - ); - } - } } /** * This method is to log all merchantConfic properties @@ -978,48 +964,6 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { } } - -function tryAutoExtractResponseMleKid(logger) { - const hasValidFilePath = typeof this.responseMlePrivateKeyFilePath === "string" && this.responseMlePrivateKeyFilePath.trim() !== ""; - - if (!hasValidFilePath) { - return; - } - - const fileExtension = path.extname(this.responseMlePrivateKeyFilePath).toLowerCase(); - const isP12File = fileExtension === ".p12"; - - if (!isP12File) { - logger.debug('Private key file is not a P12 file, skipping auto-extraction of responseMleKID'); - return; - } - - const isCybersourceP12 = Utility.isCybersourceP12( - this.responseMlePrivateKeyFilePath, - this.responseMlePrivateKeyFilePassword, - logger - ); - - if (!isCybersourceP12) { - logger.debug('P12 file is not a CyberSource-generated certificate, skipping auto-extraction of responseMleKID'); - return; - } - - logger.debug('Detected CyberSource P12 file, attempting to auto-extract responseMleKID'); - try { - const extractedKid = Utility.extractResponseMleKid( - this.responseMlePrivateKeyFilePath, - this.responseMlePrivateKeyFilePassword, - this.merchantID, - logger - ); - this.setResponseMleKID(extractedKid); - logger.info('Successfully auto-extracted responseMleKID from CyberSource P12 certificate'); - } catch (error) { - logger.warn(`Failed to auto-extract responseMleKID from P12 file: ${error.message}. Please provide responseMleKID manually.`); - } -} - function validateAndSetMapToControlMLEonAPI(mapFromConfig) { let tempMap; var logger = Logger.getLogger(this, 'MerchantConfig'); diff --git a/src/authentication/jwt/JWTSigToken.js b/src/authentication/jwt/JWTSigToken.js index 15aaa388..c9437d61 100644 --- a/src/authentication/jwt/JWTSigToken.js +++ b/src/authentication/jwt/JWTSigToken.js @@ -5,6 +5,7 @@ const Constants = require('../util/Constants'); const KeyCertificate = require('./KeyCertificateGenerator'); const DigestGenerator = require('../payloadDigest/DigestGenerator'); const ApiException = require('../util/ApiException'); +const Utility = require('../util/Utility'); // Constants for algorithms const JWT_ALGORITHM = 'RS256'; @@ -52,8 +53,9 @@ exports.getToken = function (merchantConfig, isResponseMLEForApi, logger) { // Add MLE key ID if MLE is enabled if (isResponseMLEForApi === true) { + const responseMleKID = Utility.validateAndAutoExtractResponseMleKid(merchantConfig, logger); // Using bracket notation for property name with hyphens - claimSetJson["v-c-response-mle-kid"] = merchantConfig.getResponseMleKID(); + claimSetJson["v-c-response-mle-kid"] = responseMleKID; } const customHeader = { diff --git a/src/authentication/util/Cache.js b/src/authentication/util/Cache.js index cefe77be..cbd1dc5f 100644 --- a/src/authentication/util/Cache.js +++ b/src/authentication/util/Cache.js @@ -281,3 +281,46 @@ function putMLEResponsePrivateKeyInCache(merchantConfig, cacheKey, privateKeyPat }; cache.put(cacheKey, cacheEntry); } + +exports.fetchCachedP12FromFile = function(filePath, password, logger, cacheKey) { + // Use provided cache key or default to filePath + identifier + const finalCacheKey = cacheKey || (filePath + Constants.RESPONSE_MLE_P12_PFX_CACHE_IDENTIFIER); + const cachedEntry = cache.get(finalCacheKey); + + logger.debug(`Fetching P12/PFX from cache with key: ${finalCacheKey}`); + + // Check if file exists + if (!fs.existsSync(filePath)) { + logger.error(`File not found: ${filePath}`); + throw new Error(Constants.FILE_NOT_FOUND + filePath); + } + + const currentFileLastModifiedTime = fs.statSync(filePath).mtimeMs; + + // Check if cache is valid (exists and file hasn't been modified) + if (cachedEntry && cachedEntry.fileLastModifiedTime === currentFileLastModifiedTime) { + logger.debug(`P12/PFX found in cache and file not modified`); + return cachedEntry.p12Object; + } + + // Cache miss or file modified - parse and cache + logger.debug(`P12/PFX not in cache or file modified. Loading from file: ${filePath}`); + + try { + const p12Asn1 = loadP12FileToAsn1(filePath); + const p12Object = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, password); + + // Store in cache with file modification time + cache.put(finalCacheKey, { + p12Object: p12Object, + fileLastModifiedTime: currentFileLastModifiedTime + }); + + logger.debug(`Successfully cached P12/PFX object`); + return p12Object; + + } catch (error) { + logger.error(`Error parsing P12/PFX file: ${error.message}`); + ApiException.AuthException(`${error.message}. ${Constants.INCORRECT_KEY_PASS}`); + } +}; diff --git a/src/authentication/util/Constants.js b/src/authentication/util/Constants.js index 00180bc7..0c3e109c 100644 --- a/src/authentication/util/Constants.js +++ b/src/authentication/util/Constants.js @@ -103,6 +103,7 @@ module.exports = { DEFAULT_MAX_IDLE_SOCKETS : 100, DEFAULT_USER_DEFINED_TIMEOUT : 4000, // Value in milliseconds MLE_CACHE_KEY_IDENTIFIER_FOR_RESPONSE_PRIVATE_KEY : "_mleResponsePrivateKeyFromFile", + RESPONSE_MLE_P12_PFX_CACHE_IDENTIFIER : "_responseMleP12Pfx", STATUS200 : "Transaction Successful", STATUS400 : "Bad Request", diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index 7017a52b..9f4cf15d 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -385,7 +385,9 @@ exports.isCybersourceP12 = function(filePath, password, logger) { try { logger.debug(`Checking if P12 file is generated by CyberSource: ${filePath}`); - const p12 = parseP12File(filePath, password, logger); + // 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]; @@ -453,6 +455,75 @@ exports.isCybersourceP12 = function(filePath, password, logger) { } }; +/** + * Validates and auto-extracts responseMleKID if necessary + * @param {object} merchantConfig - Merchant configuration object + * @param {object} logger - Logger object for logging messages + * @returns {string} - The validated or auto-extracted responseMleKID + * @throws Will throw an error if responseMleKID is not available and cannot be auto-extracted + */ +exports.validateAndAutoExtractResponseMleKid = function(merchantConfig, logger) { + logger.debug('Validating responseMleKID for JWT token generation'); + + // First, try to auto-extract from CyberSource P12 certificate if applicable + const hasValidFilePath = typeof merchantConfig.getResponseMlePrivateKeyFilePath() === "string" + && merchantConfig.getResponseMlePrivateKeyFilePath().trim() !== ""; + + if (hasValidFilePath) { + const path = require('path'); + const fileExtension = path.extname(merchantConfig.getResponseMlePrivateKeyFilePath()).toLowerCase(); + const isP12File = fileExtension === ".p12" || fileExtension === ".pfx"; + + if (isP12File) { + logger.debug('P12/PFX file detected, checking if it is a CyberSource certificate'); + + const isCybersourceP12 = exports.isCybersourceP12( + merchantConfig.getResponseMlePrivateKeyFilePath(), + merchantConfig.getResponseMlePrivateKeyFilePassword(), + logger + ); + + if (isCybersourceP12) { + logger.debug('Detected CyberSource P12 file, attempting to auto-extract responseMleKID'); + try { + const extractedKid = exports.extractResponseMleKid( + merchantConfig.getResponseMlePrivateKeyFilePath(), + merchantConfig.getResponseMlePrivateKeyFilePassword(), + merchantConfig.getMerchantID(), + logger + ); + + logger.info('Successfully auto-extracted responseMleKID from CyberSource P12 certificate'); + return extractedKid; + } catch (error) { + logger.warn(`Failed to auto-extract responseMleKID from P12 file: ${error.message}. Will check for manually configured value.`); + } + } else { + logger.debug('P12 file is not a CyberSource-generated certificate, skipping auto-extraction'); + } + } else { + logger.debug('Private key file is not a P12/PFX file, skipping auto-extraction'); + } + } else { + logger.debug('No valid private key file path provided, skipping auto-extraction'); + } + + // If auto-extraction didn't work, check if responseMleKID is manually configured + let responseMleKID = merchantConfig.getResponseMleKID(); + if (responseMleKID && typeof responseMleKID === "string" && responseMleKID.trim()) { + logger.debug('Using manually configured responseMleKID'); + return responseMleKID; + } + + logger.error('responseMleKID is required but not available'); + ApiException.ApiException( + "responseMleKID is required when response MLE is enabled. " + + "Could not auto-extract from certificate and no manual configuration provided. " + + "Please provide responseMleKID explicitly in your configuration.", + logger + ); +}; + /** * Extracts the serial number (KID) from a certificate's subject in a P12 file where CN matches the merchantId * @param {string} filePath - Path to the P12 file From 5621770954d44c705f3b5ece5a94b1087343330e Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Mon, 17 Nov 2025 18:37:57 +0530 Subject: [PATCH 25/30] moved all utils function to MLEUtility.js --- src/authentication/jwt/JWTSigToken.js | 5 +- src/authentication/util/MLEUtility.js | 177 +++++++++++++++++++++++++- src/authentication/util/Utility.js | 157 +---------------------- 3 files changed, 179 insertions(+), 160 deletions(-) diff --git a/src/authentication/jwt/JWTSigToken.js b/src/authentication/jwt/JWTSigToken.js index c9437d61..cfb2d7ef 100644 --- a/src/authentication/jwt/JWTSigToken.js +++ b/src/authentication/jwt/JWTSigToken.js @@ -4,8 +4,7 @@ const Jwt = require('jwt-simple'); const Constants = require('../util/Constants'); const KeyCertificate = require('./KeyCertificateGenerator'); const DigestGenerator = require('../payloadDigest/DigestGenerator'); -const ApiException = require('../util/ApiException'); -const Utility = require('../util/Utility'); +const MLEUtility = require('../util/MLEUtility'); // Constants for algorithms const JWT_ALGORITHM = 'RS256'; @@ -53,7 +52,7 @@ exports.getToken = function (merchantConfig, isResponseMLEForApi, logger) { // Add MLE key ID if MLE is enabled if (isResponseMLEForApi === true) { - const responseMleKID = Utility.validateAndAutoExtractResponseMleKid(merchantConfig, logger); + const responseMleKID = MLEUtility.validateAndAutoExtractResponseMleKid(merchantConfig, logger); // Using bracket notation for property name with hyphens claimSetJson["v-c-response-mle-kid"] = responseMleKID; } diff --git a/src/authentication/util/MLEUtility.js b/src/authentication/util/MLEUtility.js index 65da5e82..8cacf1d3 100644 --- a/src/authentication/util/MLEUtility.js +++ b/src/authentication/util/MLEUtility.js @@ -7,6 +7,7 @@ const ApiException= require('./ApiException'); const Constants = require('./Constants'); const Cache = require('./Cache'); const JWEUtility = require('./JWEUtility'); +const Utility = require('./Utility'); exports.checkIsMLEForAPI = function (merchantConfig, inboundMLEStatus, operationId) { //isMLE for an api is false by default @@ -134,7 +135,8 @@ exports.encryptRequestPayload = function(merchantConfig, requestBody) { const customHeaders = { iat: Math.floor(Date.now() / 1000) //epoch time in seconds }; - const serialNumber = getSerialNumberFromCert(cert, merchantConfig, logger); + const warningMsg = `Serial number not found in request MLE certificate for alias ${merchantConfig.getRequestmleKeyAlias()} in ${merchantConfig.getKeyFileName()}.p12, using certificate serial number as fallback`; + const serialNumber = getSerialNumberFromCert(cert, logger, warningMsg); const headers = { alg: "RSA-OAEP-256", enc: "A256GCM", @@ -173,7 +175,7 @@ function toBase64Url(bi) { return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } -function getSerialNumberFromCert(cert, merchantConfig, logger) { +function getSerialNumberFromCert(cert, logger, warningMessage) { if (!cert.subject || !cert.subject.attributes) { throw new Error("Subject or attributes are missing in MLE cert"); } @@ -182,7 +184,176 @@ function getSerialNumberFromCert(cert, merchantConfig, logger) { if (serialNumberAttr) { return serialNumberAttr.value; } else { - logger.warn("Serial number not found in MLE certificate for alias " + merchantConfig.getRequestmleKeyAlias() + " in " + merchantConfig.getKeyFileName() + ".p12"); + if (warningMessage) { + logger.warn(warningMessage); + } return cert.serialNumber; } } + +/** + * Validates and auto-extracts responseMleKID if necessary + * @param {object} merchantConfig - Merchant configuration object + * @param {object} logger - Logger object for logging messages + * @returns {string} - The validated or auto-extracted responseMleKID + * @throws Will throw an error if responseMleKID is not available and cannot be auto-extracted + */ +exports.validateAndAutoExtractResponseMleKid = function(merchantConfig, logger) { + logger.debug('Validating responseMleKID for JWT token generation'); + + // Variable to store auto-extracted KID + let cybsKid = null; + + // First, try to auto-extract from CyberSource P12 certificate if applicable + const hasValidFilePath = typeof merchantConfig.getResponseMlePrivateKeyFilePath() === "string" + && merchantConfig.getResponseMlePrivateKeyFilePath().trim() !== ""; + + if (hasValidFilePath) { + const path = require('path'); + const fileExtension = path.extname(merchantConfig.getResponseMlePrivateKeyFilePath()).toLowerCase(); + const isP12File = fileExtension === ".p12" || fileExtension === ".pfx"; + + if (isP12File) { + logger.debug('P12/PFX file detected, checking if it is a CyberSource certificate'); + + const isCybersourceP12 = Utility.isCybersourceP12( + merchantConfig.getResponseMlePrivateKeyFilePath(), + merchantConfig.getResponseMlePrivateKeyFilePassword(), + logger + ); + + if (isCybersourceP12) { + logger.debug('Detected CyberSource P12 file, attempting to auto-extract responseMleKID'); + try { + cybsKid = exports.extractResponseMleKid( + merchantConfig.getResponseMlePrivateKeyFilePath(), + merchantConfig.getResponseMlePrivateKeyFilePassword(), + merchantConfig.getMerchantID(), + merchantConfig, + logger + ); + + logger.info('Successfully auto-extracted responseMleKID from CyberSource P12 certificate'); + } catch (error) { + logger.warn(`Failed to auto-extract responseMleKID from P12 file: ${error.message}. Will check for manually configured value.`); + } + } else { + logger.debug('P12 file is not a CyberSource-generated certificate, skipping auto-extraction'); + } + } else { + logger.debug('Private key file is not a P12/PFX file, skipping auto-extraction'); + } + } else { + logger.debug('No valid private key file path provided, skipping auto-extraction'); + } + + // Get manually configured responseMleKID + let configuredKid = merchantConfig.getResponseMleKID(); + configuredKid = (configuredKid && typeof configuredKid === "string" && configuredKid.trim()) ? configuredKid.trim() : null; + + // Determine which value to use + if (!cybsKid && !configuredKid) { + logger.error('responseMleKID is required but not available'); + ApiException.ApiException( + "responseMleKID is required when response MLE is enabled. " + + "Could not auto-extract from certificate and no manual configuration provided. " + + "Please provide responseMleKID explicitly in your configuration.", + logger + ); + } + + if (cybsKid && !configuredKid) { + logger.debug('Using auto-extracted responseMleKID from CyberSource P12 certificate'); + return cybsKid; + } + + if (!cybsKid && configuredKid) { + logger.debug('Using manually configured responseMleKID'); + return configuredKid; + } + + // Both exist + if (cybsKid !== configuredKid) { + logger.warn('Auto-extracted responseMleKID does not match manually configured responseMleKID. Using configured value as preference.'); + } else { + logger.debug('Auto-extracted responseMleKID matches manually configured value'); + } + return configuredKid; +}; + +/** + * Extracts the serial number (KID) from a certificate's subject in a P12 file where CN matches the merchantId + * @param {string} filePath - Path to the P12 file + * @param {string} password - Password for the P12 file + * @param {string} merchantId - The merchant ID to match against the CN in the certificate subject + * @param {object} merchantConfig - Merchant configuration object + * @param {object} logger - Logger object for logging messages + * @returns {string} - The serial number extracted from the certificate's subject attributes + * @throws Will throw an error if the certificate with matching CN is not found or serial number is missing + */ +exports.extractResponseMleKid = function(filePath, password, merchantId, merchantConfig, logger) { + try { + logger.debug(`Extracting MLE KID from P12 file: ${filePath} for merchantId: ${merchantId}`); + + const p12 = Utility.parseP12File(filePath, password, logger); + + // Get certificate bags from P12 + const certBags = p12.getBags({ bagType: forge.pki.oids.certBag }); + const certs = certBags[forge.pki.oids.certBag]; + + if (!certs || certs.length === 0) { + logger.error(`No certificates found in P12 file: ${filePath}`); + ApiException.AuthException(`No certificates found in P12 file: ${filePath}`); + } + + logger.debug(`Found ${certs.length} certificate(s) in P12 file`); + + // Iterate through certificates to find one with matching CN + for (let i = 0; i < certs.length; i++) { + const certBag = certs[i]; + const cert = certBag.cert; + + if (!cert || !cert.subject || !cert.subject.attributes) { + logger.debug(`Certificate ${i + 1} has no subject attributes, skipping`); + continue; + } + + // Extract CN from certificate subject + let cn = null; + for (const attr of cert.subject.attributes) { + if (attr.name === 'commonName' || attr.shortName === 'CN') { + cn = attr.value; + break; + } + } + + if (!cn) { + logger.debug(`Certificate ${i + 1} has no CN in subject, skipping`); + continue; + } + + logger.debug(`Certificate ${i + 1} CN: ${cn}`); + + // Check if CN matches merchantId (case-insensitive) + if (cn.toLowerCase() === merchantId.toLowerCase()) { + logger.debug(`Found certificate with matching CN: ${cn}`); + + // Use the shared getSerialNumberFromCert function + const warningMsg = `Serial number not found in response MLE certificate for merchantId ${merchantId}, using certificate serial number as fallback`; + const serialNumber = getSerialNumberFromCert(cert, logger, warningMsg); + + logger.debug(`Serial number (MLE KID) extracted: ${serialNumber}`); + + return serialNumber; + } + } + + // If we get here, no matching certificate was found + logger.error(`No certificate with CN matching merchantId (${merchantId}) and valid serialNumber found in P12 file: ${filePath}`); + ApiException.AuthException(`No certificate with CN matching merchantId (${merchantId}) found in P12 file: ${filePath}`); + + } catch (error) { + logger.error(`Error extracting MLE KID from P12 file: ${filePath}: ${error.message}`); + ApiException.AuthException(`Error extracting MLE KID from P12 file: ${filePath}: ${error.message}`); + } +}; diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index 9f4cf15d..b9a79995 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -173,14 +173,14 @@ exports.ParseMLEConfigString = function (configString, logger) { } /** - * Internal helper: Parses a P12 file and returns the pkcs12 object + * 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 */ -function parseP12File(filePath, password, logger) { +exports.parseP12File = function(filePath, password, logger) { logger.debug(`Parsing P12 file: ${filePath}`); if (!fs.existsSync(filePath)) { @@ -209,7 +209,7 @@ exports.readPrivateKeyFromP12 = function(filePath, password, logger) { try { logger.debug(`Reading private key from P12 file: ${filePath}`); - var p12 = parseP12File(filePath, password, logger); + var p12 = exports.parseP12File(filePath, password, logger); // Extract the private key var keyBags = p12.getBags({ bagType: forge.pki.oids.keyBag }); @@ -454,154 +454,3 @@ exports.isCybersourceP12 = function(filePath, password, logger) { return false; } }; - -/** - * Validates and auto-extracts responseMleKID if necessary - * @param {object} merchantConfig - Merchant configuration object - * @param {object} logger - Logger object for logging messages - * @returns {string} - The validated or auto-extracted responseMleKID - * @throws Will throw an error if responseMleKID is not available and cannot be auto-extracted - */ -exports.validateAndAutoExtractResponseMleKid = function(merchantConfig, logger) { - logger.debug('Validating responseMleKID for JWT token generation'); - - // First, try to auto-extract from CyberSource P12 certificate if applicable - const hasValidFilePath = typeof merchantConfig.getResponseMlePrivateKeyFilePath() === "string" - && merchantConfig.getResponseMlePrivateKeyFilePath().trim() !== ""; - - if (hasValidFilePath) { - const path = require('path'); - const fileExtension = path.extname(merchantConfig.getResponseMlePrivateKeyFilePath()).toLowerCase(); - const isP12File = fileExtension === ".p12" || fileExtension === ".pfx"; - - if (isP12File) { - logger.debug('P12/PFX file detected, checking if it is a CyberSource certificate'); - - const isCybersourceP12 = exports.isCybersourceP12( - merchantConfig.getResponseMlePrivateKeyFilePath(), - merchantConfig.getResponseMlePrivateKeyFilePassword(), - logger - ); - - if (isCybersourceP12) { - logger.debug('Detected CyberSource P12 file, attempting to auto-extract responseMleKID'); - try { - const extractedKid = exports.extractResponseMleKid( - merchantConfig.getResponseMlePrivateKeyFilePath(), - merchantConfig.getResponseMlePrivateKeyFilePassword(), - merchantConfig.getMerchantID(), - logger - ); - - logger.info('Successfully auto-extracted responseMleKID from CyberSource P12 certificate'); - return extractedKid; - } catch (error) { - logger.warn(`Failed to auto-extract responseMleKID from P12 file: ${error.message}. Will check for manually configured value.`); - } - } else { - logger.debug('P12 file is not a CyberSource-generated certificate, skipping auto-extraction'); - } - } else { - logger.debug('Private key file is not a P12/PFX file, skipping auto-extraction'); - } - } else { - logger.debug('No valid private key file path provided, skipping auto-extraction'); - } - - // If auto-extraction didn't work, check if responseMleKID is manually configured - let responseMleKID = merchantConfig.getResponseMleKID(); - if (responseMleKID && typeof responseMleKID === "string" && responseMleKID.trim()) { - logger.debug('Using manually configured responseMleKID'); - return responseMleKID; - } - - logger.error('responseMleKID is required but not available'); - ApiException.ApiException( - "responseMleKID is required when response MLE is enabled. " + - "Could not auto-extract from certificate and no manual configuration provided. " + - "Please provide responseMleKID explicitly in your configuration.", - logger - ); -}; - -/** - * Extracts the serial number (KID) from a certificate's subject in a P12 file where CN matches the merchantId - * @param {string} filePath - Path to the P12 file - * @param {string} password - Password for the P12 file - * @param {string} merchantId - The merchant ID to match against the CN in the certificate subject - * @param {object} logger - Logger object for logging messages - * @returns {string} - The serial number extracted from the certificate's subject attributes - * @throws Will throw an error if the certificate with matching CN is not found or serial number is missing - */ -exports.extractResponseMleKid = function(filePath, password, merchantId, logger) { - try { - logger.debug(`Extracting MLE KID from P12 file: ${filePath} for merchantId: ${merchantId}`); - - const p12 = parseP12File(filePath, password, logger); - - // Get certificate bags from P12 - const certBags = p12.getBags({ bagType: forge.pki.oids.certBag }); - const certs = certBags[forge.pki.oids.certBag]; - - if (!certs || certs.length === 0) { - logger.error(`No certificates found in P12 file: ${filePath}`); - ApiException.AuthException(`No certificates found in P12 file: ${filePath}`); - } - - logger.debug(`Found ${certs.length} certificate(s) in P12 file`); - - // Iterate through certificates to find one with matching CN - for (let i = 0; i < certs.length; i++) { - const certBag = certs[i]; - const cert = certBag.cert; - - if (!cert || !cert.subject || !cert.subject.attributes) { - logger.debug(`Certificate ${i + 1} has no subject attributes, skipping`); - continue; - } - - // Extract CN from certificate subject - let cn = null; - for (const attr of cert.subject.attributes) { - if (attr.name === 'commonName' || attr.shortName === 'CN') { - cn = attr.value; - break; - } - } - - if (!cn) { - logger.debug(`Certificate ${i + 1} has no CN in subject, skipping`); - continue; - } - - logger.debug(`Certificate ${i + 1} CN: ${cn}`); - - // Check if CN matches merchantId (case-insensitive) - if (cn.toLowerCase() === merchantId.toLowerCase()) { - logger.debug(`Found certificate with matching CN: ${cn}`); - - const serialNumberAttr = cert.subject.attributes.find(attr => attr.name === 'serialNumber'); - let serialNumber; - - if (serialNumberAttr) { - serialNumber = serialNumberAttr.value; - } else { - logger.warn(`Serial number not found in certificate subject for merchantId ${merchantId}, using certificate serial number as fallback`); - serialNumber = cert.serialNumber; - } - - logger.debug(`Serial number (MLE KID) extracted: ${serialNumber}`); - - return serialNumber; - } - } - - // If we get here, no matching certificate was found - logger.error(`No certificate with CN matching merchantId (${merchantId}) and valid serialNumber found in P12 file: ${filePath}`); - ApiException.AuthException(`No certificate with CN matching merchantId (${merchantId}) found in P12 file: ${filePath}`); - - } catch (error) { - logger.error(`Error extracting MLE KID from P12 file: ${filePath}: ${error.message}`); - ApiException.AuthException(`Error extracting MLE KID from P12 file: ${filePath}: ${error.message}`); - } -}; From b0fbd0bd96e1867794137a2ee7606d1ebe053bd2 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Tue, 18 Nov 2025 17:19:43 +0530 Subject: [PATCH 26/30] resolved comments --- src/authentication/util/Constants.js | 1 - src/authentication/util/MLEUtility.js | 28 ++++++++-------- src/authentication/util/Utility.js | 46 +++++++++++++-------------- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/authentication/util/Constants.js b/src/authentication/util/Constants.js index 0c3e109c..cdbb7590 100644 --- a/src/authentication/util/Constants.js +++ b/src/authentication/util/Constants.js @@ -29,7 +29,6 @@ module.exports = { CERTIFICATE_EXPIRY_DATE_WARNING_DAYS : 90, FACTOR_DAYS_TO_MILLISECONDS : 24 * 60 * 60 * 1000, DEFAULT_MLE_ALIAS_FOR_CERT : "CyberSource_SJC_US", - CYBERSOURCE_P12_CERT_ALIAS : "CyberSource_SJC_US", MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT : "_mleCertFromMerchantConfig", MLE_CACHE_IDENTIFIER_FOR_P12_CERT : "_mleCertFromP12", diff --git a/src/authentication/util/MLEUtility.js b/src/authentication/util/MLEUtility.js index 8cacf1d3..029ec77c 100644 --- a/src/authentication/util/MLEUtility.js +++ b/src/authentication/util/MLEUtility.js @@ -135,8 +135,8 @@ exports.encryptRequestPayload = function(merchantConfig, requestBody) { const customHeaders = { iat: Math.floor(Date.now() / 1000) //epoch time in seconds }; - const warningMsg = `Serial number not found in request MLE certificate for alias ${merchantConfig.getRequestmleKeyAlias()} in ${merchantConfig.getKeyFileName()}.p12, using certificate serial number as fallback`; - const serialNumber = getSerialNumberFromCert(cert, logger, warningMsg); + const errorMessage = `Serial number not found in request MLE certificate for alias ${merchantConfig.getRequestmleKeyAlias()} in ${merchantConfig.getKeyFileName()}.p12, using certificate serial number as fallback`; + const serialNumber = getSerialNumberFromCert(cert, errorMessage); const headers = { alg: "RSA-OAEP-256", enc: "A256GCM", @@ -175,7 +175,7 @@ function toBase64Url(bi) { return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } -function getSerialNumberFromCert(cert, logger, warningMessage) { +function getSerialNumberFromCert(cert, errorMessage) { if (!cert.subject || !cert.subject.attributes) { throw new Error("Subject or attributes are missing in MLE cert"); } @@ -183,12 +183,8 @@ function getSerialNumberFromCert(cert, logger, warningMessage) { const serialNumberAttr = cert.subject.attributes.find(attr => attr.name === 'serialNumber'); if (serialNumberAttr) { return serialNumberAttr.value; - } else { - if (warningMessage) { - logger.warn(warningMessage); - } - return cert.serialNumber; } + throw new Error(errorMessage || "Serial number attribute not found in cert subject"); } /** @@ -229,7 +225,6 @@ exports.validateAndAutoExtractResponseMleKid = function(merchantConfig, logger) merchantConfig.getResponseMlePrivateKeyFilePath(), merchantConfig.getResponseMlePrivateKeyFilePassword(), merchantConfig.getMerchantID(), - merchantConfig, logger ); @@ -286,16 +281,15 @@ exports.validateAndAutoExtractResponseMleKid = function(merchantConfig, logger) * @param {string} filePath - Path to the P12 file * @param {string} password - Password for the P12 file * @param {string} merchantId - The merchant ID to match against the CN in the certificate subject - * @param {object} merchantConfig - Merchant configuration object * @param {object} logger - Logger object for logging messages * @returns {string} - The serial number extracted from the certificate's subject attributes * @throws Will throw an error if the certificate with matching CN is not found or serial number is missing */ -exports.extractResponseMleKid = function(filePath, password, merchantId, merchantConfig, logger) { +exports.extractResponseMleKid = function(filePath, password, merchantId, logger) { try { logger.debug(`Extracting MLE KID from P12 file: ${filePath} for merchantId: ${merchantId}`); - const p12 = Utility.parseP12File(filePath, password, logger); + const p12 = Cache.fetchCachedP12FromFile(filePath, password, logger); // Get certificate bags from P12 const certBags = p12.getBags({ bagType: forge.pki.oids.certBag }); @@ -339,8 +333,14 @@ exports.extractResponseMleKid = function(filePath, password, merchantId, merchan logger.debug(`Found certificate with matching CN: ${cn}`); // Use the shared getSerialNumberFromCert function - const warningMsg = `Serial number not found in response MLE certificate for merchantId ${merchantId}, using certificate serial number as fallback`; - const serialNumber = getSerialNumberFromCert(cert, logger, warningMsg); + let serialNumber = null; + try { + serialNumber = getSerialNumberFromCert(cert, logger); + } catch (error) { + logger.warn(`Failed to extract serial number from certificate subject: ${error.message}` + `Using certificate serial number as fallback.`); + serialNumber = cert.serialNumber; + + } logger.debug(`Serial number (MLE KID) extracted: ${serialNumber}`); diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js index b9a79995..c2eeb97e 100644 --- a/src/authentication/util/Utility.js +++ b/src/authentication/util/Utility.js @@ -409,8 +409,8 @@ exports.isCybersourceP12 = function(filePath, password, logger) { if (cnAttr) { logger.debug(`Found certificate with CN: ${cnAttr.value}`); - if (cnAttr.value === Constants.CYBERSOURCE_P12_CERT_ALIAS) { - logger.debug(`Found CyberSource certificate (CN=${Constants.CYBERSOURCE_P12_CERT_ALIAS})`); + if (cnAttr.value === Constants.DEFAULT_MLE_ALIAS_FOR_CERT) { + logger.debug(`Found CyberSource certificate (CN=${Constants.DEFAULT_MLE_ALIAS_FOR_CERT})`); return true; } } @@ -418,35 +418,35 @@ exports.isCybersourceP12 = function(filePath, password, logger) { }); if (!hasCybersourceCert) { - logger.debug(`P12 file does not contain CyberSource certificate (CN=${Constants.CYBERSOURCE_P12_CERT_ALIAS})`); + 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' } - ]; + // 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)`); - } - } + // 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}`); + // 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; - } + // // 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 and exactly one private key'); + logger.debug('P12 file is generated by CyberSource: contains CyberSource certificate'); return true; } catch (error) { From abbc3d72df5d2ba65337aa14456e229f5e7a1a76 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Tue, 18 Nov 2025 18:05:38 +0530 Subject: [PATCH 27/30] using the utilty parse p12 function in cache --- src/authentication/util/Cache.js | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/authentication/util/Cache.js b/src/authentication/util/Cache.js index cbd1dc5f..acf7feeb 100644 --- a/src/authentication/util/Cache.js +++ b/src/authentication/util/Cache.js @@ -9,14 +9,6 @@ var ApiException = require('./ApiException'); var Logger = require('../logging/Logger'); var Utility = require('./Utility'); -function loadP12FileToAsn1(filePath) { - var p12Buffer = fs.readFileSync(filePath); - var p12Der = forge.util.binary.raw.encode(new Uint8Array(p12Buffer)); - var p12Asn1 = forge.asn1.fromDer(p12Der); - return p12Asn1; -} - - /** * This module is doing Caching. * Certificate will be available in the memory cache if it has initialized once. @@ -55,8 +47,7 @@ exports.fetchCachedCertificate = function (merchantConfig, logger) { //Function to read the file and put values to new cache function getCertificate(keyPass, filePath, fileLastModifiedTime, logger) { try { - var p12Asn1 = loadP12FileToAsn1(filePath); - var certificate = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, keyPass); + var certificate = Utility.parseP12File(filePath, keyPass, logger); cache.put("certificateFromP12File", certificate); cache.put("certificateLastModifideTimeStamp", fileLastModifiedTime); return certificate; @@ -137,9 +128,8 @@ function setupMLECache(merchantConfig, cacheKey, certificateSourcePath) { function loadCertificateFromP12(merchantConfig, certificatePath) { const logger = Logger.getLogger(merchantConfig, 'Cache'); try { - // Read the P12 file and convert to ASN1 - var p12Asn1 = loadP12FileToAsn1(certificatePath); - var p12Cert = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, merchantConfig.getKeyPass()); + // Read and parse the P12 file + var p12Cert = Utility.parseP12File(certificatePath, merchantConfig.getKeyPass(), logger); // Extract the certificate from the P12 container var certBags = p12Cert.getBags({ bagType: forge.pki.oids.certBag }); @@ -307,8 +297,7 @@ exports.fetchCachedP12FromFile = function(filePath, password, logger, cacheKey) logger.debug(`P12/PFX not in cache or file modified. Loading from file: ${filePath}`); try { - const p12Asn1 = loadP12FileToAsn1(filePath); - const p12Object = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, password); + const p12Object = Utility.parseP12File(filePath, password, logger); // Store in cache with file modification time cache.put(finalCacheKey, { From a8bef85cd3e3b6486a3c782e7a6489997f6d06cc Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Tue, 18 Nov 2025 20:06:16 +0530 Subject: [PATCH 28/30] added validation in merchent config also --- src/authentication/core/MerchantConfig.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index d5a85250..994f6a96 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -917,7 +917,8 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { ); } - // Validate file path accessibility if provided + // Validate file path accessibility if provided and determine if P12/PFX + let isP12File = false; if (hasValidFilePath) { try { fs.accessSync(this.responseMlePrivateKeyFilePath, fs.constants.R_OK); @@ -928,6 +929,8 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { logger ); } + // Check if it's a P12/PFX file for KID auto-extraction + isP12File = ext === '.p12' || ext === '.pfx'; } catch (err) { const errorType = err.code === 'ENOENT' ? 'does not exist' : 'is not readable'; throw new ApiException.ApiException( @@ -936,6 +939,17 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { ); } } + + // Validate KID - only required for non-P12/PFX files + // P12/PFX files support auto-extraction of KID in MLEUtility.js + if (!isP12File) { + if (typeof this.responseMleKID !== "string" || !this.responseMleKID?.trim()) { + throw new ApiException.ApiException( + "responseMleKID is required when response MLE is enabled for non-P12/PFX files.", + logger + ); + } + } } /** * This method is to log all merchantConfic properties From 0394bbcf6e353e17fec17190593bfba9fdac0fc6 Mon Sep 17 00:00:00 2001 From: mahmishr Date: Thu, 20 Nov 2025 14:29:03 +0530 Subject: [PATCH 29/30] Update MerchantConfig.js --- src/authentication/core/MerchantConfig.js | 48 ++++++----------------- 1 file changed, 11 insertions(+), 37 deletions(-) diff --git a/src/authentication/core/MerchantConfig.js b/src/authentication/core/MerchantConfig.js index d12d28fe..1d0946be 100644 --- a/src/authentication/core/MerchantConfig.js +++ b/src/authentication/core/MerchantConfig.js @@ -83,16 +83,20 @@ function MerchantConfig(result) { //MLE Params for Request Body /** - * Deprecated flag to enable MLE for request. This flag is now known as "enableRequestMLEForOptionalApisGlobally" - */ - this.useMLEGlobally = result.useMLEGlobally; - - /** * Flag to enable MLE (Message Level Encryption) for request body to all APIs in SDK which have optional support for MLE. * This means the API can send both non-encrypted and encrypted requests. * Older flag "useMLEGlobally" is deprecated and will be used as alias/another name for enableRequestMLEForOptionalApisGlobally. */ - this.enableRequestMLEForOptionalApisGlobally = result.enableRequestMLEForOptionalApisGlobally !== undefined ? result.enableRequestMLEForOptionalApisGlobally : this.useMLEGlobally; + this.enableRequestMLEForOptionalApisGlobally = result.enableRequestMLEForOptionalApisGlobally !== undefined && typeof result.enableRequestMLEForOptionalApisGlobally === "boolean" ? result.enableRequestMLEForOptionalApisGlobally : + (result.useMLEGlobally !== undefined && typeof result.useMLEGlobally === "boolean" ? result.useMLEGlobally : false); + + // Validate that both flags are not set with different values + if (result.enableRequestMLEForOptionalApisGlobally !== undefined && typeof result.enableRequestMLEForOptionalApisGlobally === "boolean" && + result.useMLEGlobally !== undefined && typeof result.useMLEGlobally === "boolean" && + result.enableRequestMLEForOptionalApisGlobally !== result.useMLEGlobally) { + var logger = Logger.getLogger(this, 'MerchantConfig'); + ApiException.ApiException("enableRequestMLEForOptionalApisGlobally and useMLEGlobally must have the same value if both are provided.", logger); + } /** * Flag to disable MLE (Message Level Encryption) for request body to APIs in SDK which have mandatory MLE requirement when sending calls. @@ -466,34 +470,12 @@ MerchantConfig.prototype.setpemFileDirectory = function getpemFileDirectory(pemF this.pemFileDirectory = pemFileDirectory; } -MerchantConfig.prototype.getUseMLEGlobally = function getUseMLEGlobally() { - return this.useMLEGlobally; -} - -MerchantConfig.prototype.setUseMLEGlobally = function setUseMLEGlobally(useMLEGlobally) { - this.useMLEGlobally = useMLEGlobally; - // If enableRequestMLEForOptionalApisGlobally is not set, set it to useMLEGlobally - if (this.enableRequestMLEForOptionalApisGlobally === undefined) { - this.enableRequestMLEForOptionalApisGlobally = useMLEGlobally; - } - // If it is set but has a different value, throw an exception - else if (this.enableRequestMLEForOptionalApisGlobally !== useMLEGlobally) { - var logger = Logger.getLogger(this, 'MerchantConfig'); - ApiException.ApiException("enableRequestMLEForOptionalApisGlobally and useMLEGlobally must have the same value if both are provided.", logger); - } -} - MerchantConfig.prototype.getEnableRequestMLEForOptionalApisGlobally = function getEnableRequestMLEForOptionalApisGlobally() { return this.enableRequestMLEForOptionalApisGlobally; } MerchantConfig.prototype.setEnableRequestMLEForOptionalApisGlobally = function setEnableRequestMLEForOptionalApisGlobally(enableRequestMLEForOptionalApisGlobally) { this.enableRequestMLEForOptionalApisGlobally = enableRequestMLEForOptionalApisGlobally; - // If it is set but has a different value, throw an exception - if (this.useMLEGlobally !== undefined && (this.useMLEGlobally !== enableRequestMLEForOptionalApisGlobally)) { - var logger = Logger.getLogger(this, 'MerchantConfig'); - ApiException.ApiException("enableRequestMLEForOptionalApisGlobally and useMLEGlobally must have the same value if both are provided.", logger); - } } MerchantConfig.prototype.getDisableRequestMLEForMandatoryApisGlobally = function getDisableRequestMLEForMandatoryApisGlobally() { @@ -816,14 +798,6 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { this.requestmleKeyAlias = Constants.DEFAULT_MLE_ALIAS_FOR_CERT; } - if ( - this.enableRequestMLEForOptionalApisGlobally !== undefined && - this.useMLEGlobally !== undefined && - this.enableRequestMLEForOptionalApisGlobally !== this.useMLEGlobally - ) { - ApiException.ApiException("enableRequestMLEForOptionalApisGlobally and useMLEGlobally must have the same value if both are provided.", logger); - } - // Validate maxIdleSockets is non-negative and not less than default if (this.maxIdleSockets !== null && this.maxIdleSockets !== undefined && (this.maxIdleSockets <= 0 || this.maxIdleSockets < Constants.DEFAULT_MAX_IDLE_SOCKETS)) { this.maxIdleSockets = Constants.DEFAULT_MAX_IDLE_SOCKETS; @@ -836,7 +810,7 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() { logger.warn("userDefinedTimeout cannot be non-negative or less than default (value should be in milliseconds). Setting to default value " + Constants.DEFAULT_USER_DEFINED_TIMEOUT + "."); } - //useMLEGlobally check for auth Type + //enableRequestMLEForOptionalApisGlobally check for auth Type if (this.enableRequestMLEForOptionalApisGlobally === true || this.internalMapToControlRequestMLEonAPI != null) { if (this.enableRequestMLEForOptionalApisGlobally === true && this.authenticationType.toLowerCase() !== Constants.JWT) { ApiException.ApiException("Request MLE is only supported in JWT auth type", logger); From 1d06b0091f48aac6544e960dc713c7d315393e89 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Fri, 21 Nov 2025 18:44:13 +0530 Subject: [PATCH 30/30] throwing err in case of serial number not found --- src/authentication/util/MLEUtility.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/authentication/util/MLEUtility.js b/src/authentication/util/MLEUtility.js index 029ec77c..2045f36c 100644 --- a/src/authentication/util/MLEUtility.js +++ b/src/authentication/util/MLEUtility.js @@ -333,14 +333,7 @@ exports.extractResponseMleKid = function(filePath, password, merchantId, logger) logger.debug(`Found certificate with matching CN: ${cn}`); // Use the shared getSerialNumberFromCert function - let serialNumber = null; - try { - serialNumber = getSerialNumberFromCert(cert, logger); - } catch (error) { - logger.warn(`Failed to extract serial number from certificate subject: ${error.message}` + `Using certificate serial number as fallback.`); - serialNumber = cert.serialNumber; - - } + let serialNumber = getSerialNumberFromCert(cert, logger); logger.debug(`Serial number (MLE KID) extracted: ${serialNumber}`);