diff --git a/MLE.md b/MLE.md
index 27616f4d..fcc3f6c3 100644
--- a/MLE.md
+++ b/MLE.md
@@ -4,95 +4,449 @@
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 `mleKeyAlias`, which specifies the key alias used to retrieve the MLE certificate from the JWT P12 file.
+---
-- **Variable**: `mleKeyAlias`
-- **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**: 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).
+
+---
+
+##### (ii) Option 2: Provide Private Key File Path
+
+- **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 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" }`
+
+#### Structure of Values in Object:
+
+(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
+ - `"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) **"requestMLE"** - 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 string values
+ mapToControlMLEonAPI: {
+ "createPayment": "true", // Enable request MLE for this API (simple format)
+ "capturePayment": "false::" // Disable request MLE for this API (full format)
+ }
+};
+```
+
+### (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 or JWK object)
+var privateKey = loadPrivateKeyFromSomewhere();
+
+// Create merchantConfig with private key object
+var merchantConfig = {
+ enableResponseMleGlobally: true,
+ responseMlePrivateKey: privateKey, // Supports PEM format or JWK object
+ 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
+};
+```
+
+
-## 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.
+## 5. JSON Configuration Examples
-## Example Configuration
+### (i) Minimal Request MLE
```json
{
"merchantConfig": {
- "useMLEGlobally": true //globally MLE will be enabled for all MLE supported APIs
+ "enableRequestMLEForOptionalApisGlobally": true
}
}
```
-Or
+
+### (ii) Request MLE with Deprecated Parameters
```json
{
"merchantConfig": {
- "useMLEGlobally": true, //globally MLE will be enabled for all MLE supported APIs
+ "useMLEGlobally": true,
+ "mleKeyAlias": "Custom_Key_Alias"
+ }
+}
+```
+
+### (iii) Request MLE with Custom Configuration
+
+```json
+{
+ "merchantConfig": {
+ "enableRequestMLEForOptionalApisGlobally": true,
+ "mleForRequestPublicCertPath": "/path/to/public/cert.pem",
+ "requestMleKeyAlias": "Custom_Key_Alias",
"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
- },
- "mleKeyAlias": "Custom_Key_Alias" //optional if any custom value provided by Cybs
+ "createPayment": "true",
+ "capturePayment": "false"
+ }
}
}
```
-Or
+
+### (iv) Response MLE Only
```json
{
"merchantConfig": {
- "useMLEGlobally": false, //globally MLE will be disabled for all APIs
+ "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
- },
- "mleKeyAlias": "Custom_Key_Alias" //optional if any custom value provided by Cybs
+ "createPayment": "::true"
+ }
}
}
```
-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.
+### (v) Both Request and Response MLE
-Please refer given link for sample codes with MLE:
-https://github.com/CyberSource/cybersource-rest-samples-node/tree/master/Samples/MLEFeature
+```json
+{
+ "merchantConfig": {
+ "enableRequestMLEForOptionalApisGlobally": true,
+ "enableResponseMleGlobally": true,
+ "responseMlePrivateKeyFilePath": "/path/to/private/key.p12",
+ "responseMlePrivateKeyFilePassword": "password",
+ "responseMleKID": "your-key-id",
+ "mapToControlMLEonAPI": {
+ "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 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
+
+### (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 using string format
+- Invalid formats (empty values, multiple separators) will cause configuration errors
+- Empty string after `::` separator will use global defaults
+- **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 string format values in `mapToControlMLEonAPI` will cause parsing errors
+- **Note**: Boolean values in `mapToControlMLEonAPI` are deprecated but still supported for backward compatibility
+
+
+
+## 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.
diff --git a/generator/cybersource-javascript-template/ApiClient.mustache b/generator/cybersource-javascript-template/ApiClient.mustache
index b74d5c66..1ecc902e 100644
--- a/generator/cybersource-javascript-template/ApiClient.mustache
+++ b/generator/cybersource-javascript-template/ApiClient.mustache
@@ -7,18 +7,18 @@ const agentPool = new Map();
(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', 'agentkeepalive', 'crypto', 'Authentication/MerchantConfig', 'Authentication/Logger', 'Authentication/Constants', 'Authentication/Authorization', 'Authentication/PayloadDigest'], factory);
+ define(['axios', 'axios-cookiejar-support', 'https-proxy-agent', 'https', 'querystring', 'agentkeepalive', 'crypto', '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('agentkeepalive'), require('crypto'), 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('agentkeepalive'), require('crypto'), 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.AgentKeepAlive, root.crypto, 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.AgentKeepAlive, root.crypto, 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, AgentKeepAlive, crypto, MerchantConfig, Logger, Constants, Authorization, PayloadDigest) {
+}(this, function(axios, axiosCookieJar, HttpsProxyAgent, https, querystring, AgentKeepAlive, crypto, MerchantConfig, Logger, Constants, Authorization, PayloadDigest, MLEUtility) {
{{#emitJSDoc}} /**
* @module {{#invokerPackage}}{{invokerPackage}}/{{/invokerPackage}}ApiClient
* @version {{projectVersion}}
@@ -573,8 +573,9 @@ const agentPool = new Map();
* @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)
@@ -583,7 +584,7 @@ const agentPool = new Map();
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();
@@ -658,13 +659,14 @@ const agentPool = new Map();
* @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);
@@ -741,7 +743,7 @@ const agentPool = new Map();
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()) {
@@ -841,14 +843,27 @@ const agentPool = new Map();
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 = {};
@@ -867,12 +882,26 @@ const agentPool = new Map();
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
);
}
}
diff --git a/package.json b/package.json
index 869820df..1883fc23 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,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/ApiClient.js b/src/ApiClient.js
index c4602b5c..98f3d860 100644
--- a/src/ApiClient.js
+++ b/src/ApiClient.js
@@ -21,18 +21,18 @@ const agentPool = new Map();
(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', 'agentkeepalive', 'crypto', 'Authentication/MerchantConfig', 'Authentication/Logger', 'Authentication/Constants', 'Authentication/Authorization', 'Authentication/PayloadDigest'], factory);
+ define(['axios', 'axios-cookiejar-support', 'https-proxy-agent', 'https', 'querystring', 'agentkeepalive', 'crypto', '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('agentkeepalive'), require('crypto'), 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('agentkeepalive'), require('crypto'), 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.AgentKeepAlive, root.crypto, 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.AgentKeepAlive, root.crypto, 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, AgentKeepAlive, crypto, MerchantConfig, Logger, Constants, Authorization, PayloadDigest) {
+}(this, function(axios, axiosCookieJar, HttpsProxyAgent, https, querystring, AgentKeepAlive, crypto, MerchantConfig, Logger, Constants, Authorization, PayloadDigest, MLEUtility) {
/**
* @module ApiClient
* @version 0.0.1
@@ -577,8 +577,9 @@ const agentPool = new Map();
* @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)
@@ -587,7 +588,7 @@ const agentPool = new Map();
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();
@@ -662,13 +663,14 @@ const agentPool = new Map();
* @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);
@@ -745,7 +747,7 @@ const agentPool = new Map();
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()) {
@@ -846,12 +848,38 @@ const agentPool = new Map();
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 = {};
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 c9c5254e..d12d28fe 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,20 +81,84 @@ 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 = 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.
+ */
this.mleForRequestPublicCertPath = result.mleForRequestPublicCertPath;
this.maxIdleSockets = result.maxIdleSockets; // Value should be non-negative
this.freeSocketTimeout = result.freeSocketTimeout; // Value should be non-negative and greater than or equal to 4000
+ /**
+ * 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.setResponseMlePrivateKey(result.responseMlePrivateKey);
+
+
+ this.mapToControlMLEonAPI = result.mapToControlMLEonAPI;
/* Fallback logic*/
- this.defaultPropValues();
+ if (this.mapToControlMLEonAPI != null) {
+ validateAndSetMapToControlMLEonAPI.call(this, this.mapToControlMLEonAPI);
+ }
+ this.defaultPropValues();
}
MerchantConfig.prototype.getAuthenticationType = function getAuthenticationType() {
@@ -447,12 +512,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() {
@@ -463,6 +528,74 @@ 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) {
+ var logger = Logger.getLogger(this, 'MerchantConfig');
+
+ if (responseMlePrivateKey) {
+ logger.debug('Processing response MLE private key');
+
+ try {
+ const pemKey = Utility.parseAndReturnPem(
+ responseMlePrivateKey,
+ logger,
+ this.responseMlePrivateKeyFilePassword,
+ 'responseMlePrivateKeyFilePassword'
+ );
+ 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() {
+ 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'));
}
@@ -502,6 +635,8 @@ MerchantConfig.prototype.runEnvironmentCheck = function runEnvironmentCheck(logg
}
}
+
+
//This method is for fallback
MerchantConfig.prototype.defaultPropValues = function defaultPropValues() {
@@ -677,8 +812,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 (
@@ -702,18 +837,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.mapToControlMLEonAPI != null && Object.keys(this.mapToControlMLEonAPI).length !== 0) {
+ if (this.internalMapToControlRequestMLEonAPI != null && Object.keys(this.internalMapToControlRequestMLEonAPI).length !== 0) {
var hasTrueValue = false;
- for (const[key, value] of Object.entries(this.mapToControlMLEonAPI)) {
+ for (const[_, value] of Object.entries(this.internalMapToControlRequestMLEonAPI)) {
if (value === true) {
hasTrueValue = true;
break;
@@ -746,6 +881,67 @@ MerchantConfig.prototype.defaultPropValues = function defaultPropValues() {
}
}
+
+ 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 (isResponseMleConfigured) {
+ 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 = typeof this.responseMlePrivateKeyFilePath === "string" && 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
@@ -773,4 +969,52 @@ 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, 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) {
+ this.internalMapToControlRequestMLEonAPI.set(apiFunctionName, config.requestMLE);
+ }
+ if (config.responseMLE !== undefined) {
+ this.internalMapToControlResponseMLEonAPI.set(apiFunctionName, config.responseMLE);
+ }
+ }
+}
+
+
+
module.exports = MerchantConfig;
diff --git a/src/authentication/jwt/JWTSigToken.js b/src/authentication/jwt/JWTSigToken.js
index dad1ad2b..15aaa388 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.getResponseMleKID();
}
- 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..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
@@ -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 50ccb1cc..9a149d7d 100644
--- a/src/authentication/util/Constants.js
+++ b/src/authentication/util/Constants.js
@@ -99,6 +99,7 @@ module.exports = {
DEFAULT_LOGGING_LEVEL : "error",
DEFAULT_MAX_IDLE_SOCKETS : 100,
DEFAULT_USER_DEFINED_TIMEOUT : 4000, // Value in milliseconds
+ 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..2dbdb8f9 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,19 +28,90 @@ 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) {
- isMLEForAPI = true;
- }
-
- if (merchantConfig.mapToControlMLEonAPI[operationId] === false) {
- isMLEForAPI = false;
- }
+ const mleControlMap = merchantConfig.getInternalMapToControlRequestMLEonAPI();
+ if (mleControlMap && mleControlMap.has(operationId)) {
+ isMLEForAPI = mleControlMap.get(operationId);
}
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 (responseMLEMap.has(trimmedId)) {
+ isResponseMLEForAPI = responseMLEMap.get(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');
+ 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);
+
+ 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('LOG_NETWORK_RESPONSE_AFTER_MLE_DECRYPTION: ' + JSON.stringify(decryptedData));
+ return JSON.parse(decryptedData);
+ })
+ .catch(error => {
+ 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));
+ });
+ } 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);
@@ -54,9 +126,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 = {
@@ -110,7 +182,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;
}
}
diff --git a/src/authentication/util/Utility.js b/src/authentication/util/Utility.js
index a813bfd3..92712a42 100644
--- a/src/authentication/util/Utility.js
+++ b/src/authentication/util/Utility.js
@@ -1,7 +1,10 @@
'use strict'
var ApiException = require('./ApiException');
-var Constants = require('./Constants')
+var Constants = require('./Constants');
+var fs = require('fs');
+var forge = require('node-forge');
+var jwkToPem = require('jwk-to-pem');
exports.getResponseCodeMessage = function (responseCode) {
@@ -115,3 +118,242 @@ 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?.trim()) {
+ ApiException.ApiException("Unsupported empty. Expected format: 'requestMLE::responseMLE' or 'requestMLE' as true/false.", logger);
+ } else if (configString.indexOf('::') != -1) {
+ const parts = configString.split('::');
+ if (parts.length !== 2) {
+ ApiException.ApiException("Invalid MLE control map value format. Expected format: true/false for 'requestMLE::responseMLE' but got: '" + configString + "'", logger);
+ }
+ const requestMLEPart = parts[0].trim();
+ const responseMLEPart = parts[1].trim();
+
+ if (requestMLEPart !== "" && ((requestMLEPart !== 'true' && requestMLEPart !== 'false'))) {
+ ApiException.ApiException("Invalid MLE control map value format. Expected format: true/false for 'requestMLE::responseMLE' but got: '" + configString + "'", logger);
+ }
+ if (responseMLEPart !== "" && ((responseMLEPart !== 'true' && responseMLEPart !== 'false'))) {
+ ApiException.ApiException("Invalid MLE control map value format. Expected format: true/false for 'requestMLE::responseMLE' but got: '" + configString + "'", logger);
+ }
+
+
+ // Create the result object
+ const result = {};
+
+ // Only set requestMLE if requestMLEPart is not empty
+ if (requestMLEPart !== "") {
+ result.requestMLE = (requestMLEPart === 'true');
+ }
+
+ // Only set responseMLE if responseMLEPart is not empty
+ if (responseMLEPart !== "") {
+ result.responseMLE = (responseMLEPart === 'true');
+ }
+
+ return result;
+
+ } else {
+ if (configString === 'true' || configString === 'false') {
+ const result = {
+ requestMLE: configString === 'true'
+ };
+ return result;
+ } else {
+ ApiException.ApiException("Invalid MLE control map value format: '" + configString + "'. Expected format: true/false for 'requestMLE' but got: '" + configString + "'", logger);
+ }
+ }
+}
+
+/**
+ * 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: ${filePath}: ${error.message}`);
+ ApiException.AuthException(`Error reading private key from P12 file: ${filePath}: ${error.message}. ${Constants.INCORRECT_KEY_PASS}`);
+ }
+};
+
+/**
+ * Loads a private key from a PEM file
+ * @param {string} filePath - Path to the PEM file
+ * @param {string} password - Password for the encrypted PEM file (optional)
+ * @param {object} logger - Logger object for logging messages
+ * @returns {string} - Private key in PEM format
+ */
+exports.readPrivateKeyFromPemFile = function(filePath, password, logger) {
+ try {
+ logger.debug(`Reading private key from PEM file: ${filePath}`);
+
+ if (!fs.existsSync(filePath)) {
+ logger.error(`File not found: ${filePath}`);
+ ApiException.AuthException(Constants.FILE_NOT_FOUND + filePath);
+ }
+
+ // Read the PEM file
+ var pemData = fs.readFileSync(filePath, 'utf8');
+
+ logger.debug(`Successfully read PEM file`);
+
+ // Check if the private key is encrypted
+ var isEncrypted = pemData.includes('ENCRYPTED');
+
+ logger.debug(`PEM file contains ${isEncrypted ? 'an encrypted' : 'an unencrypted'} private key`);
+
+ if (isEncrypted && (!password || password.trim() === '')) {
+ logger.error(`Password is required for encrypted private key: ${filePath}`);
+ ApiException.AuthException(`Password is required for encrypted private key: ${filePath}`);
+ }
+
+ try {
+ var privateKey;
+ if (isEncrypted) {
+ logger.debug(`Decrypting private key using provided password`);
+ // Decrypt the private key using the provided password
+ privateKey = forge.pki.decryptRsaPrivateKey(pemData, password);
+ } else {
+ logger.debug(`Parsing unencrypted private key`);
+ // Parse the unencrypted private key
+ privateKey = forge.pki.privateKeyFromPem(pemData);
+ }
+
+ if (!privateKey) {
+ logger.error(`Failed to parse private key from PEM file: ${filePath}`);
+ ApiException.AuthException(`Failed to parse private key from PEM file: ${filePath}`);
+ }
+
+ logger.debug(`Successfully extracted private key from PEM file`);
+
+ return forge.pki.privateKeyToPem(privateKey);
+ } catch (error) {
+ if (isEncrypted) {
+ logger.error(`Error decrypting private key from ${filePath}: ${error.message}. This may be due to an incorrect password.`);
+ ApiException.AuthException(`Error decrypting private key from ${filePath}: ${error.message}. ${Constants.INCORRECT_KEY_PASS}`);
+ } else {
+ logger.error(`Error parsing private key from ${filePath}: ${error.message}`);
+ ApiException.AuthException(`Error parsing private key from ${filePath}: ${error.message}`);
+ }
+ }
+ } catch (error) {
+ logger.error(`Error loading private key from PEM file: ${filePath}: ${error.message}`);
+ ApiException.AuthException(`Error loading private key from PEM file: ${filePath}: ${error.message}`);
+ }
+};
+
+exports.parseAndReturnPem = function(key, logger, password, passwordPropertyName) {
+ logger.debug(`Parsing private key to PEM format synchronously, key type: ${typeof key}`);
+
+ if (typeof key === 'string') {
+ logger.debug('Processing string key as potential PEM private key');
+
+ // Check if the key is encrypted
+ const isEncrypted = key.includes('ENCRYPTED');
+
+ if (isEncrypted) {
+ logger.debug('Detected encrypted private key');
+
+ // Check if password is provided for encrypted key
+ if (!password || password.trim() === '') {
+ const propertyHint = passwordPropertyName ? ` Please set the '${passwordPropertyName}' property in your configuration.` : '';
+ const errorMessage = `Password is required for encrypted private key.${propertyHint}`;
+ logger.error(errorMessage);
+ throw new Error(errorMessage);
+ }
+
+ try {
+ // Decrypt the private key using the provided password
+ logger.debug('Attempting to decrypt private key with provided password');
+ const privateKey = forge.pki.decryptRsaPrivateKey(key, password);
+
+ if (!privateKey) {
+ logger.error('Failed to decrypt private key. Incorrect password or invalid key format.');
+ throw new Error('Failed to decrypt private key. Incorrect password or invalid key format.');
+ }
+
+ // Convert the decrypted key back to PEM format
+ const pemKey = forge.pki.privateKeyToPem(privateKey);
+ logger.debug('Successfully decrypted and converted private key to PEM format');
+ return pemKey;
+ } catch (error) {
+ logger.error(`Error decrypting private key: ${error.message}`);
+ throw new Error(`Error decrypting private key: ${error.message}`);
+ }
+ } else {
+ // Not encrypted, proceed with normal validation
+ try {
+ // Validate it's a valid private key PEM
+ forge.pki.privateKeyFromPem(key);
+ logger.debug('Successfully validated private key PEM format');
+ return key;
+ } catch (error) {
+ logger.error(`Invalid private key PEM format: ${error.message}`);
+ throw new Error('Invalid private key PEM format');
+ }
+ }
+ } else if (typeof key === 'object' && key !== null) {
+ logger.debug('Processing object key as potential JWK private key');
+ try {
+ // Check if it has the 'd' property which indicates a private key
+ if (!key.d) {
+ logger.error('JWK object is not a private key (missing d parameter)');
+ throw new Error('JWK object is not a private key');
+ }
+
+ // Convert JWK to PEM (private key)
+ logger.debug('Converting JWK to private key PEM');
+ const pem = jwkToPem(key, { private: true });
+ logger.debug('Successfully converted JWK to private key PEM format');
+ return pem;
+ } catch (error) {
+ logger.error(`Invalid JWK private key object: ${error.message}`);
+ throw new Error('Invalid JWK private key object');
+ }
+ } else {
+ logger.error(`Unsupported key format: ${typeof key}`);
+ throw new Error('Unsupported key format');
+ }
+}