Skip to content

Commit 1efd2dd

Browse files
authored
Merge pull request #59 from aleh-douhi/feature/first-level-node-jwe-encryption-decryption
Add ability to encrypt and decrypt first level field with JWE
2 parents 2ea501b + bd8bf1e commit 1efd2dd

File tree

9 files changed

+290
-6
lines changed

9 files changed

+290
-6
lines changed

README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ Output:
239239
- [Performing JWE Decryption](#performing-jwe-decryption)
240240
- [Encrypting Entire Payloads](#encrypting-entire-payloads-jwe)
241241
- [Decrypting Entire Payloads](#decrypting-entire-payloads-jwe)
242+
- [First Level Field Encryption and Decryption](#encrypting-decrypting-first-level-field-jwe)
242243

243244
##### • Introduction <a name="jwe-introduction"></a>
244245

@@ -466,6 +467,86 @@ Output:
466467
}
467468
```
468469

470+
##### • First Level Field Encryption and Decryption <a name="encrypting-decrypting-first-level-field-jwe"></a>
471+
472+
To have encrypted results in the first level field or to decrypt the first level field, specify `encryptedValueFieldName` to be the same as `obj` (for encryption) or `element` (for decryption):
473+
474+
Example of configuration:
475+
476+
```js
477+
const config = {
478+
paths: [
479+
{
480+
path: "/resource1",
481+
toEncrypt: [
482+
{
483+
/* path to element to be encrypted in request json body */
484+
element: "sensitive",
485+
/* path to object where to store encryption fields in request json body */
486+
obj: "encryptedData",
487+
},
488+
],
489+
toDecrypt: [
490+
{
491+
/* path to element where to store decrypted fields in response object */
492+
element: "encryptedData",
493+
/* path to object with encryption fields */
494+
obj: "sensitive",
495+
},
496+
],
497+
},
498+
],
499+
mode: "JWE",
500+
encryptedValueFieldName: "encryptedData",
501+
encryptionCertificate: "./path/to/public.cert",
502+
privateKey: "./path/to/your/private.key",
503+
};
504+
```
505+
506+
Example of encryption:
507+
508+
```js
509+
const payload = {
510+
sensitive: "this is a secret!",
511+
notSensitive: "not a secret",
512+
};
513+
const jwe = new (require("mastercard-client-encryption").JweEncryption)(config);
514+
//
515+
let responsePayload = jwe.encrypt("/resource1", header, payload);
516+
```
517+
518+
Output:
519+
520+
```json
521+
{
522+
"encryptedData": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM….Y+oPYKZEMTKyYcSIVEgtQw",
523+
"notSensitive": "not a secret"
524+
}
525+
```
526+
527+
Example of decryption:
528+
529+
```js
530+
const response = {};
531+
response.request = { url: "/resource1" };
532+
response.body =
533+
"{" +
534+
' "encryptedData": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM….Y+oPYKZEMTKyYcSIVEgtQw",' +
535+
' "notSensitive": "not a secret"' +
536+
"}";
537+
const jwe = new (require("mastercard-client-encryption").JweEncryption)(config);
538+
let responsePayload = jwe.decrypt(response);
539+
```
540+
541+
Output:
542+
543+
```json
544+
{
545+
"sensitive": "this is a secret",
546+
"notSensitive": "not a secret"
547+
}
548+
```
549+
469550
### Integrating with OpenAPI Generator API Client Libraries <a name="integrating-with-openapi-generator-api-client-libraries"></a>
470551

471552
[OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) generates API client libraries from [OpenAPI Specs](https://github.com/OAI/OpenAPI-Specification).

lib/mcapi/crypto/jwe-crypto.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ function JweCrypto(config) {
156156
let data = decipher.update(encryptedText, c.BASE64, c.UTF8);
157157
data += decipher.final(c.UTF8);
158158

159-
return JSON.parse(data);
159+
return utils.stringToJson(data);
160160
};
161161
}
162162

lib/mcapi/encryption/jwe-encryption.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ function encryptBody(path, body) {
8383
function decryptBody(path, body) {
8484
const elem = utils.elemFromPath(path.element, body);
8585
if (elem && elem.node) {
86-
const decryptedObj = this.crypto.decryptData(
87-
elem.node[this.config.encryptedValueFieldName]
88-
);
86+
const encryptedValue = (path.element === this.config.encryptedValueFieldName) ?
87+
elem.node : elem.node[this.config.encryptedValueFieldName];
88+
const decryptedObj = this.crypto.decryptData(encryptedValue);
8989
return utils.mutateObjectProperty(
9090
path.obj,
9191
decryptedObj,

lib/mcapi/utils/utils.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ module.exports.jsonToString = function (data) {
7676
}
7777
if (data === "") throw new Error("Json not valid");
7878
try {
79-
if ((typeof data === "string" || data instanceof String) && isJson(data)) {
80-
return JSON.stringify(JSON.parse(data));
79+
if (typeof data === "string" || data instanceof String) {
80+
return isJson(data) ? JSON.stringify(JSON.parse(data)) : data.valueOf();
8181
} else {
8282
return JSON.stringify(data);
8383
}
@@ -86,6 +86,28 @@ module.exports.jsonToString = function (data) {
8686
}
8787
};
8888

89+
/**
90+
* Convert Json string to Json object if it is a valid Json
91+
* Return back input string if it is not a Json
92+
*
93+
* @param {string} Json string or string
94+
* @returns {Object|string}
95+
*/
96+
module.exports.stringToJson = function (data) {
97+
if (typeof data === "undefined" || data === null) {
98+
throw new Error("Input not valid");
99+
}
100+
if (data === "") throw new Error("String is empty");
101+
if (!(typeof data === "string" || data instanceof String)) {
102+
throw new Error("Input should be a string");
103+
}
104+
try {
105+
return JSON.parse(data);
106+
} catch (e) {
107+
return data.valueOf();
108+
}
109+
};
110+
89111
function isJson(str) {
90112
try {
91113
JSON.parse(str);
@@ -432,6 +454,9 @@ module.exports.addEncryptedDataToBody = function(encryptedData, path, encryptedV
432454
) {
433455
this.deleteNode(path.element, body);
434456
}
457+
if (encryptedValueFieldName === path.obj) {
458+
encryptedData = encryptedData[encryptedValueFieldName];
459+
}
435460
body = this.mutateObjectProperty(path.obj, encryptedData, body);
436461
return body;
437462
};

test/jwe-crypto.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,24 @@ describe("JWE Crypto", () => {
189189
assert.ok(resp[3].length === 24);
190190
assert.ok(resp[4].length === 22);
191191
});
192+
193+
it("encrypt primitive string", () => {
194+
const data = "message";
195+
let resp = crypto.encryptData({
196+
data: data,
197+
});
198+
resp = resp[testConfig.encryptedValueFieldName].split(".");
199+
assert.ok(resp.length === 5);
200+
//Header is always constant
201+
assert.strictEqual(
202+
resp[0],
203+
"eyJraWQiOiJnSUVQd1RxREdmenc0dXd5TElLa3d3UzNnc3c4NW5FWFkwUFA2QllNSW5rPSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0"
204+
);
205+
assert.ok(resp[1].length === 342);
206+
assert.ok(resp[2].length === 22);
207+
assert.ok(resp[3].length === 10);
208+
assert.ok(resp[4].length === 22);
209+
});
192210
});
193211

194212
describe("#decryptData()", () => {
@@ -230,6 +248,13 @@ describe("JWE Crypto", () => {
230248
assert.ok(JSON.stringify(resp) === JSON.stringify({ foo: "bar" }));
231249
});
232250

251+
it("with valid encrypted string", () => {
252+
const resp = crypto.decryptData(
253+
"eyJraWQiOiJnSUVQd1RxREdmenc0dXd5TElLa3d3UzNnc3c4NW5FWFkwUFA2QllNSW5rPSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.vZBAXJC5Xcr0LaxdTRooLrbKc0B2cqjqAv3Dfz70s2EdUHf-swWPFe6QE-gb8PW7PQ-PZxkkIE5MhP6IMH4e/NH79V5j27c6Tno3R1/DfWQjRoN8xNm4sZver4FXESBiQia-PZip4D/hVmDWLKbom4SCD6ibLLmB9WcDVXpQEmX5G-lmd6kuEoBNOKQy08/QfVhqEr2H/2Q7PAcOjizPWUw6QK0SYzkaQIgTC6nlN/swa82zZa9NJeeTxJ1sJVmXzd4J-qjxwWjtzuRqb-kh4t/CUYT/lpf5NRaktBjXFyZFJ1dir5OgfdoA6-oIh8oUNMCt26SCCuYg-ev8sfHGDA.rxP1lOVCy4hIDwi5ETr2Bw.a3IIS9/6lA.77pOElwKjHBEwaPgRfHI4w"
254+
);
255+
assert.strictEqual(resp, "message");
256+
});
257+
233258
});
234259

235260
describe("#readPublicCertificate", () => {

test/jwe-encryption.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ describe("JWE Encryption", () => {
5050
!Object.prototype.hasOwnProperty.call(res.foo, "encryptedData")
5151
);
5252
});
53+
54+
it("decrypt first level element in response", () => {
55+
const encryption = new JweEncryption(testConfig);
56+
const decrypt = JweEncryption.__get__("decrypt");
57+
const response = require("./mock/jwe-response");
58+
const res = decrypt.call(encryption, response);
59+
assert.ok(res.decryptedData.accountNumber === "5123456789012345");
60+
assert.ok(!res.encryptedData);
61+
});
5362
});
5463

5564
describe("#import JweEncryption", () => {

test/mock/jwe-config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ module.exports = {
1313
element: "foo.elem1",
1414
obj: "foo",
1515
},
16+
{
17+
element: "encryptedData",
18+
obj: "decryptedData",
19+
},
1620
],
1721
},
1822
{

test/mock/jwe-response.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module.exports = {
22
request: { url: "/resource" },
33
body: {
4+
encryptedData : "eyJraWQiOiJnSUVQd1RxREdmenc0dXd5TElLa3d3UzNnc3c4NW5FWFkwUFA2QllNSW5rPSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.bg6UTkMY9omV6CpXZnTsLNDQgKHVSOf7pO4KxhjkQfYSM6I2WBLNL8TMVFuVnOwGnq7jXwBBtszIj0Enk7nmwme2VNKnBEyoe-1t99j1VPX7CVQIdezNnJbwFl746Vi-izt6fatRPHUbp47pvZ7qL8u_xS9Ucv8-1HxuykoVbiHcY1lb-Mm_dzEJf-2eG0fkJxuVJBtUktrO0nu6BC_D53UULTxju6goGcjmgOqrAzf4Yg0NRrzObLchWisbzVcbzO0Lnv0rxDYYIeN54BSKpKowglQ8EElKqLx3rCZ8is6Un2nKqhzsY52-DAZ5-HLSCDCyjEO6CB1w8Y2CXvANZg.B5pVI2hqtwLFuiCNnzonAw.ZDnLiPNy63ocuwhYQFfSneS13Ff5GlFc87kegf8mnAJxGsYS.YFistvxKLMfLGVY5vWV_Dw",
45
foo: {
56
elem1: {
67
encryptedData:

test/utils.test.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,58 @@ describe("Utils", () => {
140140
const res = utils.jsonToString('{"field": "value"}');
141141
assert.strictEqual(res, '{"field":"value"}');
142142
});
143+
144+
it("string from string", () => {
145+
const res = utils.jsonToString('value');
146+
assert.strictEqual(res, 'value');
147+
});
148+
149+
it("string from string object", () => {
150+
const res = utils.jsonToString(new String("value"));
151+
assert.strictEqual(res, 'value');
152+
});
153+
});
154+
155+
describe("#stringToJson", () => {
156+
it("when null", () => {
157+
assert.throws(() => {
158+
utils.stringToJson(null);
159+
});
160+
});
161+
162+
it("when undefined", () => {
163+
assert.throws(() => {
164+
utils.stringToJson(undefined);
165+
});
166+
});
167+
168+
it("when empty obj", () => {
169+
const res = utils.stringToJson('{}');
170+
assert.strictEqual(JSON.stringify(res), JSON.stringify({}));
171+
});
172+
173+
it("when empty str", () => {
174+
assert.throws(() => utils.stringToJson(""));
175+
});
176+
177+
it("when Json object", () => {
178+
assert.throws(() => utils.stringToJson({ field: "value" }));
179+
});
180+
181+
it("string from string", () => {
182+
const res = utils.stringToJson('value');
183+
assert.strictEqual(res, "value");
184+
});
185+
186+
it("string from string object", () => {
187+
const res = utils.stringToJson(new String("value"));
188+
assert.strictEqual(res, "value");
189+
});
190+
191+
it("correct json object from string", () => {
192+
const res = utils.stringToJson('{"field": "value"}');
193+
assert.strictEqual(JSON.stringify(res), JSON.stringify({ field: 'value' }));
194+
});
143195
});
144196

145197
describe("#mutateObjectProperty", () => {
@@ -314,6 +366,93 @@ describe("Utils", () => {
314366
});
315367
});
316368

369+
describe("#addEncryptedDataToBody", () => {
370+
it("encrypting first level field", () => {
371+
const body = {
372+
firstLevelField: "secret",
373+
anotherFirstLevelField: "value"
374+
};
375+
const expected = JSON.stringify({
376+
anotherFirstLevelField: "value",
377+
encryptedData: "changed"
378+
});
379+
const value = { encryptedData: "changed" };
380+
const path = { element: 'firstLevelField', obj: 'encryptedData' };
381+
utils.addEncryptedDataToBody(value, path, "encryptedData", body);
382+
assert.strictEqual(JSON.stringify(body), expected);
383+
});
384+
385+
it("encryptedValueFieldName is not in the path", () => {
386+
const body = {
387+
field: "secret",
388+
anotherField: "value"
389+
};
390+
const expected = JSON.stringify({
391+
anotherField: "value",
392+
encryptedField: {
393+
encryptedData: "changed"
394+
}
395+
});
396+
const value = { encryptedData: "changed" };
397+
const path = { element: 'field', obj: 'encryptedField' };
398+
utils.addEncryptedDataToBody(value, path, "encryptedData", body);
399+
assert.strictEqual(JSON.stringify(body), expected);
400+
});
401+
402+
it("encrypting to existing field", () => {
403+
const body = {
404+
field: "secret",
405+
anotherField: "value"
406+
};
407+
const expected = JSON.stringify({
408+
anotherField: "value",
409+
field: "changed"
410+
});
411+
const value = { field: "changed" };
412+
const path = { element: 'field', obj: 'field' };
413+
utils.addEncryptedDataToBody(value, path, "field", body);
414+
assert.strictEqual(JSON.stringify(body), expected);
415+
});
416+
417+
it("encrypting to existing subfield", () => {
418+
const body = {
419+
field: {
420+
subField: "secret"
421+
},
422+
anotherField: "value"
423+
};
424+
const expected = JSON.stringify({
425+
field: {
426+
subField: "changed"
427+
},
428+
anotherField: "value"
429+
});
430+
const value = { subField: "changed" };
431+
const path = { element: 'field.subField', obj: 'field' };
432+
utils.addEncryptedDataToBody(value, path, "subField", body);
433+
assert.strictEqual(JSON.stringify(body), expected);
434+
});
435+
436+
it("encrypting to new subfield", () => {
437+
const body = {
438+
field: {
439+
subField: "secret"
440+
},
441+
anotherField: "value"
442+
};
443+
const expected = JSON.stringify({
444+
field: {
445+
encryptedData: "changed"
446+
},
447+
anotherField: "value"
448+
});
449+
const value = { encryptedData: "changed" };
450+
const path = { element: 'field.subField', obj: 'field' };
451+
utils.addEncryptedDataToBody(value, path, "encryptedData", body);
452+
assert.strictEqual(JSON.stringify(body), expected);
453+
});
454+
});
455+
317456
describe("#getPrivateKey12", () => {
318457
it("empty alias", () => {
319458
assert.throws(() => {

0 commit comments

Comments
 (0)