Skip to content

Commit 3a5b800

Browse files
authored
Merge pull request #3 from Mastercard/feature/subtree_path_node
Build passing here: https://travis-ci.org/Mastercard/client-encryption-nodejs/builds/532391707
2 parents c99bf9f + 2b44d0e commit 3a5b800

File tree

13 files changed

+541
-96
lines changed

13 files changed

+541
-96
lines changed

README.md

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,24 @@ const config = {
9797
path: "/resource",
9898
toEncrypt: [
9999
{
100-
/* path to element to be encrypted in request object */
100+
/* path to element to be encrypted in request json body */
101101
element: "path.to.foo",
102-
/* path to object where to store encryption fields in request object */
102+
/* path to object where to store encryption fields in request json body */
103103
obj: "path.to.encryptedFoo"
104104
}],
105105
toDecrypt: [
106106
{
107-
element: "path.to.foo",
108-
obj: "path.to.encryptedFoo"
107+
/* path to element where to store decrypted fields in response object */
108+
element: "path.to.encryptedFoo",
109+
/* path to object with encryption fields */
110+
obj: "path.to.foo"
109111
}
110112
]
111113
}
112114
],
113115
ivFieldName: 'iv',
114116
encryptedKeyFieldName: 'encryptedKey',
115-
encryptedValueFieldName: 'encryptedValue',
117+
encryptedValueFieldName: 'encryptedData',
116118
dataEncoding: 'hex',
117119
encryptionCertificate: "./path/to/public.cert"
118120
oaepPaddingDigestAlgorithm: 'SHA-256',
@@ -139,8 +141,8 @@ const string payload =
139141
"path": {
140142
"to": {
141143
"foo": {
142-
"sensitiveField1": "sensitiveValue1",
143-
"sensitiveField2": "sensitiveValue2"
144+
"sensitive": "this is a secret!",
145+
"sensitive2": "this is a super-secret!"
144146
}
145147
}
146148
}
@@ -153,15 +155,17 @@ Output:
153155

154156
```json
155157
{
156-
"path": {
157-
"to": {
158-
"encryptedFoo": {
159-
"iv": "7f1105fb0c684864a189fb3709ce3d28",
160-
"encryptedKey": "67f467d1b653d98411a0c6d3c(...)ffd4c09dd42f713a51bff2b48f937c8",
161-
"encryptedValue": "b73aabd267517fc09ed72455c2(...)dffb5fa04bf6e6ce9ade1ff514ed6141"
162-
}
163-
}
158+
"path": {
159+
"to": {
160+
"encryptedFoo": {
161+
"iv": "7f1105fb0c684864a189fb3709ce3d28",
162+
"encryptedKey": "67f467d1b653d98411a0c6d3c(...)ffd4c09dd42f713a51bff2b48f937c8",
163+
"encryptedData": "b73aabd267517fc09ed72455c2(...)dffb5fa04bf6e6ce9ade1ff514ed6141",
164+
"publicKeyFingerprint": "80810fc13a8319fcf0e2e(...)82cc3ce671176343cfe8160c2279",
165+
"oaepHashingAlgorithm": "SHA256"
166+
}
164167
}
168+
}
165169
}
166170
```
167171

@@ -186,28 +190,29 @@ response.body =
186190
"encryptedFoo": {
187191
"iv": "e5d313c056c411170bf07ac82ede78c9",
188192
"encryptedKey": "e3a56746c0f9109d18b3a2652b76(...)f16d8afeff36b2479652f5c24ae7bd",
189-
"encryptedValue": "809a09d78257af5379df0c454dcdf(...)353ed59fe72fd4a7735c69da4080e74f"
193+
"encryptedData": "809a09d78257af5379df0c454dcdf(...)353ed59fe72fd4a7735c69da4080e74f",
194+
"oaepHashingAlgorithm": "SHA256",
195+
"publicKeyFingerprint": "80810fc13a8319fcf0e2e(...)3ce671176343cfe8160c2279"
190196
}
191197
}
192198
}
193199
};
194200
const fle = new require('mastercard-client-encryption').FieldLevelEncryption(config);
195201
let responsePayload = fle.decrypt(response);
196-
console.log(responsePayload);
197202
```
198203

199204
Output:
200205

201206
```json
202207
{
203-
"path": {
204-
"to": {
205-
"foo": {
206-
"sensitiveField1": "sensitiveValue1",
207-
"sensitiveField2": "sensitiveValue2"
208-
}
209-
}
208+
"path": {
209+
"to": {
210+
"foo": {
211+
"sensitive": "this is a secret",
212+
"sensitive2": "this is a super secret!"
213+
}
210214
}
215+
}
211216
}
212217
```
213218

@@ -236,15 +241,15 @@ See also:
236241

237242
To use it:
238243

239-
1. Generate the OpenAPI client, as (above)[#openapi-generator]
244+
1. Generate the OpenAPI client, as [above](#openapi-generator)
240245

241246
2. Import the **mastercard-client-encryption** library
242247

243248
```js
244249
const mcapi = require('mastercard-client-encryption');
245250
```
246251

247-
3. Import the OpenAPI Client using our decorator object:
252+
3. Import the OpenAPI Client using the `Service` decorator object:
248253

249254
```js
250255
const openAPIClient = require('./path/to/generated/openapi/client');
@@ -262,7 +267,7 @@ To use it:
262267
let merchant = /* ... */
263268
api.createMerchants(merchant, (error, data, response) => {
264269
// requests and responses will be automatically encrypted and decrypted
265-
// accordingly to the configuration used to instantiate the mcapi.Service.
270+
// accordingly with the configuration used to instantiate the mcapi.Service.
266271

267272
/* use response/data object here */
268273
});

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

Lines changed: 85 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ function FieldLevelEncryption(config) {
1414
this.config = config;
1515
this.crypto = new Crypto(config);
1616
this.isWithHeader = config.hasOwnProperty("ivHeaderName") && config.hasOwnProperty("encryptedKeyHeaderName");
17+
this.encryptionResponseProperties = [this.config.ivFieldName, this.config.encryptedKeyFieldName,
18+
this.config.publicKeyFingerprintFieldName, this.config.oaepHashingAlgorithmFieldName];
1719
}
1820

1921
/**
@@ -83,18 +85,12 @@ function encrypt(endpoint, header, body) {
8385
if (fleConfig) {
8486
if (!this.isWithHeader) {
8587
fleConfig.toEncrypt.forEach((v) => {
86-
let elem = elemFromPath(v.element, body).node;
87-
let encryptedData = this.crypto.encryptData({data: elem});
88-
utils.mutateObjectProperty(v.obj,
89-
encryptedData,
90-
body);
88+
encryptBody.call(this, v, body);
9189
});
9290
} else {
9391
let encParams = this.crypto.newEncryptionParams({});
9492
fleConfig.toEncrypt.forEach((v) => {
95-
let elem = elemFromPath(v.element, body).node;
96-
let encrypted = this.crypto.encryptData({data: elem}, encParams);
97-
body = {[v.obj]: {[this.config.encryptedValueFieldName]: encrypted[this.config.encryptedValueFieldName]}};
93+
body = encryptWithHeader.call(this, encParams, v, body);
9894
});
9995
setHeader.call(this, header, encParams);
10096
}
@@ -118,37 +114,94 @@ function decrypt(response) {
118114
if (fleConfig) {
119115
if (!this.isWithHeader) {
120116
fleConfig.toDecrypt.forEach((v) => {
121-
const elemEncryptedNode = elemFromPath(v.element, body);
122-
if (elemEncryptedNode) {
123-
// TODO v.obj could have sub-tree
124-
((v.obj) ? body[v.obj] : body)[this.config.encryptedValueFieldName] = this.crypto.decryptData(
125-
elemEncryptedNode.node, // encrypted data
126-
elemEncryptedNode.parent[this.config.ivFieldName], // iv field
127-
elemEncryptedNode.parent[this.config.oaepHashingAlgorithmFieldName], // oaepHashingAlgorithm
128-
elemEncryptedNode.parent[this.config.encryptedKeyFieldName] // encryptedKey
129-
);
130-
}
117+
decryptBody.call(this, v, body);
131118
});
132119
} else {
133120
fleConfig.toDecrypt.forEach((v) => {
134-
const elemEncryptedNode = elemFromPath(v.element, body);
135-
if (elemEncryptedNode.node[v.obj]) {
136-
const encryptedData = elemEncryptedNode.node[v.obj][this.config.encryptedValueFieldName];
137-
for (let k in body) {
138-
// noinspection JSUnfilteredForInLoop
139-
delete body[k];
140-
}
141-
Object.assign(body, this.crypto.decryptData(
142-
encryptedData,
143-
response.header[this.config.ivHeaderName],
144-
response.header[this.config.oaepHashingAlgorithmHeaderName],
145-
response.header[this.config.encryptedKeyHeaderName]
146-
));
147-
}
121+
decryptWithHeader.call(this, v, body, response);
148122
});
149123
}
150124
}
151125
return body;
152126
}
153127

128+
/**
129+
* Encrypt body nodes inplace with given path
130+
*
131+
* @private
132+
* @param path Config json path
133+
* @param body Body to encrypt
134+
*/
135+
function encryptBody(path, body) {
136+
let elem = elemFromPath(path.element, body);
137+
let encryptedData = this.crypto.encryptData({data: elem.node});
138+
utils.mutateObjectProperty(path.obj,
139+
encryptedData,
140+
body);
141+
// delete encrypted field if not overridden
142+
if (path.element !== path.obj + "." + this.config.encryptedValueFieldName) {
143+
utils.deleteNode(path.element, body);
144+
}
145+
}
146+
147+
/**
148+
* Encrypt body nodes inplace with given path, without setting crypto info in the body
149+
*
150+
* @private
151+
* @param encParams encoding params to use
152+
* @param path Config json path
153+
* @param body body to encrypt
154+
* @returns {Object} Encrypted body
155+
*/
156+
function encryptWithHeader(encParams, path, body) {
157+
let elem = elemFromPath(path.element, body).node;
158+
let encrypted = this.crypto.encryptData({data: elem}, encParams);
159+
return {[path.obj]: {[this.config.encryptedValueFieldName]: encrypted[this.config.encryptedValueFieldName]}};
160+
}
161+
162+
/**
163+
* Decrypt body nodes inplace with given path
164+
*
165+
* @private
166+
* @param path Config json path
167+
* @param body encrypted body
168+
*/
169+
function decryptBody(path, body) {
170+
const elem = elemFromPath(path.element, body);
171+
if (elem && elem.node) {
172+
let decryptedObj = this.crypto.decryptData(
173+
elem.node[this.config.encryptedValueFieldName], // encrypted data
174+
elem.node[this.config.ivFieldName], // iv field
175+
elem.node[this.config.oaepHashingAlgorithmFieldName], // oaepHashingAlgorithm
176+
elem.node[this.config.encryptedKeyFieldName] // encryptedKey
177+
);
178+
utils.mutateObjectProperty(path.obj, decryptedObj, body, path.element, this.encryptionResponseProperties);
179+
}
180+
}
181+
182+
/**
183+
* Decrypt body nodes inplace with given path, getting crypto info from the header
184+
*
185+
* @private
186+
* @param path Config json path
187+
* @param body encrypted body
188+
* @param response Response with header to update
189+
*/
190+
function decryptWithHeader(path, body, response) {
191+
const elemEncryptedNode = elemFromPath(path.obj, body);
192+
if (elemEncryptedNode.node[path.element]) {
193+
const encryptedData = elemEncryptedNode.node[path.element][this.config.encryptedValueFieldName];
194+
for (let k in body) {
195+
// noinspection JSUnfilteredForInLoop
196+
delete body[k];
197+
}
198+
Object.assign(body, this.crypto.decryptData(
199+
encryptedData,
200+
response.header[this.config.ivHeaderName],
201+
response.header[this.config.oaepHashingAlgorithmHeaderName],
202+
response.header[this.config.encryptedKeyHeaderName]
203+
));
204+
}
205+
}
206+
154207
module.exports = FieldLevelEncryption;

lib/mcapi/utils/utils.js

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,28 +76,58 @@ module.exports.jsonToString = function (data) {
7676
}
7777
};
7878

79-
module.exports.mutateObjectProperty = function (path, value, obj) {
79+
module.exports.mutateObjectProperty = function (path, value, obj, srcPath, properties) {
8080
let tmp = obj;
8181
let prev = null;
82-
let paths = path.split(".");
83-
paths.forEach((e) => {
84-
if (tmp.hasOwnProperty(e)) {
82+
if (path) {
83+
if (srcPath !== null && srcPath !== undefined) {
84+
this.deleteNode(srcPath, obj, properties); // delete src
85+
}
86+
let paths = path.split(".");
87+
paths.forEach((e) => {
88+
if (!tmp.hasOwnProperty(e)) {
89+
tmp[e] = {};
90+
}
8591
prev = tmp;
8692
tmp = tmp[e];
87-
}
88-
});
89-
let elem = path.split(".").pop();
90-
if (prev && prev.hasOwnProperty(elem)) {
91-
if (typeof value === 'object') {
93+
});
94+
let elem = path.split(".").pop();
95+
if (typeof value === 'object' && !(value instanceof Array)) { // decrypted value
96+
if (typeof prev[elem] !== 'object') {
97+
prev[elem] = {};
98+
}
9299
overrideProperties(prev[elem], value);
93100
} else {
94101
prev[elem] = value;
95102
}
96103
}
97104
};
98105

106+
module.exports.deleteNode = function (path, obj, properties) {
107+
properties = properties || [];
108+
let prev = obj;
109+
if (path !== null && path !== undefined && obj !== null && obj !== undefined) {
110+
let paths = path.split('.');
111+
let toDelete = paths[paths.length - 1];
112+
paths.forEach((e, index) => {
113+
prev = obj;
114+
if (obj.hasOwnProperty(e)) {
115+
obj = obj[e];
116+
if (obj && index === paths.length - 1) {
117+
delete prev[toDelete];
118+
}
119+
}
120+
});
121+
if (paths.length === 1 && paths[0] === "") {
122+
properties.forEach((e) => {
123+
delete obj[e];
124+
});
125+
}
126+
}
127+
};
128+
99129
function overrideProperties(target, obj) {
100130
for (let k in obj) {
101131
target[k] = obj[k];
102132
}
103-
}
133+
};

test/crypto.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,10 @@ describe("Crypto", () => {
289289

290290
describe("#computePublicFingerprint", () => {
291291
let computePublicFingerprint = Crypto.__get__("computePublicFingerprint");
292+
let crypto;
293+
before(() => {
294+
crypto = new Crypto(testConfig);
295+
});
292296

293297
it("not valid config", () => {
294298
assert.ok(!computePublicFingerprint());
@@ -298,6 +302,16 @@ describe("Crypto", () => {
298302
assert.ok(!computePublicFingerprint({}));
299303
});
300304

305+
it("compute public fingerprint: certificate", () => {
306+
assert.ok("80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
307+
computePublicFingerprint.call(crypto, {publicKeyFingerprintType: "certificate"}));
308+
});
309+
310+
it("compute public fingerprint: publicKey", () => {
311+
assert.ok("80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
312+
computePublicFingerprint.call(crypto, {publicKeyFingerprintType: "publicKey"}));
313+
});
314+
301315
});
302316

303317
});

0 commit comments

Comments
 (0)