From 906d6ab11f9a281705a21a16c8e75fe248679897 Mon Sep 17 00:00:00 2001 From: Neeraj Joseph Koilparambil Date: Tue, 14 Nov 2023 11:03:46 +0000 Subject: [PATCH] fix(jwe): honour "pathToEncryptedData" property in jwe encryption "pathToEncryptedData" property was not being honoured in jwe encryption in some cases. This commit fixes this. --- package-lock.json | 31 ++++++++++++++++++++++---- package.json | 3 ++- src/jwe.js | 55 +++++++++++++---------------------------------- test/jwe.test.js | 42 ++++++++++++++++++++++++++---------- 4 files changed, 75 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ec3353..66e54b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "js-sha256": "^0.10.1", - "mastercard-client-encryption": "^1.9.0", + "mastercard-client-encryption": "^1.10.0", "node-jose": "^2.2.0" }, "devDependencies": { @@ -33,6 +33,29 @@ "uglify-js": "^3.17.4" } }, + "../client-encryption-nodejs": { + "name": "mastercard-client-encryption", + "version": "1.10.0", + "extraneous": true, + "license": "MIT", + "dependencies": { + "node-forge": "^1.3.0" + }, + "devDependencies": { + "eslint": "^7.7.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-mocha": "^8.0.0", + "mocha": "^8.1.3", + "mocha-sonar-generic-test-coverage": "0.0.6", + "nyc": "^15.1.0", + "rewire": "^5.0.0", + "webpack": "^4.44.1", + "webpack-cli": "^4.10.0" + }, + "engines": { + "node": ">=6.12.3" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -8116,9 +8139,9 @@ } }, "node_modules/mastercard-client-encryption": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/mastercard-client-encryption/-/mastercard-client-encryption-1.9.0.tgz", - "integrity": "sha512-n/hjUXnfXaJ9x7ae+as0/gfQad4G7Rav6wyo5J/sgSYWkI8tR8M/TXLRmaiBBrQXRPmPgyqKRAhZE+PssUnGLQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/mastercard-client-encryption/-/mastercard-client-encryption-1.10.0.tgz", + "integrity": "sha512-Yjvxu8qhxamehGHURor5DItnjryMM1ykTyecWZJWBRCfEz4zCSmYynm2BHXWGzalJNAdZpYvk77eDCFt+QKujQ==", "dependencies": { "node-forge": "^1.3.0" }, diff --git a/package.json b/package.json index f495dab..7c0d016 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "main": "src/index.js", "scripts": { + "check": "npm test && npm run lint && npm run minify", "test": "jest", "minify": "mkdirp dist && browserify src/index.js | uglifyjs -o dist/mastercard-postman-encryption-lib.min.js", "lint": "eslint '**/*.js' || (echo \"Run 'npm run lint:fix' to fix most errors\" && exit 1)", @@ -20,7 +21,7 @@ "license": "ISC", "dependencies": { "js-sha256": "^0.10.1", - "mastercard-client-encryption": "^1.9.0", + "mastercard-client-encryption": "^1.10.0", "node-jose": "^2.2.0" }, "devDependencies": { diff --git a/src/jwe.js b/src/jwe.js index ff9140b..1509ca0 100644 --- a/src/jwe.js +++ b/src/jwe.js @@ -1,5 +1,6 @@ const jose = require('node-jose'); const { validateEnv } = require('./util'); +const { EncryptionUtils } = require('mastercard-client-encryption'); function jweEncryption(pm) { validateEnv(['pathToRawData', 'pathToEncryptedData', 'publicKeyFingerprint', 'encryptionCert'], pm.environment); @@ -8,34 +9,14 @@ function jweEncryption(pm) { const reqBody = JSON.parse(pm.request.body.raw); const pathToRawData = pm.environment.get('pathToRawData'); const pathToEncryptedData = pm.environment.get('pathToEncryptedData'); - const encryptedProperty = pm.environment.get('encryptedProperty') ?? 'encryptedData'; + const encryptedValueFieldName = pm.environment.get('encryptedValueFieldName') ?? 'encryptedData'; const publicKeyFingerprint = pm.environment.get('publicKeyFingerprint'); const encryptionCertificate = pm.environment.get('encryptionCert'); - // Get element in payload to encrypt - let tmpIn = reqBody; - let prevIn = null; - - const paths = pathToRawData.split('.'); - paths.forEach((e) => { - if (pathToRawData !== '$' && !Object.prototype.hasOwnProperty.call(tmpIn, e)) { - tmpIn[e] = {}; - } - prevIn = tmpIn; - tmpIn = tmpIn[e]; - }); - const elem = pathToRawData.split('.').pop(); - const target = pathToRawData !== '$' ? prevIn[elem] : reqBody; - - // Get output path of encrypted payload - let outPath = reqBody; - const pathsOut = pathToEncryptedData.split('.'); - pathsOut.forEach((e) => { - if (pathToEncryptedData !== '$' && !Object.prototype.hasOwnProperty.call(outPath, e)) { - outPath[e] = {}; - } - outPath = outPath[e]; - }); + const encryptionTarget = EncryptionUtils.elemFromPath(pathToRawData, reqBody); + if (!encryptionTarget || !encryptionTarget.node) { + return resolve(reqBody); + } const keystore = jose.JWK.createKeyStore(); return ( @@ -44,7 +25,7 @@ function jweEncryption(pm) { // Encrypt payload and attach to request body .then((publicKey) => { - const buffer = Buffer.from(JSON.stringify(target)); + const buffer = Buffer.from(JSON.stringify(encryptionTarget.node)); return jose.JWE.createEncrypt( { format: 'compact', @@ -57,20 +38,14 @@ function jweEncryption(pm) { .final(); }) .then((encrypted) => { - if (pathToEncryptedData !== '$') { - outPath[encryptedProperty] = encrypted; - } else { - if (pathToRawData === '$') { - const properties = Object.keys(reqBody); - properties.forEach((e) => { - delete reqBody[e]; - }); - } - reqBody[encryptedProperty] = encrypted; - } - delete prevIn[elem]; - - resolve(reqBody); + // mirror what the mastercard encryption lib does + const encryptedReqBody = EncryptionUtils.addEncryptedDataToBody( + { [encryptedValueFieldName]: encrypted }, + { element: pathToRawData, obj: pathToEncryptedData }, + encryptedValueFieldName, + reqBody, + ); + resolve(encryptedReqBody); }) ); }); diff --git a/test/jwe.test.js b/test/jwe.test.js index 7cccd20..81b51d3 100644 --- a/test/jwe.test.js +++ b/test/jwe.test.js @@ -1,16 +1,19 @@ const { jweEncryption } = require('../src/jwe'); const fs = require('fs'); const path = require('path'); +const EncryptionUtils = require('mastercard-client-encryption').EncryptionUtils; describe(`Tests for ${jweEncryption.name}()`, () => { // the postman object const pm = {}; const encryptionCert = fs.readFileSync(path.resolve(__dirname, './res/encryption_cert_pubic_key.pem')); beforeEach(() => { + jest.restoreAllMocks(); + const environment = { pathToRawData: '$', pathToEncryptedData: '$', - encryptedProperty: 'encryptedData', + encryptedValueFieldName: 'encryptedData', encryptionCert, publicKeyFingerprint: 'abcdef', }; @@ -19,6 +22,23 @@ describe(`Tests for ${jweEncryption.name}()`, () => { pm.request = { method: 'post', body: {} }; }); + test(`Returns unencrypted object if finding the element to be encrypted fails`, async () => { + pm.environment.set('pathToRawData', '$'); + pm.environment.set('pathToEncryptedData', '$'); + + const requestBody = { + a: 'b', + c: 'd', + }; + pm.request.body.raw = JSON.stringify(requestBody); + + jest.spyOn(EncryptionUtils, 'elemFromPath').mockReturnValue(null); + + const encryptionResult = await jweEncryption(pm); + + expect(encryptionResult).toEqual(requestBody); + }); + test('Encrypts a request object when the encryption path is the root of the request object', async () => { pm.environment.set('pathToRawData', '$'); pm.environment.set('pathToEncryptedData', '$'); @@ -28,9 +48,6 @@ describe(`Tests for ${jweEncryption.name}()`, () => { c: 'd', }); - const mockUpdateFn = jest.fn(); - pm.request.body.update = mockUpdateFn; - const expectedBodyFormat = { encryptedData: 'the encrypted request body', }; @@ -38,10 +55,13 @@ describe(`Tests for ${jweEncryption.name}()`, () => { const actualEncryptedBody = await jweEncryption(pm); expect(Object.keys(actualEncryptedBody)).toEqual(Object.keys(expectedBodyFormat)); + expect(actualEncryptedBody.encryptedData).not.toBe(undefined); + expect(actualEncryptedBody.encryptedData).not.toBe(null); }); test('Encrypts a request object when the encryption path is nested in the request object', async () => { pm.request.body.raw = JSON.stringify({ + irrelevantProperty: 'this should be preserved', path: { to: { foo: { @@ -55,19 +75,19 @@ describe(`Tests for ${jweEncryption.name}()`, () => { pm.environment.set('pathToRawData', 'path.to.foo'); pm.environment.set('pathToEncryptedData', 'path.to.encryptedFoo'); - const mockUpdateFn = jest.fn(); - pm.request.body.update = mockUpdateFn; - const expectedBodyFormat = { - encryptedData: 'the encrypted request body', + irrelevantProperty: 'this should be preserved', + path: { + to: { + encryptedFoo: 'the encrypted request body', + }, + }, }; const actualEncryptedBody = await jweEncryption(pm); expect(actualEncryptedBody.path.to.encryptedFoo).not.toBe(undefined); expect(actualEncryptedBody.path.to.encryptedFoo).not.toBe(null); - expect(Object.keys(actualEncryptedBody.path.to.encryptedFoo).sort()).toEqual( - Object.keys(expectedBodyFormat).sort(), - ); + expect(Object.keys(actualEncryptedBody).sort()).toEqual(Object.keys(expectedBodyFormat).sort()); }); });