Skip to content

Commit cfe5666

Browse files
authored
Ability to encrypt/decrypt array bodies (#29)
* Added ability to encrypt/decrypt array bodies and fixed minor eslint warnings. * Fixed sonar scan not running on pull requests and addressed report * Refactored function to reduce its cognitive complexity
1 parent a91db0a commit cfe5666

14 files changed

+364
-196
lines changed

.eslintrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ extends:
33
- prettier
44
env:
55
es6: true
6+
node: true
7+
jest: true
68
rules:
79
semi: 2
810
no-console: 2

.github/workflows/sonar-scanner.yml

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ name: Sonar
33
push:
44
branches:
55
- main
6-
pull_request_target:
7-
types:
8-
- opened
9-
- synchronize
10-
- reopened
6+
pull_request:
7+
branches:
8+
- main
119
schedule:
1210
- cron: 0 16 * * *
1311
jobs:
14-
build:
12+
sonarcloud:
1513
runs-on: ubuntu-latest
1614
steps:
1715
- uses: actions/checkout@v2
@@ -32,11 +30,6 @@ jobs:
3230
SONAR_TOKEN: '${{ secrets.SONAR_TOKEN }}'
3331
with:
3432
args: >
35-
-Dsonar.organization=mastercard
36-
-Dsonar.projectName=client-encryption-nodejs
37-
-Dsonar.projectKey=Mastercard_client-encryption-nodejs
3833
-Dsonar.sources=./lib -Dsonar.tests=./test
3934
-Dsonar.coverage.jacoco.xmlReportPaths=test-results.xml
4035
-Dsonar.javascript.lcov.reportPaths=.nyc_output/coverage.lcov
41-
-Dsonar.host.url=https://sonarcloud.io -Dsonar.login=${{
42-
secrets.SONAR_TOKEN }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,4 @@ test-results.xml
7474
#Sonar
7575
.scannerwork
7676
sonar-*
77+
!sonar-project.properties

lib/mcapi/crypto/crypto.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ function Crypto(config) {
101101

102102
try {
103103
return JSON.parse(decipher.output.data);
104-
} catch (e){
104+
} catch (e) {
105105
return decipher.output.data;
106106
}
107107
};
@@ -145,7 +145,7 @@ function Crypto(config) {
145145
* @private
146146
*/
147147
function createOAEPOptions(asymmetricCipher, oaepHashingAlgorithm) {
148-
if (asymmetricCipher.includes('OAEP') && oaepHashingAlgorithm != null) {
148+
if (asymmetricCipher.includes('OAEP') && oaepHashingAlgorithm !== null) {
149149
const mdForOaep = createMessageDigest(oaepHashingAlgorithm);
150150
return {
151151
md: mdForOaep,
@@ -299,8 +299,6 @@ function isValidConfig(config) {
299299
const propertiesBasic = ["oaepPaddingDigestAlgorithm", "dataEncoding", "encryptionCertificate", "encryptedValueFieldName"];
300300
const propertiesField = ["ivFieldName", "encryptedKeyFieldName"];
301301
const propertiesHeader = ["ivHeaderName", "encryptedKeyHeaderName", "oaepHashingAlgorithmHeaderName"];
302-
const propertiesFingerprint = ["publicKeyFingerprintType", "publicKeyFingerprintFieldName", "publicKeyFingerprintHeaderName"];
303-
const propertiesOptionalFingerprint = ["publicKeyFingerprint"];
304302
const contains = (props) => {
305303
return props.every((elem) => {
306304
return config[elem] !== null && typeof config[elem] !== "undefined";
@@ -309,7 +307,7 @@ function isValidConfig(config) {
309307
if (typeof config !== 'object' || config === null) {
310308
throw Error("Config not valid: config should be an object.");
311309
}
312-
if (config["paths"] == null || typeof config["paths"] === "undefined" ||
310+
if (config["paths"] === null || typeof config["paths"] === "undefined" ||
313311
!(config["paths"] instanceof Array)) {
314312
throw Error("Config not valid: paths should be an array of path element.");
315313
}
@@ -322,11 +320,32 @@ function isValidConfig(config) {
322320
if (config["dataEncoding"] !== "hex" && config["dataEncoding"] !== "base64") {
323321
throw Error("Config not valid: dataEncoding should be 'hex' or 'base64'");
324322
}
323+
validateFingerprint(config, contains);
324+
validateRootMapping(config);
325+
}
326+
327+
function validateFingerprint(config, contains) {
328+
const propertiesFingerprint = ["publicKeyFingerprintType", "publicKeyFingerprintFieldName", "publicKeyFingerprintHeaderName"];
329+
const propertiesOptionalFingerprint = ["publicKeyFingerprint"];
325330
if (!contains(propertiesOptionalFingerprint)
326331
&& (config[propertiesFingerprint[1]] || config[propertiesFingerprint[2]])
327332
&& config[propertiesFingerprint[0]] !== "certificate" && config[propertiesFingerprint[0]] !== "publicKey") {
328333
throw Error("Config not valid: propertiesFingerprint should be: 'certificate' or 'publicKey'");
329334
}
330335
}
331336

337+
function validateRootMapping(config) {
338+
function multipleRoots(elem) {
339+
return elem.length !== 1 && elem.some((item) => {
340+
return item.obj === "$" || item.element === "$";
341+
});
342+
}
343+
344+
config.paths.forEach((path) => {
345+
if (multipleRoots(path.toEncrypt) || multipleRoots(path.toDecrypt)) {
346+
throw Error("Config not valid: found multiple configurations encrypt/decrypt with root mapping");
347+
}
348+
});
349+
}
350+
332351
module.exports = Crypto;

lib/mcapi/fle/field-level-encryption.js

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ function FieldLevelEncryption(config) {
1313
this.decrypt = decrypt;
1414
this.config = config;
1515
this.crypto = new Crypto(config);
16-
this.isWithHeader = config.hasOwnProperty("ivHeaderName") && config.hasOwnProperty("encryptedKeyHeaderName");
16+
this.isWithHeader = Object.prototype.hasOwnProperty.call(config, "ivHeaderName") &&
17+
Object.prototype.hasOwnProperty.call(config, "encryptedKeyHeaderName");
1718
this.encryptionResponseProperties = [this.config.ivFieldName, this.config.encryptedKeyFieldName,
1819
this.config.publicKeyFingerprintFieldName, this.config.oaepHashingAlgorithmFieldName];
1920
}
@@ -25,7 +26,6 @@ function hasConfig(config, endpoint) {
2526
if (config && endpoint) {
2627
endpoint = endpoint.split("?").shift();
2728
const conf = config.paths.find((elem) => {
28-
// TODO grep from last index
2929
const regex = new RegExp(elem.path, "g");
3030
return endpoint.match(regex);
3131
}
@@ -45,7 +45,7 @@ function elemFromPath(path, obj) {
4545
if (path && paths.length > 0) {
4646
paths.forEach((e) => {
4747
parent = obj;
48-
obj = obj[e];
48+
obj = isJsonRoot(e) ? obj : obj[e];
4949
}
5050
);
5151
}
@@ -59,7 +59,7 @@ function elemFromPath(path, obj) {
5959
}
6060

6161
/**
62-
* Set encrpytion header parameters
62+
* Set encryption header parameters
6363
*
6464
* @param header HTTP header
6565
* @param params Encryption parameters
@@ -81,23 +81,24 @@ function setHeader(header, params) {
8181
* @returns {{header: *, body: *}}
8282
*/
8383
function encrypt(endpoint, header, body) {
84+
let bodyMap = body;
8485
const fleConfig = hasConfig(this.config, endpoint);
8586
if (fleConfig) {
8687
if (!this.isWithHeader) {
87-
fleConfig.toEncrypt.forEach((v) => {
88-
encryptBody.call(this, v, body);
88+
bodyMap = fleConfig.toEncrypt.map((v) => {
89+
return encryptBody.call(this, v, body);
8990
});
9091
} else {
9192
const encParams = this.crypto.newEncryptionParams({});
92-
fleConfig.toEncrypt.forEach((v) => {
93-
body = encryptWithHeader.call(this, encParams, v, body);
93+
bodyMap = fleConfig.toEncrypt.map((v) => {
94+
return encryptWithHeader.call(this, encParams, v, body);
9495
});
9596
setHeader.call(this, header, encParams);
9697
}
9798
}
9899
return {
99100
header: header,
100-
body: body
101+
body: fleConfig ? computeBody(fleConfig.toEncrypt, body, bodyMap) : body
101102
};
102103
}
103104

@@ -110,44 +111,44 @@ function encrypt(endpoint, header, body) {
110111
*/
111112
function decrypt(response) {
112113
const body = response.body;
114+
let bodyMap = response.body;
113115
const fleConfig = hasConfig(this.config, response.request.url);
114116
if (fleConfig) {
115-
if (!this.isWithHeader) {
116-
fleConfig.toDecrypt.forEach((v) => {
117-
decryptBody.call(this, v, body);
118-
});
119-
} else {
120-
fleConfig.toDecrypt.forEach((v) => {
121-
decryptWithHeader.call(this, v, body, response);
122-
});
123-
}
117+
bodyMap = fleConfig.toDecrypt.map((v) => {
118+
if (!this.isWithHeader) {
119+
return decryptBody.call(this, v, body);
120+
} else {
121+
return decryptWithHeader.call(this, v, body, response);
122+
}
123+
});
124124
}
125-
return body;
125+
return fleConfig ? computeBody(fleConfig.toDecrypt, body, bodyMap) : body
126126
}
127127

128128
/**
129-
* Encrypt body nodes inplace with given path
129+
* Encrypt body nodes with given path
130130
*
131131
* @private
132132
* @param path Config json path
133133
* @param body Body to encrypt
134134
*/
135135
function encryptBody(path, body) {
136136
const elem = elemFromPath(path.element, body);
137-
if (elem && elem.node){
137+
if (elem && elem.node) {
138138
const encryptedData = this.crypto.encryptData({data: elem.node});
139-
utils.mutateObjectProperty(path.obj,
139+
body = utils.mutateObjectProperty(path.obj,
140140
encryptedData,
141141
body);
142142
// delete encrypted field if not overridden
143-
if (path.element !== path.obj + "." + this.config.encryptedValueFieldName) {
143+
if (!isJsonRoot(path.obj) && path.element !== path.obj + "." + this.config.encryptedValueFieldName) {
144144
utils.deleteNode(path.element, body);
145145
}
146146
}
147+
return body;
147148
}
148149

149150
/**
150-
* Encrypt body nodes inplace with given path, without setting crypto info in the body
151+
* Encrypt body nodes with given path, without setting crypto info in the body
151152
*
152153
* @private
153154
* @param encParams encoding params to use
@@ -158,15 +159,17 @@ function encryptBody(path, body) {
158159
function encryptWithHeader(encParams, path, body) {
159160
const elem = elemFromPath(path.element, body).node;
160161
const encrypted = this.crypto.encryptData({data: elem}, encParams);
161-
return {[path.obj]: {[this.config.encryptedValueFieldName]: encrypted[this.config.encryptedValueFieldName]}};
162+
const data = {[this.config.encryptedValueFieldName]: encrypted[this.config.encryptedValueFieldName]};
163+
return isJsonRoot(path.obj) ? data : {[path.obj]: data};
162164
}
163165

164166
/**
165-
* Decrypt body nodes inplace with given path
167+
* Decrypt body nodes with given path
166168
*
167169
* @private
168170
* @param path Config json path
169171
* @param body encrypted body
172+
* @returns {Object} Decrypted body
170173
*/
171174
function decryptBody(path, body) {
172175
const elem = elemFromPath(path.element, body);
@@ -177,12 +180,13 @@ function decryptBody(path, body) {
177180
elem.node[this.config.oaepHashingAlgorithmFieldName], // oaepHashingAlgorithm
178181
elem.node[this.config.encryptedKeyFieldName] // encryptedKey
179182
);
180-
utils.mutateObjectProperty(path.obj, decryptedObj, body, path.element, this.encryptionResponseProperties);
183+
return utils.mutateObjectProperty(path.obj, decryptedObj, body, path.element, this.encryptionResponseProperties);
181184
}
185+
return body;
182186
}
183187

184188
/**
185-
* Decrypt body nodes inplace with given path, getting crypto info from the header
189+
* Decrypt body nodes with given path, getting crypto info from the header
186190
*
187191
* @private
188192
* @param path Config json path
@@ -191,19 +195,33 @@ function decryptBody(path, body) {
191195
*/
192196
function decryptWithHeader(path, body, response) {
193197
const elemEncryptedNode = elemFromPath(path.obj, body);
194-
if (elemEncryptedNode.node[path.element]) {
195-
const encryptedData = elemEncryptedNode.node[path.element][this.config.encryptedValueFieldName];
198+
const node = isJsonRoot(path.element) ? elemEncryptedNode.node : elemEncryptedNode.node[path.element];
199+
if (node) {
200+
const encryptedData = node[this.config.encryptedValueFieldName];
196201
for (const k in body) {
197202
// noinspection JSUnfilteredForInLoop
198203
delete body[k];
199204
}
200-
Object.assign(body, this.crypto.decryptData(
205+
const decrypted = this.crypto.decryptData(
201206
encryptedData,
202207
response.header[this.config.ivHeaderName],
203208
response.header[this.config.oaepHashingAlgorithmHeaderName],
204209
response.header[this.config.encryptedKeyHeaderName]
205-
));
210+
);
211+
return isJsonRoot(path.obj) ? decrypted : Object.assign(body, decrypted);
206212
}
207213
}
208214

215+
function isJsonRoot(elem) {
216+
return elem === "$";
217+
}
218+
219+
function computeBody(configParam, body, bodyMap){
220+
return (hasEncryptionParam(configParam, bodyMap)) ? bodyMap.pop() : body;
221+
}
222+
223+
function hasEncryptionParam(encParams, bodyMap){
224+
return encParams && encParams.length === 1 && bodyMap && bodyMap[0];
225+
}
226+
209227
module.exports = FieldLevelEncryption;

lib/mcapi/utils/utils.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,16 @@ module.exports.mutateObjectProperty = function (path, value, obj, srcPath, prope
8585
}
8686
const paths = path.split(".");
8787
paths.forEach((e) => {
88-
if (!tmp.hasOwnProperty(e)) {
88+
if (!Object.prototype.hasOwnProperty.call(tmp, e)) {
8989
tmp[e] = {};
9090
}
9191
prev = tmp;
9292
tmp = tmp[e];
9393
});
9494
const elem = path.split(".").pop();
95-
if (typeof value === 'object' && !(value instanceof Array)) { // decrypted value
95+
if (elem === "$"){
96+
obj = value; // replace root
97+
} else if (typeof value === 'object' && !(value instanceof Array)) { // decrypted value
9698
if (typeof prev[elem] !== 'object') {
9799
prev[elem] = {};
98100
}
@@ -101,6 +103,7 @@ module.exports.mutateObjectProperty = function (path, value, obj, srcPath, prope
101103
prev[elem] = value;
102104
}
103105
}
106+
return obj;
104107
};
105108

106109
module.exports.deleteNode = function (path, obj, properties) {
@@ -111,18 +114,14 @@ module.exports.deleteNode = function (path, obj, properties) {
111114
const toDelete = paths[paths.length - 1];
112115
paths.forEach((e, index) => {
113116
prev = obj;
114-
if (obj.hasOwnProperty(e)) {
117+
if (Object.prototype.hasOwnProperty.call(obj, e)) {
115118
obj = obj[e];
116119
if (obj && index === paths.length - 1) {
117120
delete prev[toDelete];
118121
}
119122
}
120123
});
121-
if (paths.length === 1 && paths[0] === "") {
122-
properties.forEach((e) => {
123-
delete obj[e];
124-
});
125-
}
124+
deleteRoot(obj, paths, properties);
126125
}
127126
};
128127

@@ -131,3 +130,12 @@ function overrideProperties(target, obj) {
131130
target[k] = obj[k];
132131
}
133132
}
133+
134+
function deleteRoot(obj, paths, properties){
135+
if (paths.length === 1) {
136+
properties = paths[0] === "$" ? Object.keys(obj) : properties;
137+
properties.forEach((e) => {
138+
delete obj[e];
139+
});
140+
}
141+
}

sonar-project.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
sonar.projectKey=Mastercard_client-encryption-nodejs
2+
sonar.organization=mastercard
3+
sonar.projectName=client-encryption-nodejs
4+
sonar.host.url=https://sonarcloud.io

0 commit comments

Comments
 (0)