Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/jwe encryption in nested path #3

Merged
merged 1 commit into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -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": {
Expand Down
55 changes: 15 additions & 40 deletions src/jwe.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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 (
Expand All @@ -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',
Expand All @@ -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);
})
);
});
Expand Down
42 changes: 31 additions & 11 deletions test/jwe.test.js
Original file line number Diff line number Diff line change
@@ -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',
};
Expand All @@ -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', '$');
Expand All @@ -28,20 +48,20 @@ describe(`Tests for ${jweEncryption.name}()`, () => {
c: 'd',
});

const mockUpdateFn = jest.fn();
pm.request.body.update = mockUpdateFn;

const expectedBodyFormat = {
encryptedData: 'the encrypted request body',
};

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: {
Expand All @@ -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());
});
});