├── test
├── res
│ ├── empty.der
│ ├── empty.key
│ ├── empty.p12
│ ├── empty.pem
│ ├── keys
│ │ ├── pkcs12
│ │ │ ├── empty_key_container.unknown_extension
│ │ │ ├── test_key.p12
│ │ │ ├── test_key_container.p12
│ │ │ └── test_key_container.unknown_extension
│ │ ├── pkcs1
│ │ │ ├── test_key.der
│ │ │ └── test_key.pem
│ │ └── pkcs8
│ │ │ ├── test_key.der
│ │ │ └── test_key.pem
│ ├── test_key.der
│ ├── test_certificate.cert
│ ├── service.yaml
│ └── test_key.pem
├── mock
│ ├── config-readme.js
│ ├── response-root.js
│ ├── response-header.js
│ ├── response.js
│ ├── config-header.js
│ ├── response-readme.js
│ ├── jwe-response.js
│ ├── jwe-config.js
│ └── config.js
├── jwe-encryption.test.js
├── mcapi-service.test.js
├── field-level-encryption.test.js
├── payload-encryption.test.js
├── jwe-crypto.test.js
├── field-level-crypto.test.js
└── utils.test.js
├── sonar-project.properties
├── lib
└── mcapi
│ ├── utils
│ ├── constants.js
│ └── utils.js
│ ├── mcapi-service.js
│ ├── encryption
│ ├── jwe-encryption.js
│ └── field-level-encryption.js
│ └── crypto
│ ├── field-level-crypto.js
│ └── jwe-crypto.js
├── .editorconfig
├── .eslintrc
├── webpack.config.js
├── index.js
├── .github
├── workflows
│ ├── broken-links.yml
│ ├── test.yml
│ └── sonar-scanner.yml
├── pull_request_template.md
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── LICENSE
├── .gitignore
├── package.json
└── README.md
/test/res/empty.der:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/res/empty.key:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/res/empty.p12:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/res/empty.pem:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/res/keys/pkcs12/empty_key_container.unknown_extension:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/res/test_key.der:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard/client-encryption-nodejs/HEAD/test/res/test_key.der
--------------------------------------------------------------------------------
/test/res/keys/pkcs1/test_key.der:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard/client-encryption-nodejs/HEAD/test/res/keys/pkcs1/test_key.der
--------------------------------------------------------------------------------
/test/res/keys/pkcs12/test_key.p12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard/client-encryption-nodejs/HEAD/test/res/keys/pkcs12/test_key.p12
--------------------------------------------------------------------------------
/test/res/keys/pkcs8/test_key.der:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard/client-encryption-nodejs/HEAD/test/res/keys/pkcs8/test_key.der
--------------------------------------------------------------------------------
/test/res/keys/pkcs12/test_key_container.p12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard/client-encryption-nodejs/HEAD/test/res/keys/pkcs12/test_key_container.p12
--------------------------------------------------------------------------------
/test/res/keys/pkcs12/test_key_container.unknown_extension:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard/client-encryption-nodejs/HEAD/test/res/keys/pkcs12/test_key_container.unknown_extension
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=Mastercard_client-encryption-nodejs
2 | sonar.organization=mastercard
3 | sonar.projectName=client-encryption-nodejs
4 | sonar.host.url=https://sonarcloud.io
5 |
--------------------------------------------------------------------------------
/lib/mcapi/utils/constants.js:
--------------------------------------------------------------------------------
1 | module.exports.BASE64 = "base64";
2 | module.exports.UTF8 = "utf8";
3 | module.exports.BASE64URL = "base64url";
4 | module.exports.BINARY = "binary";
5 | module.exports.ASCII = "ascii";
6 | module.exports.HEX = "hex";
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | extends:
2 | - eslint:recommended
3 | - prettier
4 | env:
5 | es6: true
6 | node: true
7 | jest: true
8 | rules:
9 | semi: 2
10 | no-console: 2
11 | no-empty: 2
12 | eqeqeq:
13 | - 2
14 | - always
15 | no-unused-vars: 1
16 | no-unsafe-negation: 2
17 | prefer-const: 2
18 | no-var: 2
19 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: './index.js',
5 | output: {
6 | filename: 'client-encryption-nodejs.min.js',
7 | path: path.resolve(__dirname, 'dist'),
8 | library: 'mcencrypt',
9 | libraryTarget: 'var'
10 |
11 | },
12 | resolve: {
13 | fallback: {
14 | fs: false,
15 | crypto: false
16 | }
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Expose the MC-Api Service
5 | * @see {module:mcapi/Service}
6 | */
7 | module.exports = {
8 | Service: require('./lib/mcapi/mcapi-service'),
9 | FieldLevelEncryption: require('./lib/mcapi/mcapi-service').FieldLevelEncryption,
10 | JweEncryption: require('./lib/mcapi/mcapi-service').JweEncryption,
11 | EncryptionUtils: require('./lib/mcapi/utils/utils')
12 | };
13 |
--------------------------------------------------------------------------------
/.github/workflows/broken-links.yml:
--------------------------------------------------------------------------------
1 | 'on':
2 | push:
3 | branches:
4 | - "**"
5 | schedule:
6 | - cron: 0 14 * * *
7 | name: broken links?
8 | jobs:
9 | linkChecker:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Link Checker
14 | id: lc
15 | uses: lycheeverse/lychee-action@v2
16 | with:
17 | args: --no-progress './**/*.md' './**/*.html' --insecure --verbose --accept 200,204,206 --exclude '^http://npmjs\.org' --exclude '^http://www\.npmjs\.com'
18 | - name: Fail?
19 | run: 'exit ${{ steps.lc.outputs.exit_code }}'
20 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 | ### PR checklist
3 |
4 | - [ ] An issue/feature request has been created for this PR
5 | - [ ] Pull Request title clearly describes the work in the pull request and the Pull Request description provides details about how to validate the work. Missing information here may result in a delayed response.
6 | - [ ] File the PR against the `master` branch
7 | - [ ] The code in this PR is covered by unit tests
8 |
9 | #### Link to issue/feature request: *add the link here*
10 |
11 | #### Description
12 | A clear and concise description of what is this PR for and any additional info might be useful for reviewing it.
13 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Build & Test
2 | 'on':
3 | push:
4 | branches:
5 | - "**"
6 | pull_request:
7 | branches:
8 | - "**"
9 | schedule:
10 | - cron: 0 16 * * *
11 | workflow_dispatch:
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | node-version:
18 | - 17.x
19 | - 18.x
20 | - 19.x
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: 'Use Node.js ${{ matrix.node-version }}'
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: '${{ matrix.node-version }}'
27 | - run: npm ci
28 | - run: npm run build --if-present
29 | - run: npm test
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[REQ] Feature Request Description"
5 | labels: 'Enhancement: Feature'
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Is your feature request related to a problem? Please describe.
11 |
12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
13 |
14 | ### Describe the solution you'd like
15 |
16 | A clear and concise description of what you want to happen.
17 |
18 | ### Describe alternatives you've considered
19 |
20 | A clear and concise description of any alternative solutions or features you've considered.
21 |
22 | ### Additional context
23 |
24 | Add any other context or screenshots about the feature request here.
25 |
--------------------------------------------------------------------------------
/test/mock/config-readme.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | paths: [
3 | {
4 | path: "/resource",
5 | toEncrypt: [
6 | {
7 | element: "path.to.encryptedData",
8 | obj: "path.to",
9 | },
10 | ],
11 | toDecrypt: [
12 | {
13 | element: "path.to.encryptedFoo",
14 | obj: "path.to.foo",
15 | },
16 | ],
17 | },
18 | ],
19 | oaepPaddingDigestAlgorithm: "SHA-512",
20 | ivFieldName: "iv",
21 | encryptedKeyFieldName: "encryptedKey",
22 | encryptedValueFieldName: "encryptedData",
23 | oaepHashingAlgorithmFieldName: "oaepHashingAlgorithm",
24 | publicKeyFingerprintFieldName: "publicKeyFingerprint",
25 | publicKeyFingerprintType: "certificate",
26 | dataEncoding: "hex",
27 | encryptionCertificate: "./test/res/test_certificate.cert",
28 | privateKey: "./test/res/test_key.der",
29 | };
30 |
--------------------------------------------------------------------------------
/test/res/keys/pkcs1/test_key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIICXQIBAAKBgQDHul8A7MM3ynx1XRxmLCRZ1Lr66QppWP5ZaEotsQPpFtyq3w/x
3 | dLWZd5XXQcIb/wUQWcbxBWYJPMk/GVQ3pvkIKEqC+h08VdXtfbBlM+RE8OYaQW5O
4 | Fg7YjHgL/WvCka6VJ990RuX9Zdj9TUu8GyqERll/XUDACs85atbSW6PBQQIDAQAB
5 | AoGBAL9acs0LCZn5OLalB6FoJ0edhasA/MWjysRUI8WU898s1SwsXDUEkTxAk2HR
6 | kayK7woUSYL/nhu5jkIS/Vn4clvH7XRkch22cjAAs4oGZXUUlVrcXFDiR0o2OqAI
7 | Xh24ESM/tpDWmn/N30afn31TBAKxgY5Ej2SrmK5gjSeMGI9xAkEA+gPM/P+jLiwy
8 | wuiS4QRYxeDFtluaafl7UHR/I6vL9VaqOptwV1e/JlytKNMCwoMqadd739/BAX9d
9 | qNgpniHC9QJBAMyCZBAc8PIjfM1h4seEDeb2mxl1Y2DmcDUVWIt+E4Zs3m1I59we
10 | VgY/BwX4UIElhgsVjNo6qXYPbWiql4/tzZ0CQQCLJ6RnyO2NXIJgY8yku6Ohd6r0
11 | BeZbR8XwEPdW5l8eTb9v4WZU5vz4oCqtB02I8DKiOJK1F7g4WijKOo5neoklAkBt
12 | G9/w7M/sD9zk4qWQVrboE4faRFPZ/fe9in7sJT6biHf/DFePi6vPt06y87FXxcJH
13 | JZ85SvTgZQi1P9aO1ovNAkAVxgJq94CKCZjOiks5xTro6V0pYdsx0tuuIlx/vGVG
14 | H3bcmpSm/S54nSovzbEDN7gKr4CSSsAQqC6oPwJSQN6m
15 | -----END RSA PRIVATE KEY-----
16 |
--------------------------------------------------------------------------------
/test/res/keys/pkcs8/test_key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALCcqlioDdtVqSIW
3 | /Y0PUM0YX5Us76B6VSS5e9LwFeK9g/jsrfGs72WdRxEX1uHTzZXHGimMmmODqv4M
4 | bZ5Qbbs7dCZwSKlqNlSKLx40zwy4xZh1ie/UJ2CgUhu62wXDnUMg4HE0l9oY5g/P
5 | BXvSbuGM87gMnNOk0E+pfWHjQbvlAgMBAAECgYBggfuD3rFTvYdinXWH82qP6FWy
6 | yo9W/gIwwzqqlY8gC7dl+s9CVOGsgTkoWgKN/JNG2Tmuoqpq3rQ9hsUP0Ztj32ty
7 | VfEWqKipDtC0x2xi2D7I25gxVLu521tqLhoaWN1oWdPraflXEkEY9p8CD8tzxc6Q
8 | wi6ShZ8TSacuLvivgQJBAOd/X4iNmmPZ0AY+ZVri4Ic+YXOOapagkXuR2Y5pcRfw
9 | 3jueEwZMIo142L+dFZMDORsqPhFwwCzOcbn2NEID4ckCQQDDTh607HbkvXAwYHDZ
10 | 6gpczrhSAkpjQ0fffOFuF8sI5A64KjnTGte+fm8ic5Mess1oP6LdaS0HvJs4n+m7
11 | nPc9AkEApTwbOmKoQoEjpHFA8wBhducls8+BcQYnEWZnPOkyGf6JAVCxD5ukRgpt
12 | 20cKMSbpyeP67YPnB5RLRIrhfgU7UQJAX3d5NRD9UPR0uYD6yNpJNHJr0NKD0B+c
13 | K1dczjbdLTxlIYqqd1GAsgIViu6ZtIDMPTAWCUqXE1gTO8uXMfkZNQJASgdEiTiI
14 | EnZLto+1hRr/fYNzIJHDelJ2oGtzbRmUtXQ8d489obsZusK2kSPWgX1lzXjvrv07
15 | jznkRk0QtDG8Vw==
16 | -----END PRIVATE KEY-----
17 |
--------------------------------------------------------------------------------
/test/mock/response-root.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | request: { url: "/resource" },
3 | body: {
4 | encryptedData:
5 | "3097e36bf8b71637a0273abe69c23752d6157464ce49f6f35120d28bedfb63a1f2c8087be3a3bc9775592db41db87a8c",
6 | iv: "22507f596fffb45b15244356981d7ea1",
7 | encryptedKey:
8 | "d4714161898b8bc5c54a63f71ae7c7a40734e4f7c7e27d121ac5e85a3fa47946aa3546027abe0874d751d5ae701491a7f572fc30fa08dd671d358746ffe8709cba36010f97864105b175c51b6f32d36d981287698a3f6f8707aedf980cce19bfe7c5286ddba87b7f3e5abbfa88a980779037c0b7902d340d73201cf3f0b546c2ad9f54e4b71a43504da947a3cb7af54d61717624e636a90069be3c46c19b9ae8b76794321b877544dd03f0ca816288672ef361c3e8f14d4a1ee96ba72d21e3a36c020aa174635a8579b0e9af761d96437e1fa167f00888ff2532292e7a220f5bc948f8159dea2541b8c6df6463213de292b4485076241c90706efad93f9b98ea",
9 | publicKeyFingerprint:
10 | "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
11 | oaepHashingAlgorithm: "SHA512",
12 | notDelete: "this field should be deleted",
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/test/mock/response-header.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | request: { url: "/resource" },
3 | header: {
4 | "x-iv": "22507f596fffb45b15244356981d7ea1",
5 | "x-encrypted-key":
6 | "d4714161898b8bc5c54a63f71ae7c7a40734e4f7c7e27d121ac5e85a3fa47946aa3546027abe0874d751d5ae701491a7f572fc30fa08dd671d358746ffe8709cba36010f97864105b175c51b6f32d36d981287698a3f6f8707aedf980cce19bfe7c5286ddba87b7f3e5abbfa88a980779037c0b7902d340d73201cf3f0b546c2ad9f54e4b71a43504da947a3cb7af54d61717624e636a90069be3c46c19b9ae8b76794321b877544dd03f0ca816288672ef361c3e8f14d4a1ee96ba72d21e3a36c020aa174635a8579b0e9af761d96437e1fa167f00888ff2532292e7a220f5bc948f8159dea2541b8c6df6463213de292b4485076241c90706efad93f9b98ea",
7 | "x-public-key-fingerprint":
8 | "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
9 | "x-oaep-hashing-algorithm": "SHA512",
10 | },
11 | body: {
12 | encrypted_payload: {
13 | data: "3097e36bf8b71637a0273abe69c23752d6157464ce49f6f35120d28bedfb63a1f2c8087be3a3bc9775592db41db87a8c",
14 | },
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/test/mock/response.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | request: { url: "/resource" },
3 | body: {
4 | foo: {
5 | elem1: {
6 | encryptedData:
7 | "3097e36bf8b71637a0273abe69c23752d6157464ce49f6f35120d28bedfb63a1f2c8087be3a3bc9775592db41db87a8c",
8 | iv: "22507f596fffb45b15244356981d7ea1",
9 | encryptedKey:
10 | "d4714161898b8bc5c54a63f71ae7c7a40734e4f7c7e27d121ac5e85a3fa47946aa3546027abe0874d751d5ae701491a7f572fc30fa08dd671d358746ffe8709cba36010f97864105b175c51b6f32d36d981287698a3f6f8707aedf980cce19bfe7c5286ddba87b7f3e5abbfa88a980779037c0b7902d340d73201cf3f0b546c2ad9f54e4b71a43504da947a3cb7af54d61717624e636a90069be3c46c19b9ae8b76794321b877544dd03f0ca816288672ef361c3e8f14d4a1ee96ba72d21e3a36c020aa174635a8579b0e9af761d96437e1fa167f00888ff2532292e7a220f5bc948f8159dea2541b8c6df6463213de292b4485076241c90706efad93f9b98ea",
11 | publicKeyFingerprint:
12 | "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
13 | oaepHashingAlgorithm: "SHA512",
14 | },
15 | },
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 - 2021 Mastercard
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/mock/config-header.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | paths: [
3 | {
4 | path: "/resource",
5 | toEncrypt: [
6 | {
7 | element: "",
8 | obj: "encrypted_payload",
9 | },
10 | ],
11 | toDecrypt: [
12 | {
13 | element: "encrypted_payload",
14 | obj: "",
15 | },
16 | ],
17 | },
18 | {
19 | path: "/array-resp$",
20 | toEncrypt: [
21 | {
22 | element: "$",
23 | obj: "$",
24 | },
25 | ],
26 | toDecrypt: [
27 | {
28 | element: "$",
29 | obj: "$",
30 | },
31 | ],
32 | },
33 | ],
34 | oaepPaddingDigestAlgorithm: "SHA-512",
35 |
36 | ivHeaderName: "x-iv",
37 | encryptedKeyHeaderName: "x-encrypted-key",
38 | oaepHashingAlgorithmHeaderName: "x-oaep-hashing-algorithm",
39 | publicKeyFingerprintHeaderName: "x-public-key-fingerprint",
40 | publicKeyFingerprintType: "certificate",
41 | encryptedValueFieldName: "data",
42 | dataEncoding: "hex",
43 | encryptionCertificate: "./test/res/test_certificate.cert",
44 | privateKey: "./test/res/test_key.der",
45 | };
46 |
--------------------------------------------------------------------------------
/test/mock/response-readme.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | request: { url: "/resource" },
3 | body: {
4 | path: {
5 | to: {
6 | encryptedFoo: {
7 | encryptedData:
8 | "9ed1d9e8c449d75296a6f3515f7d65f8650fc4f0737b93d8e376e4cd2a5ad0eb16e43fbdb357ee86da83f1e1c4786628497250c999287758dc3345cfd7309d56c977300b0c68be7486b2d09757b149ef",
9 | encryptedKey:
10 | "8a1fd4f6bdc5fba0807882c8322873e0ac397b2572a2f103ac82d99945e106c27f8ae6e233f7ef943ee558bf361990ffde06ec7b18ec778c542e8b390e0e37ab227b57934803d14180254c6904dfaa04337cdc667f77d11ae740396dad5f3f599572111d6089cd29af3b7334171d1602ec9b36a790e200c11803b8746a2ef9445f612f4fce1fdb87d4d26cc9e38c6a0f1d369356858d86c3382404bb602b7ad0dc08c4f647d3f2c7238063e00acea123a3d2ad2a62291313e915c07aa3598fe77cc49499bef7dcda285d62c117b089a210251be72ce255e49121eec24e054b2b4af2d0a43dc889c593087d5d47f0f7cbe309ee593794337920ee8e5aa21aa970",
11 | iv: "ff570f91a5299c20543a9cf0e9fbb492",
12 | oaepHashingAlgorithm: "SHA512",
13 | publicKeyFingerprint:
14 | "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
15 | },
16 | },
17 | },
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/test/res/test_certificate.cert:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDITCCAgmgAwIBAgIJANLIazc8xI4iMA0GCSqGSIb3DQEBBQUAMCcxJTAjBgNV
3 | BAMMHHd3dy5qZWFuLWFsZXhpcy1hdWZhdXZyZS5jb20wHhcNMTkwMjIxMDg1MTM1
4 | WhcNMjkwMjE4MDg1MTM1WjAnMSUwIwYDVQQDDBx3d3cuamVhbi1hbGV4aXMtYXVm
5 | YXV2cmUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9Mp6gEFp
6 | 9E+/1SS5XrUyYKMbE7eU0dyJCfmJPz8YOkOYV7ohqwXQvjlaP/YazZ6bbmYfa2WC
7 | raOpW0o2BYijHgQ7z2a2Az87rKdAtCpZSKFW82Ijnsw++lx7EABI3tFF282ZV7LT
8 | 13n9m4th5Kldukk9euy+TuJqCvPu4xzE/NE+l4LFMr8rfD47EPQkrun5w/TXwkmJ
9 | rdnG9ejl3BLQO06Ns6Bs516geiYZ7RYxtI8Xnu0ZC0fpqDqjCPZBTORkiFeLocEP
10 | RbTgo1H+0xQFNdsMH1/0F1BI+hvdxlbc3+kHZFZFoeBMkR3jC8jDXOXNCMNWb13T
11 | in6HqPReO0KW8wIDAQABo1AwTjAdBgNVHQ4EFgQUDtqNZacrC6wR53kCpw/BfG2C
12 | t3AwHwYDVR0jBBgwFoAUDtqNZacrC6wR53kCpw/BfG2Ct3AwDAYDVR0TBAUwAwEB
13 | /zANBgkqhkiG9w0BAQUFAAOCAQEAJ09tz2BDzSgNOArYtF4lgRtjViKpV7gHVqtc
14 | 3xQT9ujbaxEgaZFPbf7/zYfWZfJggX9T54NTGqo5AXM0l/fz9AZ0bOm03rnF2I/F
15 | /ewhSlHYzvKiPM+YaswaRo1M1UPPgKpLlRDMO0u5LYiU5ICgCNm13TWgjBlzLpP6
16 | U4z2iBNq/RWBgYxypi/8NMYZ1RcCrAVSt3QnW6Gp+vW/HrE7KIlAp1gFdme3Xcx1
17 | vDRpA+MeeEyrnc4UNIqT/4bHGkKlIMKdcjZgrFfEJVFav3eJ4CZ7ZSV6Bx+9yRCL
18 | DPGlRJLISxgwsOTuUmLOxjotRxO8TdR5e1V+skEtfEctMuSVYA==
19 | -----END CERTIFICATE-----
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a bug report to help us improve
4 | title: "[BUG] Description"
5 | labels: 'Issue: Bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | #### Bug Report Checklist
11 |
12 | - [ ] Have you provided a code sample to reproduce the issue?
13 | - [ ] Have you tested with the latest release to confirm the issue still exists?
14 | - [ ] Have you searched for related issues/PRs?
15 | - [ ] What's the actual output vs expected output?
16 |
17 |
20 |
21 | **Description**
22 | A clear and concise description of what is the question, suggestion, or issue and why this is a problem for you.
23 |
24 | **To Reproduce**
25 | Steps to reproduce the behavior.
26 |
27 | **Expected behavior**
28 | A clear and concise description of what you expected to happen.
29 |
30 | **Screenshots**
31 | If applicable, add screenshots to help explain your problem.
32 |
33 | **Additional context**
34 | Add any other context about the problem here (OS, language version, etc..).
35 |
36 |
37 | **Related issues/PRs**
38 | Has a similar issue/PR been reported/opened before?
39 |
40 | **Suggest a fix/enhancement**
41 | If you can't fix the bug yourself, perhaps you can point to what might be causing the problem (line of code or commit), or simply make a suggestion.
--------------------------------------------------------------------------------
/test/mock/jwe-response.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | request: { url: "/resource" },
3 | body: {
4 | encryptedData : "eyJraWQiOiJnSUVQd1RxREdmenc0dXd5TElLa3d3UzNnc3c4NW5FWFkwUFA2QllNSW5rPSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.bg6UTkMY9omV6CpXZnTsLNDQgKHVSOf7pO4KxhjkQfYSM6I2WBLNL8TMVFuVnOwGnq7jXwBBtszIj0Enk7nmwme2VNKnBEyoe-1t99j1VPX7CVQIdezNnJbwFl746Vi-izt6fatRPHUbp47pvZ7qL8u_xS9Ucv8-1HxuykoVbiHcY1lb-Mm_dzEJf-2eG0fkJxuVJBtUktrO0nu6BC_D53UULTxju6goGcjmgOqrAzf4Yg0NRrzObLchWisbzVcbzO0Lnv0rxDYYIeN54BSKpKowglQ8EElKqLx3rCZ8is6Un2nKqhzsY52-DAZ5-HLSCDCyjEO6CB1w8Y2CXvANZg.B5pVI2hqtwLFuiCNnzonAw.ZDnLiPNy63ocuwhYQFfSneS13Ff5GlFc87kegf8mnAJxGsYS.YFistvxKLMfLGVY5vWV_Dw",
5 | foo: {
6 | elem1: {
7 | encryptedData:
8 | "eyJraWQiOiJnSUVQd1RxREdmenc0dXd5TElLa3d3UzNnc3c4NW5FWFkwUFA2QllNSW5rPSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.bg6UTkMY9omV6CpXZnTsLNDQgKHVSOf7pO4KxhjkQfYSM6I2WBLNL8TMVFuVnOwGnq7jXwBBtszIj0Enk7nmwme2VNKnBEyoe-1t99j1VPX7CVQIdezNnJbwFl746Vi-izt6fatRPHUbp47pvZ7qL8u_xS9Ucv8-1HxuykoVbiHcY1lb-Mm_dzEJf-2eG0fkJxuVJBtUktrO0nu6BC_D53UULTxju6goGcjmgOqrAzf4Yg0NRrzObLchWisbzVcbzO0Lnv0rxDYYIeN54BSKpKowglQ8EElKqLx3rCZ8is6Un2nKqhzsY52-DAZ5-HLSCDCyjEO6CB1w8Y2CXvANZg.B5pVI2hqtwLFuiCNnzonAw.ZDnLiPNy63ocuwhYQFfSneS13Ff5GlFc87kegf8mnAJxGsYS.YFistvxKLMfLGVY5vWV_Dw",
9 | },
10 | },
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | #Idea
64 | *.iml
65 | .idea
66 | .idea/
67 |
68 | #Eslint output folder
69 | target/
70 |
71 | #Test Result
72 | test-results.xml
73 |
74 | #Sonar
75 | .scannerwork
76 | sonar-*
77 | !sonar-project.properties
78 |
--------------------------------------------------------------------------------
/.github/workflows/sonar-scanner.yml:
--------------------------------------------------------------------------------
1 | name: Sonar
2 | 'on':
3 | push:
4 | branches:
5 | - "**"
6 | pull_request_target:
7 | branches:
8 | - "**"
9 | types: [opened, synchronize, reopened, labeled]
10 | schedule:
11 | - cron: 0 16 * * *
12 | workflow_dispatch:
13 | jobs:
14 | sonarcloud:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Check for external PR
19 | if: ${{ !(contains(github.event.pull_request.labels.*.name, 'safe') ||
20 | github.event.pull_request.head.repo.full_name == github.repository ||
21 | github.event_name != 'pull_request_target') }}
22 | run: echo "Unsecure PR, must be labelled with the 'safe' label, then run the workflow again" && exit 1
23 | - name: Use Node.js 17
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: 17
27 | - name: Build with npm
28 | run: |
29 | npm ci
30 | npm run build --if-present
31 | npm test
32 | npm run coverage
33 | - name: SonarCloud
34 | uses: sonarsource/sonarcloud-github-action@master
35 | env:
36 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
37 | SONAR_TOKEN: '${{ secrets.SONAR_TOKEN }}'
38 | with:
39 | args: >
40 | -Dsonar.sources=./lib -Dsonar.tests=./test
41 | -Dsonar.coverage.jacoco.xmlReportPaths=test-results.xml
42 | -Dsonar.javascript.lcov.reportPaths=.nyc_output/coverage.lcov
43 |
--------------------------------------------------------------------------------
/test/mock/jwe-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | paths: [
3 | {
4 | path: "/resource",
5 | toEncrypt: [
6 | {
7 | element: "elem1.encryptedData",
8 | obj: "elem1",
9 | },
10 | ],
11 | toDecrypt: [
12 | {
13 | element: "foo.elem1",
14 | obj: "foo",
15 | },
16 | {
17 | element: "encryptedData",
18 | obj: "decryptedData",
19 | },
20 | ],
21 | },
22 | {
23 | path: "/mappings/*",
24 | toEncrypt: [
25 | {
26 | element: "elem2.encryptedData",
27 | obj: "elem2",
28 | },
29 | ],
30 | toDecrypt: [
31 | {
32 | element: "foo.elem1",
33 | obj: "foo",
34 | },
35 | ],
36 | },
37 | {
38 | path: "/array-resp$",
39 | toEncrypt: [
40 | {
41 | element: "$",
42 | obj: "$",
43 | },
44 | ],
45 | toDecrypt: [
46 | {
47 | element: "$",
48 | obj: "$",
49 | },
50 | ],
51 | },
52 | {
53 | path: "/array-resp2",
54 | toEncrypt: [
55 | {
56 | element: "$",
57 | obj: "$",
58 | },
59 | ],
60 | toDecrypt: [
61 | {
62 | element: "$",
63 | obj: "path.to.foo",
64 | },
65 | ],
66 | },
67 | ],
68 | mode: "JWE",
69 | encryptedValueFieldName: "encryptedData",
70 | publicKeyFingerprintType: "certificate",
71 | dataEncoding: "base64",
72 | encryptionCertificate: "./test/res/test_certificate.cert",
73 | privateKey: "./test/res/test_key.der",
74 | };
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mastercard-client-encryption",
3 | "version": "1.10.7",
4 | "description": "Library for Mastercard API compliant payload encryption/decryption.",
5 | "main": "index.js",
6 | "engines": {
7 | "node": ">=6.12.3"
8 | },
9 | "scripts": {
10 | "build": "webpack",
11 | "test": "mocha && mocha --reporter mocha-sonar-generic-test-coverage > test-results.xml",
12 | "coverage": "nyc mocha && nyc report --reporter=text-lcov > .nyc_output/coverage.lcov",
13 | "lint": "eslint lib --ext .js",
14 | "lint:fix": "eslint lib --ext .js --fix",
15 | "lint:report": "npm run lint -- -f checkstyle --output-file target/checkstyle.xml",
16 | "precommit": "lint-staged"
17 | },
18 | "mocha": {
19 | "timeout": "5000"
20 | },
21 | "lint-staged": {
22 | "linters": {
23 | "*.{js,jsx,json,scss}": [
24 | "prettier --write",
25 | "git add"
26 | ]
27 | }
28 | },
29 | "repository": {
30 | "type": "git",
31 | "url": "git@github.com:Mastercard/client-encryption-nodejs.git"
32 | },
33 | "author": "Mastercard",
34 | "license": "MIT",
35 | "dependencies": {
36 | "node-forge": "^1.3.3"
37 | },
38 | "devDependencies": {
39 | "eslint": "^7.7.0",
40 | "eslint-config-prettier": "^6.11.0",
41 | "eslint-plugin-mocha": "^8.0.0",
42 | "mocha": "^10.7.3",
43 | "mocha-sonar-generic-test-coverage": "^0.0.1",
44 | "nyc": "^15.1.0",
45 | "rewire": "^9.0.0",
46 | "webpack": "^5.93.0",
47 | "webpack-cli": "^4.10.0"
48 | },
49 | "publishConfig": {
50 | "registry": "https://registry.npmjs.org/"
51 | },
52 | "files": [
53 | "lib",
54 | "index.js",
55 | "README.md",
56 | "LICENSE"
57 | ]
58 | }
59 |
--------------------------------------------------------------------------------
/test/res/service.yaml:
--------------------------------------------------------------------------------
1 | swagger: "2.0"
2 | info:
3 | version: 1.0.0
4 | title: Test Service
5 | description: This is a sample Test Service
6 | contact:
7 | name: Tutorial API Team
8 | email: apisupport@mastercard.com
9 | host: api.mastercard.com
10 | basePath: /api
11 | schemes:
12 | - https
13 | consumes:
14 | - application/json
15 | produces:
16 | - application/json
17 | paths:
18 | /merchants:
19 | get:
20 | tags:
21 | - Merchants
22 | description: Returns all merchants
23 | responses:
24 | "200":
25 | description: List of Merchant
26 | schema:
27 | type: array
28 | items:
29 | $ref: "#/definitions/Merchant"
30 | /tests:
31 | post:
32 | tags:
33 | - Tests
34 | description: Just a POST test
35 | consumes:
36 | - application/json
37 | parameters:
38 | - in: body
39 | name: user
40 | description: The user to create.
41 | schema:
42 | type: object
43 | required:
44 | - userName
45 | properties:
46 | userName:
47 | type: string
48 | firstName:
49 | type: string
50 | lastName:
51 | type: string
52 | responses:
53 | "200":
54 | description: OK response
55 | schema:
56 | type: string
57 | example: OK
58 |
59 | definitions:
60 | Merchant:
61 | type: "object"
62 | required:
63 | - "id"
64 | - "name"
65 | properties:
66 | id:
67 | type: "integer"
68 | format: "int64"
69 | name:
70 | type: "string"
71 | example: "Starbucks"
72 |
--------------------------------------------------------------------------------
/test/mock/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | paths: [
3 | {
4 | path: "/resource",
5 | toEncrypt: [
6 | {
7 | element: "elem1.encryptedData",
8 | obj: "elem1",
9 | },
10 | ],
11 | toDecrypt: [
12 | {
13 | element: "foo.elem1",
14 | obj: "foo",
15 | },
16 | ],
17 | },
18 | {
19 | path: "/mappings/*",
20 | toEncrypt: [
21 | {
22 | element: "elem2.encryptedData",
23 | obj: "elem2",
24 | },
25 | ],
26 | toDecrypt: [
27 | {
28 | element: "foo.elem1",
29 | obj: "foo",
30 | },
31 | ],
32 | },
33 | {
34 | path: "/array-resp$",
35 | toEncrypt: [
36 | {
37 | element: "$",
38 | obj: "$",
39 | },
40 | ],
41 | toDecrypt: [
42 | {
43 | element: "$",
44 | obj: "$",
45 | },
46 | ],
47 | },
48 | {
49 | path: "/array-resp2",
50 | toEncrypt: [
51 | {
52 | element: "$",
53 | obj: "$",
54 | },
55 | ],
56 | toDecrypt: [
57 | {
58 | element: "$",
59 | obj: "path.to.foo",
60 | },
61 | ],
62 | },
63 | ],
64 | oaepPaddingDigestAlgorithm: "SHA-512",
65 | ivFieldName: "iv",
66 | encryptedKeyFieldName: "encryptedKey",
67 | encryptedValueFieldName: "encryptedData",
68 | oaepHashingAlgorithmFieldName: "oaepHashingAlgorithm",
69 | publicKeyFingerprintFieldName: "publicKeyFingerprint",
70 | publicKeyFingerprintType: "certificate",
71 | dataEncoding: "hex",
72 | encryptionCertificate: "./test/res/test_certificate.cert",
73 | privateKey: "./test/res/test_key.der",
74 | };
75 |
--------------------------------------------------------------------------------
/test/res/test_key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpAIBAAKCAQEA9Mp6gEFp9E+/1SS5XrUyYKMbE7eU0dyJCfmJPz8YOkOYV7oh
3 | qwXQvjlaP/YazZ6bbmYfa2WCraOpW0o2BYijHgQ7z2a2Az87rKdAtCpZSKFW82Ij
4 | nsw++lx7EABI3tFF282ZV7LT13n9m4th5Kldukk9euy+TuJqCvPu4xzE/NE+l4LF
5 | Mr8rfD47EPQkrun5w/TXwkmJrdnG9ejl3BLQO06Ns6Bs516geiYZ7RYxtI8Xnu0Z
6 | C0fpqDqjCPZBTORkiFeLocEPRbTgo1H+0xQFNdsMH1/0F1BI+hvdxlbc3+kHZFZF
7 | oeBMkR3jC8jDXOXNCMNWb13Tin6HqPReO0KW8wIDAQABAoIBACt3TJs7gkncY07A
8 | j53OhNx9+c/deDJve3Hm+kTdujqWSBXDuji49qDgZDMZSxcZw7WtsoSKaNLWTbsl
9 | CUSgR36L9+bS8RzQfJMiu62CG4p3A46B7ZtD3ybrhvqHYmkWNdZawpC88zq+sZaO
10 | AFru6/KpEJ6kLQKq13OMbu2q7RuK3zCLpxp441kBs6L7XstSYGXLTvezFOFr8NfL
11 | 4GASOiTOHhyZGcYBIVUG5QUymbODn4lpOjER3IYL6p1roMJdtdxYAs8MvXJ8sUz9
12 | d4vUCmhcy9vU2Jpjem0VgR/ec+NTPCAGtAhmoa0iY7nWQhcLBYeNd2Fr/6XanZCh
13 | 4NbKrHkCgYEA+uNUWEJDJ+BvbzosAf4rOTNDgIYVHVh9ULQfL74WOSEk4xTKoCCF
14 | ULxpqgw5PEKhTDnxRai7ZwKxp/B6ypgVzKUYoEuGnv0IBxM9+P0RYzQ0Pc8SH/8q
15 | AkR+T5pzz1OsfI924/7yTEUAOv8p4KHGvvar4t0d9gLQuQ1BC58MszcCgYEA+cdY
16 | hFfmpW8gjTci6E/3+i0h9TVXDvtCDryGgFTLX8voRBF8+4s79/wIHKIFWc0QEUit
17 | KrEPuFkDekNlhuXEGEsgfdj0flYMIZ2oFbwPkBzQy/fPMaDIQea9g9iFUkbkwdhD
18 | oOy4lh1ByBMWsVZGwAzgw4+2FdmkAiVb9IGw0CUCgYEAmgAYseRanIujWz717HM7
19 | zOyurqGfLFg48+Tcj826jm7N2aXVitzreFdu9LZ0G406vTOD6iJchiqdKlzuwpUA
20 | LJHav+ocRFNFLjKdg8yzc5WDy7zjf0h9XM72SZ6hH85YvkzBycmgqThhn9Uou34S
21 | JP39HFBmJ7AqtqxwFNYYUZkCgYBPwSU0bNTKsicUsCxHPXGSwmJ7Z2K69+NpzSyt
22 | QWYG2pb5VRQxRY4KasE0U0+eEuo0ep5AaXT5igKgQXDjl+37S9G+HU5EILmS6kJH
23 | XlshyvGojyHrWMlYsZKFzNcVJGnas3E0qyFtXT4p4l52lXPV0sbZ6sNbSrkhrkgk
24 | VFzeuQKBgQDfwJk9UuDwEKUlrnwvDVCXsvEtG1EVDTY3edjEO6VsxqtkAtNPnQS5
25 | Ez49yjhmKV8PUnGT28TrYQRcxzsREVqNegBcNznkbZ+FQdUCfV76AP6M+JRISSm8
26 | FH7hRhr0qWL4N72XDhxds5b3FLuwtLm/1oTo1fY2b1OQdhL/MdZ3mw==
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/lib/mcapi/mcapi-service.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @class
3 | * MC-Api Service
4 | *
5 | * Decorator class for OpenAPI client service
6 | *
7 | * Act as HTTP Interceptor for the OpenAPI `superagent` HTTP client adding encryption/decryption capabilities for the request/response payload.
8 | *
9 | * @param service OpenAPI client service (it can be generated by the swagger code generator)
10 | * @param config Configuration object describing which field to enable encryption/decryption
11 | *
12 | * @module mcapi/Service
13 | * @version 1.0.0
14 | */
15 | function Service(service, config) {
16 | const FieldLevelEncryption = require("./encryption/field-level-encryption");
17 | const JweEncryption = require("./encryption/jwe-encryption");
18 |
19 | this.encryption =
20 | config.mode === "JWE"
21 | ? new JweEncryption(config)
22 | : new FieldLevelEncryption(config);
23 |
24 | function override(object, methodName, callback, outObj) {
25 | object[methodName] = callback(object[methodName], outObj);
26 | }
27 |
28 | if (!service || !service.ApiClient || !service.ApiClient.instance) {
29 | throw new Error("service should be a valid OpenAPI client.");
30 | }
31 |
32 | override(
33 | service.ApiClient.instance,
34 | "callApi",
35 | function (original, outObj) {
36 | return function () {
37 | const endpoint = arguments[0];
38 | const header = arguments[arguments.length - 9];
39 | const body = arguments[arguments.length - 7];
40 | const cb = arguments[arguments.length - 1];
41 | if (body) {
42 | const encrypted = outObj.encryption.encrypt(endpoint, header, body);
43 | arguments[arguments.length - 9] = encrypted.header;
44 | arguments[arguments.length - 7] = encrypted.body;
45 | }
46 | arguments[arguments.length - 1] = function (error, data, response) {
47 | if (response && response.body) {
48 | const decryptedPayload = outObj.encryption.decrypt(response);
49 | response.body = decryptedPayload;
50 | data = decryptedPayload;
51 | }
52 | cb(error, data, response);
53 | };
54 | return original.apply(this, arguments);
55 | };
56 | },
57 | this
58 | );
59 |
60 | return service;
61 | }
62 |
63 | module.exports = Service;
64 | module.exports.FieldLevelEncryption = require("./encryption/field-level-encryption");
65 | module.exports.JweEncryption = require("./encryption/jwe-encryption");
66 |
--------------------------------------------------------------------------------
/test/jwe-encryption.test.js:
--------------------------------------------------------------------------------
1 | const assert = require("assert");
2 | const rewire = require("rewire");
3 | const JweEncryption = rewire("../lib/mcapi/encryption/jwe-encryption");
4 | const utils = require("../lib/mcapi/utils/utils");
5 |
6 | const testConfig = require("./mock/jwe-config");
7 |
8 | describe("JWE Encryption", () => {
9 | before(function () {
10 | if (!utils.nodeVersionSupportsJWE()) {
11 | this.skip();
12 | }
13 | });
14 |
15 | describe("#new JWEEncryption", () => {
16 | it("when valid config", () => {
17 | const encryption = new JweEncryption(testConfig);
18 | assert.ok(encryption.crypto);
19 | });
20 | });
21 |
22 | describe("#encrypt", () => {
23 | it("encrypt body payload", () => {
24 | const encryption = new JweEncryption(testConfig);
25 | const encrypt = JweEncryption.__get__("encrypt");
26 | const res = encrypt.call(encryption, "/resource", null, {
27 | elem1: {
28 | encryptedData: {
29 | accountNumber: "5123456789012345",
30 | },
31 | shouldBeThere: "shouldBeThere",
32 | },
33 | });
34 | assert.ok(res.header === null);
35 | assert.ok(res.body.elem1.shouldBeThere);
36 | assert.ok(res.body.elem1.encryptedData);
37 | assert.ok(!res.body.elem1.encryptedData.accountNumber);
38 | });
39 | });
40 |
41 | describe("#decrypt", () => {
42 | it("decrypt response", () => {
43 | const encryption = new JweEncryption(testConfig);
44 | const decrypt = JweEncryption.__get__("decrypt");
45 | const response = require("./mock/jwe-response");
46 | const res = decrypt.call(encryption, response);
47 | assert.ok(res.foo.accountNumber === "5123456789012345");
48 | assert.ok(!res.foo.elem1);
49 | assert.ok(
50 | !Object.prototype.hasOwnProperty.call(res.foo, "encryptedData")
51 | );
52 | });
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 | });
62 | });
63 |
64 | describe("#import JweEncryption", () => {
65 | it("from mcapi-service", () => {
66 | const JweEncryption = require("..").JweEncryption;
67 | assert.ok(JweEncryption);
68 | const jwe = new JweEncryption(testConfig);
69 | assert.ok(jwe.crypto);
70 | });
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/lib/mcapi/encryption/jwe-encryption.js:
--------------------------------------------------------------------------------
1 | const JweCrypto = require("../crypto/jwe-crypto");
2 | const utils = require("../utils/utils");
3 |
4 | /**
5 | * @class
6 | * Performs JWE encryption on HTTP payloads.
7 | *
8 | * @module encryption/JweEncryption
9 | * @version 1.0.0
10 | */
11 | function JweEncryption(config) {
12 | this.encrypt = encrypt;
13 | this.decrypt = decrypt;
14 | this.config = config;
15 | this.crypto = new JweCrypto(config);
16 | }
17 |
18 | /**
19 | * Encrypt parts of a HTTP request using the given config
20 | *
21 | * @param endpoint HTTP URL for the current call
22 | * @param header HTTP Header
23 | * @param body HTTP Body
24 | * @returns {{header: *, body: *}}
25 | */
26 | function encrypt(endpoint, header, body) {
27 | let bodyMap = body;
28 | const fleConfig = utils.hasConfig(this.config, endpoint);
29 | if (fleConfig) {
30 | bodyMap = fleConfig.toEncrypt.map((v) => {
31 | return encryptBody.call(this, v, body);
32 | });
33 | }
34 | return {
35 | header: header,
36 | body: fleConfig ? utils.computeBody(fleConfig.toEncrypt, body, bodyMap) : body,
37 | };
38 | }
39 |
40 | /**
41 | * Decrypt part of the HTTP response using the given config
42 | *
43 | * @public
44 | * @param response HTTP response to decrypt
45 | * @returns {*}
46 | */
47 | function decrypt(response) {
48 | const body = response.body;
49 | let bodyMap = response.body;
50 | const fleConfig = utils.hasConfig(this.config, response.request.url);
51 | if (fleConfig) {
52 | bodyMap = fleConfig.toDecrypt.map((v) => {
53 | return decryptBody.call(this, v, body);
54 | });
55 | }
56 | return fleConfig ? utils.computeBody(fleConfig.toDecrypt, body, bodyMap) : body;
57 | }
58 |
59 | /**
60 | * Encrypt body nodes with given path
61 | *
62 | * @private
63 | * @param path Config json path
64 | * @param body Body to encrypt
65 | */
66 | function encryptBody(path, body) {
67 | const elem = utils.elemFromPath(path.element, body);
68 | if (elem && elem.node) {
69 | const encryptedData = this.crypto.encryptData({ data: elem.node });
70 | body = utils.addEncryptedDataToBody(encryptedData, path, this.config.encryptedValueFieldName, body);
71 | }
72 | return body;
73 | }
74 |
75 | /**
76 | * Decrypt body nodes with given path
77 | *
78 | * @private
79 | * @param path Config json path
80 | * @param body encrypted body
81 | * @returns {Object} Decrypted body
82 | */
83 | function decryptBody(path, body) {
84 | const elem = utils.elemFromPath(path.element, body);
85 | if (elem && elem.node) {
86 | const encryptedValue = (path.element === this.config.encryptedValueFieldName) ?
87 | elem.node : elem.node[this.config.encryptedValueFieldName];
88 | const decryptedObj = this.crypto.decryptData(encryptedValue);
89 | return utils.mutateObjectProperty(
90 | path.obj,
91 | decryptedObj,
92 | body,
93 | path.element,
94 | []
95 | );
96 | }
97 | return body;
98 | }
99 |
100 | module.exports = JweEncryption;
101 |
--------------------------------------------------------------------------------
/test/mcapi-service.test.js:
--------------------------------------------------------------------------------
1 | const assert = require("assert");
2 | const MCService = require("../lib/mcapi/mcapi-service");
3 | const testConfig = require("./mock/config");
4 |
5 | describe("MC API Service", () => {
6 | describe("#interceptor", () => {
7 | it("when null", () => {
8 | assert.throws(() => {
9 | new MCService(null, testConfig);
10 | }, /service should be a valid OpenAPI client./);
11 | });
12 |
13 | it("with not right openapi client", () => {
14 | assert.throws(() => {
15 | new MCService({}, testConfig);
16 | }, /service should be a valid OpenAPI client./);
17 | assert.throws(() => {
18 | new MCService({ ApiClient: {} }, testConfig);
19 | }, /service should be a valid OpenAPI client./);
20 | });
21 |
22 | it("callApi intercepted for ApiClient with collectionQueryParams", function (done) {
23 | const postBody = {
24 | elem1: {
25 | encryptedData: {
26 | accountNumber: "5123456789012345",
27 | },
28 | },
29 | };
30 | const service = {
31 | ApiClient: {
32 | instance: {
33 | callApi: function () {
34 | arguments[arguments.length - 1](null, arguments[7], {
35 | body: arguments[7],
36 | request: { url: "/resource" },
37 | });
38 | },
39 | },
40 | },
41 | };
42 | const mcService = new MCService(service, testConfig);
43 | // simulate callApi call from client
44 | service.ApiClient.instance.callApi.call(
45 | mcService,
46 | "/resource",
47 | "POST",
48 | null,
49 | null,
50 | null,
51 | { test: "header" },
52 | null,
53 | postBody,
54 | null,
55 | null,
56 | null,
57 | null,
58 | null,
59 | function cb(error, data) {
60 | assert.ok(data.elem1.encryptedData);
61 | assert.ok(data.elem1.encryptedKey);
62 | assert.ok(data.elem1.publicKeyFingerprint);
63 | assert.ok(data.elem1.oaepHashingAlgorithm);
64 | done();
65 | }
66 | );
67 | });
68 |
69 | it("callApi intercepted for ApiClient without collectionQueryParams", function (done) {
70 | const postBody = {
71 | elem1: {
72 | encryptedData: {
73 | accountNumber: "5123456789012345",
74 | },
75 | },
76 | };
77 | const service = {
78 | ApiClient: {
79 | instance: {
80 | callApi: function () {
81 | arguments[arguments.length - 1](null, arguments[6], {
82 | body: arguments[6],
83 | request: { url: "/resource" },
84 | });
85 | },
86 | },
87 | },
88 | };
89 | const mcService = new MCService(service, testConfig);
90 | // simulate callApi call from client
91 | service.ApiClient.instance.callApi.call(
92 | mcService,
93 | "/resource",
94 | "POST",
95 | null,
96 | null,
97 | { test: "header" },
98 | null,
99 | postBody,
100 | null,
101 | null,
102 | null,
103 | null,
104 | null,
105 | function cb(error, data) {
106 | assert.ok(data.elem1.encryptedData);
107 | assert.ok(data.elem1.encryptedKey);
108 | assert.ok(data.elem1.publicKeyFingerprint);
109 | assert.ok(data.elem1.oaepHashingAlgorithm);
110 | done();
111 | }
112 | );
113 | });
114 |
115 | it("callApi intercepted, without body", function (done) {
116 | const service = {
117 | ApiClient: {
118 | instance: {
119 | callApi: function () {
120 | arguments[arguments.length - 1](null, arguments[7], {
121 | body: arguments[7],
122 | request: { url: "/resource" },
123 | });
124 | },
125 | },
126 | },
127 | };
128 | const mcService = new MCService(service, testConfig);
129 | // simulate callApi call from client
130 | service.ApiClient.instance.callApi.call(
131 | mcService,
132 | "/resource",
133 | "POST",
134 | null,
135 | null,
136 | null,
137 | { test: "header" },
138 | null,
139 | null,
140 | function cb(error, data) {
141 | assert.ok(!data);
142 | done();
143 | }
144 | );
145 | });
146 | });
147 | });
148 |
--------------------------------------------------------------------------------
/lib/mcapi/encryption/field-level-encryption.js:
--------------------------------------------------------------------------------
1 | const FieldLevelCrypto = require("../crypto/field-level-crypto");
2 | const utils = require("../utils/utils");
3 |
4 | /**
5 | * @class
6 | * Performs field level encryption on HTTP payloads.
7 | *
8 | * @module encryption/FieldLevelEncryption
9 | * @version 1.0.0
10 | */
11 | function FieldLevelEncryption(config) {
12 | this.encrypt = encrypt;
13 | this.decrypt = decrypt;
14 | this.config = config;
15 | this.crypto = new FieldLevelCrypto(config);
16 | this.isWithHeader =
17 | Object.prototype.hasOwnProperty.call(config, "ivHeaderName") &&
18 | Object.prototype.hasOwnProperty.call(config, "encryptedKeyHeaderName");
19 | this.encryptionResponseProperties = [
20 | this.config.ivFieldName,
21 | this.config.encryptedKeyFieldName,
22 | this.config.publicKeyFingerprintFieldName,
23 | this.config.oaepHashingAlgorithmFieldName,
24 | ];
25 | }
26 |
27 | /**
28 | * Set encryption header parameters
29 | *
30 | * @param header HTTP header
31 | * @param params Encryption parameters
32 | */
33 | function setHeader(header, params) {
34 | header[this.config.encryptedKeyHeaderName] = params.encoded.encryptedKey;
35 | header[this.config.ivHeaderName] = params.encoded.iv;
36 | header[this.config.oaepHashingAlgorithmHeaderName] =
37 | params.oaepHashingAlgorithm.replace("-", "");
38 | header[this.config.publicKeyFingerprintHeaderName] =
39 | params.publicKeyFingerprint;
40 | }
41 |
42 | /**
43 | * Encrypt parts of a HTTP request using the given config
44 | *
45 | * @param endpoint HTTP URL for the current call
46 | * @param header HTTP Header
47 | * @param body HTTP Body
48 | * @returns {{header: *, body: *}}
49 | */
50 | function encrypt(endpoint, header, body) {
51 | let bodyMap = body;
52 | const fleConfig = utils.hasConfig(this.config, endpoint);
53 | if (fleConfig) {
54 | if (!this.isWithHeader) {
55 | bodyMap = fleConfig.toEncrypt.map((v) => {
56 | return encryptBody.call(this, v, body);
57 | });
58 | } else {
59 | const encParams = this.crypto.newEncryptionParams({});
60 | bodyMap = fleConfig.toEncrypt.map((v) => {
61 | return encryptWithHeader.call(this, encParams, v, body);
62 | });
63 | setHeader.call(this, header, encParams);
64 | }
65 | }
66 | return {
67 | header: header,
68 | body: fleConfig ? utils.computeBody(fleConfig.toEncrypt, body, bodyMap) : body,
69 | };
70 | }
71 |
72 | /**
73 | * Decrypt part of the HTTP response using the given config
74 | *
75 | * @public
76 | * @param response HTTP response to decrypt
77 | * @returns {*}
78 | */
79 | function decrypt(response) {
80 | const body = response.body;
81 | let bodyMap = response.body;
82 | const fleConfig = utils.hasConfig(this.config, response.request.url);
83 | if (fleConfig) {
84 | bodyMap = fleConfig.toDecrypt.map((v) => {
85 | if (!this.isWithHeader) {
86 | return decryptBody.call(this, v, body);
87 | } else {
88 | return decryptWithHeader.call(this, v, body, response);
89 | }
90 | });
91 | }
92 | return fleConfig ? utils.computeBody(fleConfig.toDecrypt, body, bodyMap) : body;
93 | }
94 |
95 | /**
96 | * Encrypt body nodes with given path
97 | *
98 | * @private
99 | * @param path Config json path
100 | * @param body Body to encrypt
101 | */
102 | function encryptBody(path, body) {
103 | const elem = utils.elemFromPath(path.element, body);
104 | if (elem && elem.node) {
105 | const encryptedData = this.crypto.encryptData({ data: elem.node });
106 | body = utils.addEncryptedDataToBody(encryptedData, path, this.config.encryptedValueFieldName, body);
107 | }
108 | return body;
109 | }
110 |
111 | /**
112 | * Encrypt body nodes with given path, without setting crypto info in the body
113 | *
114 | * @private
115 | * @param encParams encoding params to use
116 | * @param path Config json path
117 | * @param body Body to encrypt
118 | * @returns {Object} Encrypted body
119 | */
120 | function encryptWithHeader(encParams, path, body) {
121 | const elem = utils.elemFromPath(path.element, body).node;
122 | const encrypted = this.crypto.encryptData({ data: elem }, encParams);
123 | const data = {
124 | [this.config.encryptedValueFieldName]:
125 | encrypted[this.config.encryptedValueFieldName],
126 | };
127 | return utils.isJsonRoot(path.obj) ? data : { [path.obj]: data };
128 | }
129 |
130 | /**
131 | * Decrypt body nodes with given path
132 | *
133 | * @private
134 | * @param path Config json path
135 | * @param body encrypted body
136 | * @returns {Object} Decrypted body
137 | */
138 | function decryptBody(path, body) {
139 | const elem = utils.elemFromPath(path.element, body);
140 | if (elem && elem.node) {
141 | const decryptedObj = this.crypto.decryptData(
142 | elem.node[this.config.encryptedValueFieldName], // encrypted data
143 | elem.node[this.config.ivFieldName], // iv field
144 | elem.node[this.config.oaepHashingAlgorithmFieldName], // oaepHashingAlgorithm
145 | elem.node[this.config.encryptedKeyFieldName] // encryptedKey
146 | );
147 | return utils.mutateObjectProperty(
148 | path.obj,
149 | decryptedObj,
150 | body,
151 | path.element,
152 | this.encryptionResponseProperties
153 | );
154 | }
155 | return body;
156 | }
157 |
158 | /**
159 | * Decrypt body nodes with given path, getting crypto info from the header
160 | *
161 | * @private
162 | * @param path Config json path
163 | * @param body Encrypted body
164 | * @param response Response with header to update
165 | */
166 | function decryptWithHeader(path, body, response) {
167 | const elemEncryptedNode = utils.elemFromPath(path.obj, body);
168 | const node = utils.isJsonRoot(path.element)
169 | ? elemEncryptedNode.node
170 | : elemEncryptedNode.node[path.element];
171 | if (node) {
172 | const encryptedData = node[this.config.encryptedValueFieldName];
173 | for (const k in body) {
174 | // noinspection JSUnfilteredForInLoop
175 | delete body[k];
176 | }
177 | const decrypted = this.crypto.decryptData(
178 | encryptedData,
179 | response.header[this.config.ivHeaderName],
180 | response.header[this.config.oaepHashingAlgorithmHeaderName],
181 | response.header[this.config.encryptedKeyHeaderName]
182 | );
183 | return utils.isJsonRoot(path.obj) ? decrypted : Object.assign(body, decrypted);
184 | }
185 | }
186 |
187 | module.exports = FieldLevelEncryption;
188 |
--------------------------------------------------------------------------------
/lib/mcapi/crypto/field-level-crypto.js:
--------------------------------------------------------------------------------
1 | const forge = require("node-forge");
2 | const utils = require("../utils/utils");
3 | const c = require("../utils/constants");
4 |
5 | /**
6 | * @class Field Level Crypto
7 | *
8 | * Constructor to create an instance of `FieldLevelCrypto`: provide encrypt/decrypt methods
9 | *
10 | * @param config encryption/decryption service configuration
11 | * @constructor
12 | */
13 | function FieldLevelCrypto(config) {
14 | isValidConfig.call(this, config);
15 |
16 | //Load public/private keys from text
17 | if(config.useCertificateContent){
18 | this.encryptionCertificate = utils.readPublicCertificateContent(config);
19 | this.privateKey = utils.getPrivateKeyFromContent(config);
20 | }
21 | // Load public certificate (for encryption)
22 | else{
23 | this.encryptionCertificate = utils.readPublicCertificate(
24 | config.encryptionCertificate
25 | );
26 |
27 | // Load private key (for decryption)
28 | this.privateKey = utils.getPrivateKey(config);
29 | }
30 | this.encoding = config.dataEncoding;
31 |
32 | this.publicKeyFingerprint =
33 | config.publicKeyFingerprint ||
34 | utils.computePublicFingerprint(
35 | config,
36 | this.encryptionCertificate,
37 | this.encoding
38 | );
39 |
40 | this.publicKeyFingerprintFieldName = config.publicKeyFingerprintFieldName;
41 |
42 | this.oaepHashingAlgorithm = config.oaepPaddingDigestAlgorithm;
43 |
44 | this.encryptedValueFieldName = config.encryptedValueFieldName;
45 |
46 | this.encryptedKeyFieldName = config.encryptedKeyFieldName;
47 |
48 | this.oaepHashingAlgorithmFieldName = config.oaepHashingAlgorithmFieldName;
49 |
50 | this.ivFieldName = config.ivFieldName || 'iv';
51 |
52 | /**
53 | * Perform data encryption
54 | *
55 | * @public
56 | * @param options {Object} parameters
57 | * @param options.data {string} json string to encrypt
58 | * @param options.iv (optional) byte string IV to use or generate a random one
59 | * @param options.secretKey (optional) byte string secretKey to use or generate a random one
60 | * @param encryptionParams encryption parameters
61 | */
62 | this.encryptData = function (options, encryptionParams) {
63 | const data = utils.jsonToString(options.data);
64 |
65 | // Generate encryption params
66 | const enc = encryptionParams || this.newEncryptionParams(options);
67 |
68 | // Create Cipher
69 | const cipher = forge.cipher.createCipher("AES-CBC", enc.secretKey);
70 |
71 | // Encrypt payload
72 | cipher.start({ iv: enc.iv });
73 | cipher.update(forge.util.createBuffer(data, c.UTF8));
74 | cipher.finish();
75 | const encrypted = cipher.output.getBytes();
76 |
77 | const encryptedData = {
78 | [this.encryptedValueFieldName]: utils.bytesToString(
79 | encrypted,
80 | this.encoding
81 | ),
82 | [this.ivFieldName]: utils.bytesToString(enc.iv, this.encoding),
83 | [this.encryptedKeyFieldName]: utils.bytesToString(
84 | enc.encryptedKey,
85 | this.encoding
86 | ),
87 | };
88 | if (this.publicKeyFingerprint) {
89 | encryptedData[this.publicKeyFingerprintFieldName] =
90 | this.publicKeyFingerprint;
91 | }
92 | if (this.oaepHashingAlgorithmFieldName) {
93 | encryptedData[this.oaepHashingAlgorithmFieldName] =
94 | this.oaepHashingAlgorithm.replace("-", "");
95 | }
96 | return encryptedData;
97 | };
98 |
99 | /**
100 | * Perform data decryption
101 | *
102 | * @public
103 | * @param encryptedData encrypted data
104 | * @param iv Initialization vector to use to create the Decipher
105 | * @param oaepHashingAlgorithm OAEP Algorithm to use
106 | * @param encryptedKey Encrypted key
107 | * @returns {Object} Decrypted Json object
108 | */
109 | this.decryptData = function (
110 | encryptedData,
111 | iv,
112 | oaepHashingAlgorithm,
113 | encryptedKey
114 | ) {
115 | const decryptedKey = this.privateKey.decrypt(
116 | utils.stringToBytes(encryptedKey, this.encoding),
117 | "RSA-OAEP",
118 | createOAEPOptions("RSA-OAEP", oaepHashingAlgorithm)
119 | );
120 |
121 | // Init decipher
122 | const decipher = forge.cipher.createDecipher("AES-CBC", decryptedKey);
123 |
124 | // Decrypt payload
125 | decipher.start({ iv: utils.stringToBytes(iv, this.encoding) });
126 | decipher.update(
127 | forge.util.createBuffer(
128 | utils.stringToBytes(encryptedData, this.encoding),
129 | c.BINARY
130 | )
131 | );
132 | decipher.finish();
133 |
134 | try {
135 | return JSON.parse(decipher.output.data);
136 | } catch (e) {
137 | return decipher.output.data;
138 | }
139 | };
140 |
141 | /**
142 | * Generate encryption parameters.
143 | * @public
144 | * @param options.iv IV to use instead to use a random IV
145 | * @param options.secretKey Secret Key to use instead to generate a random key
146 | * @returns {{secretKey: *, encryptedKey: *, iv: *}}
147 | */
148 | this.newEncryptionParams = function (options) {
149 | options = options || {};
150 | // Generate a random initialization vector (IV)
151 | const iv = options["iv"] || forge.random.getBytesSync(16);
152 |
153 | // Generate a secret key (should be 128 (or 256) bits)
154 | const secretKey =
155 | options["secretKey"] || generateSecretKey("AES", 128, "SHA-256");
156 |
157 | // Encrypt secret key with issuer key
158 | const encryptedKey = this.encryptionCertificate.publicKey.encrypt(
159 | secretKey,
160 | "RSA-OAEP",
161 | createOAEPOptions("RSA-OAEP", this.oaepHashingAlgorithm)
162 | );
163 |
164 | return {
165 | iv: iv,
166 | secretKey: secretKey,
167 | encryptedKey: encryptedKey,
168 | oaepHashingAlgorithm: this.oaepHashingAlgorithm,
169 | publicKeyFingerprint: this.publicKeyFingerprint,
170 | encoded: {
171 | iv: utils.bytesToString(iv, this.encoding),
172 | secretKey: utils.bytesToString(secretKey, this.encoding),
173 | encryptedKey: utils.bytesToString(encryptedKey, this.encoding),
174 | },
175 | };
176 | };
177 | }
178 |
179 | /**
180 | * @private
181 | */
182 | function createOAEPOptions(asymmetricCipher, oaepHashingAlgorithm) {
183 | if (asymmetricCipher.includes("OAEP") && oaepHashingAlgorithm !== null) {
184 | const mdForOaep = createMessageDigest(oaepHashingAlgorithm);
185 | return {
186 | md: mdForOaep,
187 | mgf1: {
188 | md: mdForOaep,
189 | },
190 | };
191 | }
192 | }
193 |
194 | /**
195 | * @private
196 | */
197 | function createMessageDigest(digest) {
198 | switch (digest.toUpperCase()) {
199 | case "SHA-256":
200 | case "SHA256":
201 | return forge.md.sha256.create();
202 | case "SHA-512":
203 | case "SHA512":
204 | return forge.md.sha512.create();
205 | }
206 | }
207 |
208 | /**
209 | * @private
210 | */
211 | function generateSecretKey(algorithm, size, digest) {
212 | if (algorithm === "AES") {
213 | const key = forge.random.getBytesSync(size / 8);
214 | const md = createMessageDigest(digest);
215 | md.update(key);
216 | return md.digest().getBytes(16);
217 | }
218 | throw new Error("Unsupported symmetric algorithm");
219 | }
220 |
221 | /**
222 | * Check if the passed configuration is valid
223 | * @throws {Error} if the config is not valid
224 | * @private
225 | */
226 | function isValidConfig(config) {
227 | const propertiesBasic = [
228 | "oaepPaddingDigestAlgorithm",
229 | "dataEncoding",
230 | "encryptionCertificate",
231 | "encryptedValueFieldName",
232 | ];
233 | const propertiesField = ["ivFieldName", "encryptedKeyFieldName"];
234 | const propertiesHeader = [
235 | "ivHeaderName",
236 | "encryptedKeyHeaderName",
237 | "oaepHashingAlgorithmHeaderName",
238 | ];
239 | const contains = utils.checkConfigFieldsArePopulated(config, propertiesBasic, propertiesField, propertiesHeader);
240 | if (config["dataEncoding"] !== c.HEX && config["dataEncoding"] !== c.BASE64) {
241 | throw Error("Config not valid: dataEncoding should be 'hex' or 'base64'");
242 | }
243 | validateFingerprint(config, contains);
244 | utils.validateRootMapping(config);
245 | }
246 |
247 | function validateFingerprint(config, contains) {
248 | const propertiesFingerprint = [
249 | "publicKeyFingerprintType",
250 | "publicKeyFingerprintFieldName",
251 | "publicKeyFingerprintHeaderName",
252 | ];
253 | const propertiesOptionalFingerprint = ["publicKeyFingerprint"];
254 | if (
255 | !contains(propertiesOptionalFingerprint) &&
256 | (config[propertiesFingerprint[1]] || config[propertiesFingerprint[2]]) &&
257 | config[propertiesFingerprint[0]] !== "certificate" &&
258 | config[propertiesFingerprint[0]] !== "publicKey"
259 | ) {
260 | throw Error(
261 | `Config not valid: ${propertiesFingerprint[0]} should be: 'certificate' or 'publicKey'`
262 | );
263 | }
264 | }
265 |
266 | module.exports = FieldLevelCrypto;
267 |
--------------------------------------------------------------------------------
/lib/mcapi/crypto/jwe-crypto.js:
--------------------------------------------------------------------------------
1 | const forge = require("node-forge");
2 | const utils = require("../utils/utils");
3 | const c = require("../utils/constants");
4 | const nodeCrypto = require("crypto");
5 |
6 | /**
7 | * @class JWE Crypto
8 | *
9 | * Constructor to create an instance of `JweCrypto`: provide JWE encrypt/decrypt methods
10 | *
11 | * @param config encryption/decryption service configuration
12 | * @constructor
13 | */
14 | function JweCrypto(config) {
15 | isValidConfig.call(this, config);
16 |
17 | this.encryptionCertificate = readPublicCertificate(
18 | config.encryptionCertificate
19 | );
20 |
21 | this.privateKey = getPrivateKey(config);
22 |
23 | this.publicKeyFingerprint =
24 | config.publicKeyFingerprint ||
25 | computePublicFingerprint(config, this.encryptionCertificate);
26 |
27 | this.encryptedValueFieldName = config.encryptedValueFieldName;
28 |
29 | /**
30 | * Perform data encryption
31 | *
32 | * @public
33 | * @param options {Object} parameters
34 | * @param options.data {string} json string to encrypt
35 | */
36 | this.encryptData = function (options) {
37 | const data = utils.jsonToString(options.data);
38 |
39 | const jweHeader = {
40 | kid: this.publicKeyFingerprint,
41 | cty: "application/json",
42 | alg: "RSA-OAEP-256",
43 | enc: "A256GCM",
44 | };
45 |
46 | const encodedJweHeader = toEncodedString(
47 | JSON.stringify(jweHeader),
48 | c.UTF8,
49 | c.BASE64URL
50 | );
51 |
52 | const secretKey = nodeCrypto.randomBytes(32);
53 | const secretKeyBuffer = Buffer.from(secretKey, c.BINARY);
54 |
55 | const encryptedSecretKey = nodeCrypto.publicEncrypt(
56 | {
57 | key: this.encryptionCertificate,
58 | padding: nodeCrypto.constants.RSA_PKCS1_OAEP_PADDING,
59 | oaepHash: "sha256",
60 | },
61 | secretKeyBuffer
62 | );
63 |
64 | const iv = nodeCrypto.randomBytes(16);
65 |
66 | const cipher = nodeCrypto.createCipheriv("AES-256-GCM", secretKey, iv);
67 | cipher.setAAD(Buffer.from(encodedJweHeader, c.ASCII));
68 | let cipherText = cipher.update(data, c.UTF8, c.BASE64);
69 | cipherText += cipher.final(c.BASE64);
70 | const authTag = cipher.getAuthTag().toString(c.BASE64);
71 |
72 | const encodedEncryptedSecretKey = toEncodedString(
73 | encryptedSecretKey,
74 | c.BINARY,
75 | c.BASE64URL
76 | );
77 | const encodedIv = toEncodedString(iv, c.BINARY, c.BASE64URL);
78 | const encodedEncryptedText = toEncodedString(
79 | cipherText,
80 | c.BASE64,
81 | c.BASE64URL
82 | );
83 | const encodedAuthTag = toEncodedString(authTag, c.BASE64, c.BASE64URL);
84 |
85 | const encryptedData = serialize(
86 | encodedJweHeader,
87 | encodedEncryptedSecretKey,
88 | encodedIv,
89 | encodedEncryptedText,
90 | encodedAuthTag
91 | );
92 | return { [this.encryptedValueFieldName]: encryptedData };
93 | };
94 |
95 | /**
96 | * Perform data decryption
97 | *
98 | * @public
99 | * @param encryptedData encrypted data
100 | */
101 | this.decryptData = function (encryptedData) {
102 | if (!encryptedData || encryptedData.length <= 1) {
103 | throw new Error("Input not valid");
104 | }
105 |
106 | const jweTokenParts = deSerialize(encryptedData);
107 | const jweHeader = Buffer.from(jweTokenParts[0], c.BASE64).toString(c.UTF8);
108 | const encryptedSecretKey = Buffer.from(jweTokenParts[1], c.BASE64);
109 | const iv = Buffer.from(jweTokenParts[2], c.BASE64);
110 | const encryptedText = Buffer.from(jweTokenParts[3], c.BASE64);
111 | const authTag = Buffer.from(jweTokenParts[4], c.BASE64);
112 |
113 | let secretKey = nodeCrypto.privateDecrypt(
114 | {
115 | key: this.privateKey,
116 | padding: nodeCrypto.constants.RSA_PKCS1_OAEP_PADDING,
117 | oaepHash: "sha256",
118 | },
119 | Buffer.from(encryptedSecretKey, c.BINARY)
120 | );
121 |
122 | let decryptionEncoding = JSON.parse(jweHeader).enc;
123 | let gcmMode = true;
124 | switch (decryptionEncoding) {
125 | case "A128GCM":
126 | decryptionEncoding = "AES-128-GCM";
127 | break;
128 | case "A192GCM":
129 | decryptionEncoding = "AES-192-GCM";
130 | break;
131 | case "A256GCM":
132 | decryptionEncoding = "AES-256-GCM";
133 | break;
134 | case "A128CBC-HS256":
135 | decryptionEncoding = "AES-128-CBC";
136 | secretKey = secretKey.slice(16, 32);
137 | gcmMode = false;
138 | break;
139 | default:
140 | throw new Error(
141 | "Unsupported decryption encoding: " + decryptionEncoding
142 | );
143 | }
144 |
145 | const authTagBinary = Buffer.from(authTag, c.BASE64);
146 | const decipher = nodeCrypto.createDecipheriv(
147 | decryptionEncoding,
148 | secretKey,
149 | Buffer.from(iv, c.BINARY),
150 | { authTagLength: authTagBinary.length }
151 | );
152 | if (gcmMode === true) {
153 | decipher.setAAD(Buffer.from(jweTokenParts[0], c.ASCII));
154 | decipher.setAuthTag(authTagBinary);
155 | }
156 | let data = decipher.update(encryptedText, c.BASE64, c.UTF8);
157 | data += decipher.final(c.UTF8);
158 |
159 | return utils.stringToJson(data);
160 | };
161 | }
162 |
163 | function serialize(header, encryptedSecretKey, iv, encryptedText, authTag) {
164 | return (
165 | header +
166 | "." +
167 | encryptedSecretKey +
168 | "." +
169 | iv +
170 | "." +
171 | encryptedText +
172 | "." +
173 | authTag
174 | );
175 | }
176 |
177 | function deSerialize(jweToken) {
178 | return jweToken.split(".");
179 | }
180 |
181 | /**
182 | * @private
183 | */
184 | function readPublicCertificate(publicCertificatePath) {
185 | const publicCertificate = utils.readPublicCertificate(publicCertificatePath);
186 | if (publicCertificate) {
187 | return forge.pki.certificateToPem(publicCertificate);
188 | } else {
189 | return null;
190 | }
191 | }
192 |
193 | /**
194 | * @private
195 | */
196 | function getPrivateKey(config) {
197 | const privateKey = utils.getPrivateKey(config);
198 | if (privateKey) {
199 | return forge.pki.privateKeyToPem(privateKey);
200 | } else {
201 | return null;
202 | }
203 | }
204 |
205 | /**
206 | * @private
207 | * @param config Configuration object
208 | */
209 | function computePublicFingerprint(config, encryptionCertificate) {
210 | if (config && encryptionCertificate) {
211 | if(config.publicKeyFingerprintType) {
212 | return utils.computePublicFingerprint(
213 | config,
214 | forge.pki.certificateFromPem(encryptionCertificate),
215 | config.dataEncoding
216 | );
217 | } else {
218 | return utils.publicKeyFingerprint(
219 | forge.pki.certificateFromPem(encryptionCertificate)
220 | );
221 | }
222 | } else {
223 | return null;
224 | }
225 | }
226 |
227 | /**
228 | * Check if the passed configuration is valid
229 | * @throws {Error} if the config is not valid
230 | * @private
231 | */
232 | function isValidConfig(config) {
233 | if (!utils.nodeVersionSupportsJWE()) {
234 | throw Error("JWE Encryption is only supported on Node 13+");
235 | }
236 | const propertiesBasic = ["encryptionCertificate", "encryptedValueFieldName"];
237 | const contains = utils.checkConfigFieldsArePopulated(config, propertiesBasic);
238 | utils.validateRootMapping(config);
239 | validateFingerprint(config, contains);
240 | }
241 |
242 | /**
243 | * @private
244 | */
245 | function validateFingerprint(config, contains) {
246 | const propertiesFingerprint = ["publicKeyFingerprintType"];
247 | const propertiesOptionalDataEncoding = ["dataEncoding"];
248 | const propertiesOptionalFingerprint = ["publicKeyFingerprint"];
249 | if (
250 | contains(propertiesFingerprint) &&
251 | config[propertiesFingerprint[0]] !== "certificate" &&
252 | config[propertiesFingerprint[0]] !== "publicKey"
253 | ) {
254 | throw Error(
255 | "Config not valid: publicKeyFingerprintType should be: 'certificate' or 'publicKey'"
256 | );
257 | }
258 | if (
259 | !contains(propertiesOptionalFingerprint) &&
260 | config[propertiesFingerprint[0]] === "certificate" &&
261 | !(config[propertiesOptionalDataEncoding[0]] === c.BASE64 ||
262 | config[propertiesOptionalDataEncoding[0]] === c.HEX)
263 | ) {
264 | throw Error(
265 | "Config not valid: if publicKeyFingerprintType is 'certificate' dataEncoding must be either 'base64' or 'hex'"
266 | );
267 | }
268 | }
269 |
270 | /**
271 | * @private
272 | */
273 | function toEncodedString(value, fromFormat, toFormat) {
274 | let result = Buffer.from(value, fromFormat);
275 | if (toFormat === c.BASE64URL) {
276 | result = result.toString(c.BASE64);
277 | result = result.replace(/\+/g, "-");
278 | result = result.replace(/\\/g, "_");
279 | return result.replace(/=/g, "");
280 | } else {
281 | return result.toString(toFormat);
282 | }
283 | }
284 |
285 | module.exports = JweCrypto;
286 |
--------------------------------------------------------------------------------
/test/field-level-encryption.test.js:
--------------------------------------------------------------------------------
1 | const assert = require("assert");
2 | const rewire = require("rewire");
3 | const FieldLevelEncryption = rewire(
4 | "../lib/mcapi/encryption/field-level-encryption"
5 | );
6 |
7 | const testConfig = require("./mock/config");
8 |
9 | describe("Field Level Encryption", () => {
10 | describe("#new FieldLevelEncryption", () => {
11 | it("when valid config", () => {
12 | const fle = new FieldLevelEncryption(testConfig);
13 | assert.ok(fle.crypto);
14 | });
15 |
16 | it("isWithHeader", () => {
17 | const config = JSON.parse(JSON.stringify(testConfig));
18 | config["ivHeaderName"] = "ivHeaderName";
19 | config["encryptedKeyHeaderName"] = "encryptedKeyHeaderName";
20 | const fle = new FieldLevelEncryption(config);
21 | assert.ok(fle.isWithHeader);
22 | });
23 | });
24 |
25 |
26 | describe("#encrypt", () => {
27 | const fle = new FieldLevelEncryption(testConfig);
28 | const encrypt = FieldLevelEncryption.__get__("encrypt");
29 |
30 | it("encrypt body payload", () => {
31 | const res = encrypt.call(fle, "/resource", null, {
32 | elem1: {
33 | encryptedData: {
34 | accountNumber: "5123456789012345",
35 | },
36 | shouldBeThere: "shouldBeThere",
37 | },
38 | });
39 | assert.ok(res.header === null);
40 | assert.ok(res.body.elem1.shouldBeThere);
41 | assert.ok(res.body.elem1.encryptedData);
42 | assert.ok(res.body.elem1.encryptedKey);
43 | assert.ok(res.body.elem1.iv);
44 | assert.ok(res.body.elem1.oaepHashingAlgorithm);
45 | assert.ok(res.body.elem1.publicKeyFingerprint);
46 | assert.ok(!res.body.elem1.encryptedData.accountNumber);
47 | });
48 |
49 | it("encrypt body payload with readme config", () => {
50 | const testConfigReadme = require("./mock/config-readme");
51 | const fle = new FieldLevelEncryption(testConfigReadme);
52 | const encrypt = FieldLevelEncryption.__get__("encrypt");
53 | const res = encrypt.call(fle, "/resource", null, {
54 | path: {
55 | to: {
56 | encryptedData: {
57 | sensitive: "this is a secret",
58 | sensitive2: "this is a super secret!",
59 | },
60 | },
61 | },
62 | });
63 | assert.ok(res.header === null);
64 | assert.ok(!res.body.path.to.encryptedData.sensitive);
65 | assert.ok(!res.body.path.to.encryptedData.sensitive2);
66 | assert.ok(res.body.path);
67 | assert.ok(res.body.path.to);
68 | assert.ok(res.body.path.to.encryptedData);
69 | assert.ok(!res.body.path.to.encryptedData.sensitive);
70 | assert.ok(res.body.path.to.iv);
71 | assert.ok(res.body.path.to.encryptedKey);
72 | assert.ok(res.body.path.to.publicKeyFingerprint);
73 | assert.ok(res.body.path.to.oaepHashingAlgorithm);
74 | });
75 |
76 | it("encrypt with header", () => {
77 | const header = {};
78 | const headerConfig = require("./mock/config-header");
79 | const fle = new FieldLevelEncryption(headerConfig);
80 | const encrypt = FieldLevelEncryption.__get__("encrypt");
81 | const res = encrypt.call(fle, "/resource", header, {
82 | encrypted_payload: {
83 | data: {
84 | accountNumber: "5123456789012345",
85 | },
86 | },
87 | });
88 | assert.ok(res.header[headerConfig.encryptedKeyHeaderName]);
89 | assert.ok(res.header[headerConfig.ivHeaderName]);
90 | assert.ok(res.header[headerConfig.oaepHashingAlgorithmHeaderName]);
91 | assert.ok(res.header[headerConfig.publicKeyFingerprintHeaderName]);
92 | assert.ok(
93 | res.body.encrypted_payload.data.length > "5123456789012345".length
94 | );
95 | });
96 |
97 | it("encrypt when config not found", () => {
98 | const body = {
99 | elem1: {
100 | encryptedData: {
101 | accountNumber: "5123456789012345",
102 | },
103 | },
104 | };
105 | const res = encrypt.call(fle, "/not-exists", null, body);
106 | assert.ok(res.header === null);
107 | assert.ok(JSON.stringify(res.body) === JSON.stringify(body));
108 | });
109 |
110 | it("encrypt root arrays", () => {
111 | const body = [{}, {}];
112 | const fle = new FieldLevelEncryption(testConfig);
113 | const res = encrypt.call(fle, "/array-resp", null, body);
114 |
115 | assert.ok(res.body.iv);
116 | assert.ok(res.body.encryptedData);
117 | assert.ok(res.body.encryptedKey);
118 | assert.ok(res.body.publicKeyFingerprint);
119 | assert.ok(res.body.oaepHashingAlgorithm);
120 |
121 | const response = { request: { url: "/array-resp" }, body: res.body };
122 | const decrypted = FieldLevelEncryption.__get__("decrypt").call(
123 | fle,
124 | response
125 | );
126 |
127 | assert.ok(JSON.stringify(body) === JSON.stringify(decrypted));
128 | });
129 |
130 | it("encrypt root arrays with header config", () => {
131 | const header = {};
132 | const body = [{}, {}];
133 | const fle = new FieldLevelEncryption(require("./mock/config-header"));
134 | const res = encrypt.call(fle, "/array-resp", header, body);
135 |
136 | const response = {
137 | request: { url: "/array-resp" },
138 | body: res.body,
139 | header: header,
140 | };
141 | const decrypted = FieldLevelEncryption.__get__("decrypt").call(
142 | fle,
143 | response
144 | );
145 |
146 | assert.ok(JSON.stringify(body) === JSON.stringify(decrypted));
147 | });
148 |
149 | it("encrypt property of primitive type", () => {
150 | const res = encrypt.call(fle, "/resource", null, {
151 | elem1: {
152 | encryptedData: "plaintext",
153 | shouldBeThere: "shouldBeThere",
154 | },
155 | });
156 | assert.ok(res.header === null);
157 | assert.ok(res.body.elem1.shouldBeThere);
158 | assert.ok(res.body.elem1.encryptedData);
159 | assert.ok(res.body.elem1.encryptedKey);
160 | assert.ok(res.body.elem1.iv);
161 | assert.ok(res.body.elem1.oaepHashingAlgorithm);
162 | assert.ok(res.body.elem1.publicKeyFingerprint);
163 | });
164 | });
165 |
166 | describe("#decrypt", () => {
167 | const fle = new FieldLevelEncryption(testConfig);
168 | const decrypt = FieldLevelEncryption.__get__("decrypt");
169 |
170 | it("decrypt response", () => {
171 | const response = require("./mock/response");
172 | const res = decrypt.call(fle, response);
173 | assert.ok(res.foo.accountNumber === "5123456789012345");
174 | assert.ok(!res.foo.elem1);
175 | assert.ok(
176 | !Object.prototype.hasOwnProperty.call(res.foo, "encryptedData")
177 | );
178 | });
179 |
180 | it("decrypt response with readme config", () => {
181 | const testConfigReadme = require("./mock/config-readme");
182 | const fle = new FieldLevelEncryption(testConfigReadme);
183 | const decrypt = FieldLevelEncryption.__get__("decrypt");
184 | const responseReadme = require("./mock/response-readme");
185 | const res = decrypt.call(fle, responseReadme);
186 | assert(res.path);
187 | assert(res.path.to);
188 | assert(res.path.to.foo);
189 | assert(res.path.to.foo.sensitive);
190 | assert(res.path.to.foo.sensitive2);
191 | });
192 |
193 | it("decrypt response with no valid config", () => {
194 | const response = JSON.parse(JSON.stringify(require("./mock/response")));
195 | delete response.body.elem1;
196 | const res = decrypt.call(fle, response);
197 | assert.ok(res === response.body);
198 | });
199 |
200 | it("decrypt response replacing whole body", () => {
201 | const config = JSON.parse(JSON.stringify(testConfig));
202 | config.paths[0].toDecrypt[0].element = "";
203 | config.paths[0].toDecrypt[0].obj = "encryptedData";
204 | const fle = new FieldLevelEncryption(config);
205 | const response = require("./mock/response-root");
206 | const res = decrypt.call(fle, response);
207 | assert.ok(res.encryptedData.accountNumber === "5123456789012345");
208 | assert.ok(res.notDelete);
209 | });
210 |
211 | it("decrypt with header", () => {
212 | const response = require("./mock/response-header");
213 | const headerConfig = require("./mock/config-header");
214 | const fle = new FieldLevelEncryption(headerConfig);
215 | const decrypt = FieldLevelEncryption.__get__("decrypt");
216 | const res = decrypt.call(fle, response);
217 | assert.ok(res.accountNumber === "5123456789012345");
218 | });
219 |
220 | it("decrypt with header when node not found in body", () => {
221 | const response = require("./mock/response-header");
222 | const headerConfig = require("./mock/config-header");
223 | const fle = new FieldLevelEncryption(headerConfig);
224 | const decrypt = FieldLevelEncryption.__get__("decrypt");
225 | delete response.body.encrypted_payload;
226 | response.body = { test: "foo" };
227 | const res = decrypt.call(fle, response);
228 | assert.ok(JSON.stringify(res) === JSON.stringify({ test: "foo" }));
229 | });
230 |
231 | it("decrypt without config", () => {
232 | const fle = new FieldLevelEncryption(testConfig);
233 | const response = { request: { url: "/foobar" }, body: "body" };
234 | const res = decrypt.call(fle, response);
235 | assert.ok(res === "body");
236 | });
237 |
238 | it("decrypt root arrays", () => {
239 | const response = {
240 | request: { url: "/array-resp" },
241 | body: {
242 | encryptedData: "3496b0c505bcea6a849f8e30b553e6d4",
243 | iv: "ed82c0496e9d5ac769d77bdb2eb27958",
244 | encryptedKey:
245 | "29ea447b70bdf85dd509b5d4a23dc0ffb29fd1acf50ed0800ec189fbcf1fb813fa075952c3de2915d63ab42f16be2ed46dc27ba289d692778a1d585b589039ba0b25bad326d699c45f6d3cffd77b5ec37fe12e2c5456d49980b2ccf16402e83a8e9765b9b93ca37d4d5181ec3e5327fd58387bc539238f1c20a8bc9f4174f5d032982a59726b3e0b9cf6011d4d7bfc3afaf617e768dea6762750bce07339e3e55fdbd1a1cd12ee6bbfbc3c7a2d7f4e1313410eb0dad13e594a50a842ee1b2d0ff59d641987c417deaa151d679bc892e5c051b48781dbdefe74a12eb2b604b981e0be32ab81d01797117a24fbf6544850eed9b4aefad0eea7b3f5747b20f65d3f",
246 | oaepHashingAlgorithm: "SHA256",
247 | },
248 | };
249 | const fle = new FieldLevelEncryption(testConfig);
250 | const res = decrypt.call(fle, response);
251 | assert.ok(res instanceof Array);
252 | assert.ok(JSON.stringify(res) === "[{},{}]");
253 | });
254 |
255 | it("decrypt root arrays to path", () => {
256 | const response = {
257 | request: { url: "/array-resp2" },
258 | body: {
259 | encryptedData: "3496b0c505bcea6a849f8e30b553e6d4",
260 | iv: "ed82c0496e9d5ac769d77bdb2eb27958",
261 | encryptedKey:
262 | "29ea447b70bdf85dd509b5d4a23dc0ffb29fd1acf50ed0800ec189fbcf1fb813fa075952c3de2915d63ab42f16be2ed46dc27ba289d692778a1d585b589039ba0b25bad326d699c45f6d3cffd77b5ec37fe12e2c5456d49980b2ccf16402e83a8e9765b9b93ca37d4d5181ec3e5327fd58387bc539238f1c20a8bc9f4174f5d032982a59726b3e0b9cf6011d4d7bfc3afaf617e768dea6762750bce07339e3e55fdbd1a1cd12ee6bbfbc3c7a2d7f4e1313410eb0dad13e594a50a842ee1b2d0ff59d641987c417deaa151d679bc892e5c051b48781dbdefe74a12eb2b604b981e0be32ab81d01797117a24fbf6544850eed9b4aefad0eea7b3f5747b20f65d3f",
263 | oaepHashingAlgorithm: "SHA256",
264 | },
265 | };
266 | const fle = new FieldLevelEncryption(testConfig);
267 | const res = decrypt.call(fle, response);
268 | assert.ok(res instanceof Object);
269 | assert.ok(JSON.stringify(res) === '{"path":{"to":{"foo":[{},{}]}}}');
270 | });
271 | });
272 |
273 | describe("#setHeader", () => {
274 | const headerConfig = require("./mock/config-header");
275 | const fle = new FieldLevelEncryption(headerConfig);
276 | const setHeader = FieldLevelEncryption.__get__("setHeader");
277 |
278 | it("set http header from config", () => {
279 | const header = {};
280 | const params = {
281 | encoded: { encryptedKey: "encryptedKey", iv: "iv" },
282 | oaepHashingAlgorithm: "oaepHashingAlgorithm",
283 | publicKeyFingerprint: "publicKeyFingerprint",
284 | };
285 | setHeader.call(fle, header, params);
286 | assert.ok(header[headerConfig.encryptedKeyHeaderName] === "encryptedKey");
287 | assert.ok(header[headerConfig.ivHeaderName] === "iv");
288 | assert.ok(
289 | header[headerConfig.oaepHashingAlgorithmHeaderName] ===
290 | "oaepHashingAlgorithm"
291 | );
292 | assert.ok(
293 | header[headerConfig.publicKeyFingerprintHeaderName] ===
294 | "publicKeyFingerprint"
295 | );
296 | });
297 | });
298 |
299 | describe("#import FieldLevelEncryption", () => {
300 | it("from mcapi-service", () => {
301 | const FieldLevelEncryption = require("..").FieldLevelEncryption;
302 | assert.ok(FieldLevelEncryption);
303 | const fle = new FieldLevelEncryption(testConfig);
304 | assert.ok(fle.crypto);
305 | });
306 | });
307 | });
308 |
--------------------------------------------------------------------------------
/test/payload-encryption.test.js:
--------------------------------------------------------------------------------
1 | const assert = require("assert");
2 | const rewire = require("rewire");
3 | const FieldLevelEncryption = rewire(
4 | "../lib/mcapi/encryption/field-level-encryption"
5 | );
6 | const JweEncryption = rewire("../lib/mcapi/encryption/jwe-encryption");
7 | const utils = require("../lib/mcapi/utils/utils");
8 |
9 | const testConfig = require("./mock/config");
10 | const jweTestConfig = require("./mock/jwe-config");
11 |
12 | describe("Payload encryption", () => {
13 | const config = JSON.parse(JSON.stringify(testConfig));
14 | config["encryptedValueFieldName"] = "encryptedValue";
15 | const jweConfig = JSON.parse(JSON.stringify(jweTestConfig));
16 | jweConfig["encryptedValueFieldName"] = "encryptedValue";
17 | const fle = new FieldLevelEncryption(config);
18 | const encryptBody = FieldLevelEncryption.__get__("encryptBody");
19 | const decryptBody = FieldLevelEncryption.__get__("decryptBody");
20 |
21 | describe("#encryptBody", () => {
22 | it("with sibling", () => {
23 | const body = {
24 | data: {
25 | field1: "value1",
26 | field2: "value2",
27 | },
28 | encryptedData: {},
29 | };
30 | encryptBody.call(
31 | fle,
32 | {
33 | element: "data",
34 | obj: "encryptedData",
35 | },
36 | body
37 | );
38 | assert.ok(!body.data);
39 | assert.ok(body.encryptedData.encryptedValue);
40 | assert.ok(body.encryptedData.iv);
41 | assert.ok(body.encryptedData.encryptedKey);
42 | assert.ok(body.encryptedData.publicKeyFingerprint);
43 | assert.ok(body.encryptedData.oaepHashingAlgorithm);
44 | });
45 |
46 | it("destination obj not exists", () => {
47 | const body = {
48 | itemsToEncrypt: {
49 | first: "first",
50 | second: "second",
51 | },
52 | dontEncrypt: {
53 | text: "just text...",
54 | },
55 | };
56 | encryptBody.call(
57 | fle,
58 | {
59 | element: "itemsToEncrypt",
60 | obj: "encryptedItems",
61 | },
62 | body
63 | );
64 | assert.ok(body.dontEncrypt);
65 | assert.ok(!body.itemsToEncrypt);
66 | assert.ok(body.encryptedItems);
67 | assert.ok(body.encryptedItems.iv);
68 | assert.ok(body.encryptedItems.encryptedValue);
69 | assert.ok(body.encryptedItems.encryptedKey);
70 | assert.ok(body.encryptedItems.publicKeyFingerprint);
71 | assert.ok(body.encryptedItems.oaepHashingAlgorithm);
72 | });
73 |
74 | it("elem not found", () => {
75 | const body = {
76 | itemsToEncrypt: {
77 | first: "first",
78 | second: "second",
79 | },
80 | dontEncrypt: {
81 | text: "just text...",
82 | },
83 | };
84 | encryptBody.call(
85 | fle,
86 | {
87 | element: "not.found",
88 | obj: "encryptedItems",
89 | },
90 | body
91 | );
92 | assert.ok(body.dontEncrypt);
93 | assert.ok(body.itemsToEncrypt);
94 | assert.ok(!body.encryptedItems);
95 | });
96 |
97 | it("nested object to encrypt", () => {
98 | const body = {
99 | path: {
100 | to: {
101 | encryptedData: {
102 | sensitive: "secret",
103 | sensitive2: "secret 2",
104 | },
105 | },
106 | },
107 | };
108 | encryptBody.call(
109 | fle,
110 | {
111 | element: "path.to.encryptedData",
112 | obj: "path.to",
113 | },
114 | body
115 | );
116 | assert.ok(body.path);
117 | assert.ok(body.path.to);
118 | assert.ok(body.path.to.encryptedValue);
119 | assert.ok(body.path.to.iv);
120 | assert.ok(body.path.to.encryptedKey);
121 | assert.ok(body.path.to.publicKeyFingerprint);
122 | assert.ok(body.path.to.oaepHashingAlgorithm);
123 | assert.ok(!body.path.to.encryptedData);
124 | });
125 |
126 | it("nested object, create different nested object and delete it", () => {
127 | const body = {
128 | path: {
129 | to: {
130 | foo: {
131 | sensitive: "secret",
132 | sensitive2: "secret 2",
133 | },
134 | },
135 | },
136 | };
137 | encryptBody.call(
138 | fle,
139 | {
140 | element: "path.to.foo",
141 | obj: "path.to.encryptedFoo",
142 | },
143 | body
144 | );
145 | assert.ok(!body.path.to.foo);
146 | assert.ok(body);
147 | assert.ok(body.path);
148 | assert.ok(body.path.to);
149 | assert.ok(body.path.to.encryptedFoo);
150 | assert.ok(body.path.to.encryptedFoo.iv);
151 | assert.ok(body.path.to.encryptedFoo.encryptedValue);
152 | assert.ok(body.path.to.encryptedFoo.encryptedKey);
153 | assert.ok(body.path.to.encryptedFoo.publicKeyFingerprint);
154 | assert.ok(body.path.to.encryptedFoo.oaepHashingAlgorithm);
155 | });
156 | });
157 |
158 | describe("#decryptBody", () => {
159 | it("nested properties, create new obj", () => {
160 | const body = {
161 | path: {
162 | to: {
163 | encryptedFoo: {
164 | encryptedValue:
165 | "3097e36bf8b71637a0273abe69c23752d6157464ce49f6f35120d28bedfb63a1f2c8087be3a3bc9775592db41db87a8c",
166 | iv: "22507f596fffb45b15244356981d7ea1",
167 | encryptedKey:
168 | "d4714161898b8bc5c54a63f71ae7c7a40734e4f7c7e27d121ac5e85a3fa47946aa3546027abe0874d751d5ae701491a7f572fc30fa08dd671d358746ffe8709cba36010f97864105b175c51b6f32d36d981287698a3f6f8707aedf980cce19bfe7c5286ddba87b7f3e5abbfa88a980779037c0b7902d340d73201cf3f0b546c2ad9f54e4b71a43504da947a3cb7af54d61717624e636a90069be3c46c19b9ae8b76794321b877544dd03f0ca816288672ef361c3e8f14d4a1ee96ba72d21e3a36c020aa174635a8579b0e9af761d96437e1fa167f00888ff2532292e7a220f5bc948f8159dea2541b8c6df6463213de292b4485076241c90706efad93f9b98ea",
169 | publicKeyFingerprint:
170 | "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
171 | oaepHashingAlgorithm: "SHA512",
172 | },
173 | },
174 | },
175 | };
176 | decryptBody.call(
177 | fle,
178 | {
179 | element: "path.to.encryptedFoo",
180 | obj: "path.to.foo",
181 | },
182 | body
183 | );
184 | assert.ok(!body.path.to.encryptedFoo);
185 | assert.ok(body);
186 | assert.ok(body.path);
187 | assert.ok(body.path.to);
188 | assert.ok(body.path.to.foo);
189 | assert.ok(body.path.to.foo.accountNumber === "5123456789012345");
190 | });
191 |
192 | it("primitive type", () => {
193 | const body = {
194 | data: {
195 | encryptedValue: "e2d6a3a76ea6e605e55b400e5a4eba11",
196 | iv: "3ce861359fa1630c7a794901ee14bf41",
197 | encryptedKey:
198 | "02bb8d5c7d113ef271f199c09f0d76db2b6d5d2d209ad1a20dbc4dd0d04576a92ceb917eea5f403ccf64c3c39dda564046909af96c82fad62f89c3cbbec880ea3105a0a171af904cd3b86ea68991202a2795dca07050ca58252701b7ecea06055fd43e96f4beee48b6275e86af93c88c21994ff46f0610171bd388a2c0a1f518ffc8346f7f513f3283feae5b102c8596ddcb2aea5e62ceb17222e646c599f258463405d28ac012bfd4cc431f94111ee07d79e660948485e38c13cdb8bba8e1df3f7dba0f4c77696f71930533c955f3a430658edaa03b0b0c393934d60f5ac3ea5c06ed64bf969fc01942eac432b8e0c56f7538659a72859d445d150c169ae690",
199 | publicKeyFingerprint:
200 | "761b003c1eade3a5490e5000d37887baa5e6ec0e226c07706e599451fc032a79",
201 | oaepHashingAlgorithm: "SHA256",
202 | },
203 | };
204 | decryptBody.call(
205 | fle,
206 | {
207 | element: "data",
208 | obj: "data",
209 | },
210 | body
211 | );
212 | assert.ok(body);
213 | assert.ok(body.data === "string");
214 | });
215 | });
216 |
217 | describe("#encryptBodyJwe", () => {
218 | before(function () {
219 | if (!utils.nodeVersionSupportsJWE()) {
220 | this.skip();
221 | }
222 | });
223 |
224 | it("jwe with sibling", () => {
225 | const jweFle = new JweEncryption(jweConfig);
226 | const body = {
227 | data: {
228 | field1: "value1",
229 | field2: "value2",
230 | },
231 | encryptedData: {},
232 | };
233 | encryptBody.call(
234 | jweFle,
235 | {
236 | element: "data",
237 | obj: "encryptedData",
238 | },
239 | body
240 | );
241 | assert.ok(!body.data);
242 | assert.ok(body.encryptedData.encryptedValue);
243 | });
244 |
245 | it("jwe destination obj not exists", () => {
246 | const jweFle = new JweEncryption(jweConfig);
247 | const body = {
248 | itemsToEncrypt: {
249 | first: "first",
250 | second: "second",
251 | },
252 | dontEncrypt: {
253 | text: "just text...",
254 | },
255 | };
256 | encryptBody.call(
257 | jweFle,
258 | {
259 | element: "itemsToEncrypt",
260 | obj: "encryptedItems",
261 | },
262 | body
263 | );
264 | assert.ok(body.dontEncrypt);
265 | assert.ok(!body.itemsToEncrypt);
266 | assert.ok(body.encryptedItems);
267 | assert.ok(body.encryptedItems.encryptedValue);
268 | });
269 |
270 | it("jwe elem not found", () => {
271 | const jweFle = new JweEncryption(jweConfig);
272 | const body = {
273 | itemsToEncrypt: {
274 | first: "first",
275 | second: "second",
276 | },
277 | dontEncrypt: {
278 | text: "just text...",
279 | },
280 | };
281 | encryptBody.call(
282 | jweFle,
283 | {
284 | element: "not.found",
285 | obj: "encryptedItems",
286 | },
287 | body
288 | );
289 | assert.ok(body.dontEncrypt);
290 | assert.ok(body.itemsToEncrypt);
291 | assert.ok(!body.encryptedItems);
292 | });
293 |
294 | it("jwe nested object to encrypt", () => {
295 | const jweFle = new JweEncryption(jweConfig);
296 | const body = {
297 | path: {
298 | to: {
299 | encryptedData: {
300 | sensitive: "secret",
301 | sensitive2: "secret 2",
302 | },
303 | },
304 | },
305 | };
306 | encryptBody.call(
307 | jweFle,
308 | {
309 | element: "path.to.encryptedData",
310 | obj: "path.to",
311 | },
312 | body
313 | );
314 | assert.ok(body.path);
315 | assert.ok(body.path.to);
316 | assert.ok(body.path.to.encryptedValue);
317 | assert.ok(!body.path.to.encryptedData);
318 | });
319 |
320 | it("jwe nested object, create different nested object and delete it", () => {
321 | const jweFle = new JweEncryption(jweConfig);
322 | const body = {
323 | path: {
324 | to: {
325 | foo: {
326 | sensitive: "secret",
327 | sensitive2: "secret 2",
328 | },
329 | },
330 | },
331 | };
332 | encryptBody.call(
333 | jweFle,
334 | {
335 | element: "path.to.foo",
336 | obj: "path.to.encryptedFoo",
337 | },
338 | body
339 | );
340 | assert.ok(!body.path.to.foo);
341 | assert.ok(body);
342 | assert.ok(body.path);
343 | assert.ok(body.path.to);
344 | assert.ok(body.path.to.encryptedFoo);
345 | assert.ok(body.path.to.encryptedFoo.encryptedValue);
346 | });
347 | });
348 |
349 | describe("#decryptBodyJwe", () => {
350 | before(function () {
351 | if (!utils.nodeVersionSupportsJWE()) {
352 | this.skip();
353 | }
354 | });
355 |
356 | it("jwe nested properties, create new obj", () => {
357 | const jweFle = new JweEncryption(jweConfig);
358 | const body = {
359 | path: {
360 | to: {
361 | encryptedFoo: {
362 | encryptedValue:
363 | "eyJraWQiOiJnSUVQd1RxREdmenc0dXd5TElLa3d3UzNnc3c4NW5FWFkwUFA2QllNSW5rPSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.6lSH7gbbarhjRh4YJKqhGZM3zpi33nuUOeBJSVfpVc55WzMnR2gHBcsTUIonAQYm2BE-TwCc4StjydjXzxnUFNsnD0Hx-l5_Ge9QGDr5VsQdt86vkzx2QEM6LKONnUmIE9rC8dA3Rh6UhaM5jo1fDq7awdqxttGqG5haXODC2FRwePgm44foSW8P-T378enucPbMbfgl1nGLA2JWBKPlsPYfSYf87s9FLmGZvJ9wTWH74-Bh_Ie7MvLAyqrpRZ0_dyRFpSTA_pjmt3lfpFCMm3Dk65kH0T01o6bSnxZMfWvrqVx7kiXOrRYVldNm4zqu3puU4_e5rqHKYyceF0DWSw.QPZnTMrNsLpLzqKiGSZZRQ.faGFTTOeLnPGJUTRlLpjE_jD9hrTak9s1wMMXQrFiCf0nBZ7.gdYK250VuTutnxz1ej1MUQ",
364 | },
365 | },
366 | },
367 | };
368 | decryptBody.call(
369 | jweFle,
370 | {
371 | element: "path.to.encryptedFoo",
372 | obj: "path.to.foo",
373 | },
374 | body
375 | );
376 | assert.ok(!body.path.to.encryptedFoo);
377 | assert.ok(body);
378 | assert.ok(body.path);
379 | assert.ok(body.path.to);
380 | assert.ok(body.path.to.foo);
381 | assert.ok(body.path.to.foo.accountNumber === "5123456789012345");
382 | });
383 | });
384 | });
385 |
--------------------------------------------------------------------------------
/test/jwe-crypto.test.js:
--------------------------------------------------------------------------------
1 | const assert = require("assert");
2 | const rewire = require("rewire");
3 | const Crypto = rewire("../lib/mcapi/crypto/jwe-crypto");
4 | const utils = require("../lib/mcapi/utils/utils");
5 |
6 | const testConfig = require("./mock/jwe-config");
7 |
8 | describe("JWE Crypto", () => {
9 | before(function () {
10 | if (!utils.nodeVersionSupportsJWE()) {
11 | this.skip();
12 | }
13 | });
14 |
15 | describe("#new Crypto", () => {
16 | it("with empty config", () => {
17 | assert.throws(
18 | () => new Crypto({}),
19 | /Config not valid: paths should be an array of path element./
20 | );
21 | });
22 |
23 | it("with string config", () => {
24 | assert.throws(
25 | () => new Crypto(""),
26 | /Config not valid: config should be an object./
27 | );
28 | });
29 |
30 | it("with one property not defined", () => {
31 | const config = JSON.parse(JSON.stringify(testConfig));
32 | delete config["encryptionCertificate"];
33 | assert.throws(
34 | () => new Crypto(config),
35 | /Config not valid: please check that all the properties are defined./
36 | );
37 | });
38 |
39 | it("with empty paths", () => {
40 | const config = JSON.parse(JSON.stringify(testConfig));
41 | config.paths = [];
42 | assert.throws(
43 | () => new Crypto(config),
44 | /Config not valid: paths should be not empty./
45 | );
46 | });
47 |
48 | it("with valid config", () => {
49 | assert.doesNotThrow(() => new Crypto(testConfig));
50 | });
51 |
52 | it("with valid config with private keystore", () => {
53 | const config = JSON.parse(JSON.stringify(testConfig));
54 | delete config.privateKey;
55 | config.keyStore = "./test/res/keys/pkcs12/test_key.p12";
56 | config.keyStoreAlias = "mykeyalias";
57 | config.keyStorePassword = "Password1";
58 | assert.doesNotThrow(() => {
59 | new Crypto(config);
60 | });
61 | });
62 |
63 | it("with valid config with private pkcs1 pem keystore", () => {
64 | const config = JSON.parse(JSON.stringify(testConfig));
65 | delete config.privateKey;
66 | config.keyStore = "./test/res/keys/pkcs1/test_key.pem";
67 | assert.doesNotThrow(() => {
68 | new Crypto(config);
69 | });
70 | });
71 |
72 | it("with valid config with private pkcs8 pem keystore", () => {
73 | const config = JSON.parse(JSON.stringify(testConfig));
74 | delete config.privateKey;
75 | config.keyStore = "./test/res/keys/pkcs8/test_key.pem";
76 | assert.doesNotThrow(() => {
77 | new Crypto(config);
78 | });
79 | });
80 |
81 | it("with valid config with private pkcs8 der keystore", () => {
82 | const config = JSON.parse(JSON.stringify(testConfig));
83 | delete config.privateKey;
84 | config.keyStore = "./test/res/keys/pkcs8/test_key.der";
85 | assert.doesNotThrow(() => {
86 | new Crypto(config);
87 | });
88 | });
89 |
90 | it("without publicKeyFingerprintType, but providing the publicKeyFingerprint", () => {
91 | const config = JSON.parse(JSON.stringify(testConfig));
92 | delete config["publicKeyFingerprintType"];
93 | config.publicKeyFingerprint = "abc";
94 | assert.ok((new Crypto(config).publicKeyFingerprint = "abc"));
95 | });
96 |
97 | it("with wrong publicKeyFingerprintType", () => {
98 | const config = JSON.parse(JSON.stringify(testConfig));
99 | config.publicKeyFingerprintType = "foobar";
100 | assert.throws(
101 | () => new Crypto(config),
102 | /Config not valid: publicKeyFingerprintType should be: 'certificate' or 'publicKey'/
103 | );
104 | });
105 |
106 | it("with right publicKeyFingerprintType: certificate and dataEncoding: base64", () => {
107 | const config = JSON.parse(JSON.stringify(testConfig));
108 | config.publicKeyFingerprintType = "certificate";
109 | config.dataEncoding = "base64";
110 | assert.doesNotThrow(() => new Crypto(config));
111 | });
112 |
113 | it("with right publicKeyFingerprintType: certificate and dataEncoding: hex", () => {
114 | const config = JSON.parse(JSON.stringify(testConfig));
115 | config.publicKeyFingerprintType = "certificate";
116 | config.dataEncoding = "hex";
117 | assert.doesNotThrow(() => new Crypto(config));
118 | });
119 |
120 | it("with right publicKeyFingerprintType: certificate and dataEncoding: null", () => {
121 | const config = JSON.parse(JSON.stringify(testConfig));
122 | config.publicKeyFingerprintType = "certificate";
123 | delete config["dataEncoding"];
124 | assert.throws(
125 | () => new Crypto(config),
126 | /Config not valid: if publicKeyFingerprintType is 'certificate' dataEncoding must be either 'base64' or 'hex'/
127 | );
128 | });
129 |
130 | it("with right publicKeyFingerprintType: publicKey", () => {
131 | const config = JSON.parse(JSON.stringify(testConfig));
132 | config.publicKeyFingerprintType = "publicKey";
133 | assert.doesNotThrow(() => new Crypto(config));
134 | });
135 |
136 | it("without keyStore and privateKey", () => {
137 | const config = JSON.parse(JSON.stringify(testConfig));
138 | delete config["privateKey"];
139 | delete config["keyStore"];
140 | assert.doesNotThrow(() => new Crypto(config));
141 | });
142 |
143 | it("with multiple roots (encrypt)", () => {
144 | const config = JSON.parse(JSON.stringify(testConfig));
145 | config.paths[2].toEncrypt.push(config.paths[2].toEncrypt[0]);
146 | assert.throws(
147 | () => new Crypto(config),
148 | /Config not valid: found multiple configurations encrypt\/decrypt with root mapping/
149 | );
150 | });
151 |
152 | it("with multiple roots (decrypt)", () => {
153 | const config = JSON.parse(JSON.stringify(testConfig));
154 | config.paths[2].toDecrypt.push(config.paths[2].toDecrypt[0]);
155 | config.paths[2].toDecrypt[1].obj = "abc";
156 | assert.throws(
157 | () => new Crypto(config),
158 | /Config not valid: found multiple configurations encrypt\/decrypt with root mapping/
159 | );
160 | });
161 | });
162 |
163 | describe("#encryptData()", () => {
164 | let crypto;
165 | before(() => {
166 | crypto = new Crypto(testConfig);
167 | });
168 |
169 | it("with empty string", () => {
170 | assert.throws(() => {
171 | crypto.encryptData({ data: "" });
172 | }, /Json not valid/);
173 | });
174 |
175 | it("with valid config", () => {
176 | const data = JSON.stringify({ text: "message" });
177 | let resp = crypto.encryptData({
178 | data: data,
179 | });
180 | resp = resp[testConfig.encryptedValueFieldName].split(".");
181 | assert.ok(resp.length === 5);
182 | //Header is always constant
183 | assert.ok(
184 | resp[0] ===
185 | "eyJraWQiOiJnSUVQd1RxREdmenc0dXd5TElLa3d3UzNnc3c4NW5FWFkwUFA2QllNSW5rPSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0"
186 | );
187 | assert.ok(resp[1].length === 342);
188 | assert.ok(resp[2].length === 22);
189 | assert.ok(resp[3].length === 24);
190 | assert.ok(resp[4].length === 22);
191 | });
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 | });
210 | });
211 |
212 | describe("#decryptData()", () => {
213 | let crypto;
214 | before(() => {
215 | crypto = new Crypto(testConfig);
216 | });
217 |
218 | it("with null", () => {
219 | assert.throws(() => {
220 | crypto.decryptData(null);
221 | }, /Input not valid/);
222 | });
223 |
224 | it("with empty string", () => {
225 | assert.throws(() => {
226 | crypto.decryptData("");
227 | }, /Input not valid/);
228 | });
229 |
230 | it("with valid AES128GCM object", () => {
231 | const resp = crypto.decryptData(
232 | "eyJlbmMiOiJBMTI4R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.WtvYljbsjdEv-Ttxx1p6PgyIrOsLpj1FMF9NQNhJUAHlKchAo5QImgEgIdgJE7HC2KfpNcHiQVqKKZq_y201FVzpicDkNzlPJr5kIH4Lq-oC5iP0agWeou9yK5vIxFRP__F_B8HSuojBJ3gDYT_KdYffUIHkm_UysNj4PW2RIRlafJ6RKYanVzk74EoKZRG7MIr3pTU6LIkeQUW41qYG8hz6DbGBOh79Nkmq7Oceg0ZwCn1_MruerP-b15SGFkuvOshStT5JJp7OOq82gNAOkMl4fylEj2-vADjP7VSK8GlqrA7u9Tn-a4Q28oy0GOKr1Z-HJgn_CElknwkUTYsWbg.PKl6_kvZ4_4MjmjW.AH6pGFkn7J49hBQcwg.zdyD73TcuveImOy4CRnVpw"
233 | );
234 | assert.ok(JSON.stringify(resp) === JSON.stringify({ foo: "bar" }));
235 | });
236 |
237 | it("with valid AES192GCM object", () => {
238 | const resp = crypto.decryptData(
239 | "eyJlbmMiOiJBMTkyR0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.FWC8PVaZoR2TRKwKO4syhSJReezVIvtkxU_yKh4qODNvlVr8t8ttvySJ-AjM8xdI6vNyIg9jBMWASG4cE49jT9FYuQ72fP4R-Td4vX8wpB8GonQj40yLqZyfRLDrMgPR20RcQDW2ThzLXsgI55B5l5fpwQ9Nhmx8irGifrFWOcJ_k1dUSBdlsHsYxkjRKMENu5x4H6h12gGZ21aZSPtwAj9msMYnKLdiUbdGmGG_P8a6gPzc9ih20McxZk8fHzXKujjukr_1p5OO4o1N4d3qa-YI8Sns2fPtf7xPHnwi1wipmCC6ThFLU80r3173RXcpyZkF8Y3UacOS9y1f8eUfVQ.JRE7kZLN4Im1Rtdb.eW_lJ-U330n0QHqZnQ._r5xYVvMCrvICwLz4chjdw"
240 | );
241 | assert.ok(JSON.stringify(resp) === JSON.stringify({ foo: "bar" }));
242 | });
243 |
244 | it("with valid AES256GCM object", () => {
245 | const resp = crypto.decryptData(
246 | "eyJraWQiOiI3NjFiMDAzYzFlYWRlM2E1NDkwZTUwMDBkMzc4ODdiYWE1ZTZlYzBlMjI2YzA3NzA2ZTU5OTQ1MWZjMDMyYTc5IiwiY3R5IjoiYXBwbGljYXRpb25cL2pzb24iLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.8c6vxeZOUBS8A9SXYUSrRnfl1ht9xxciB7TAEv84etZhQQ2civQKso-htpa2DWFBSUm-UYlxb6XtXNXZxuWu-A0WXjwi1K5ZAACc8KUoYnqPldEtC9Q2bhbQgc_qZF_GxeKrOZfuXc9oi45xfVysF_db4RZ6VkLvY2YpPeDGEMX_nLEjzqKaDz_2m0Ae_nknr0p_Nu0m5UJgMzZGR4Sk1DJWa9x-WJLEyo4w_nRDThOjHJshOHaOU6qR5rdEAZr_dwqnTHrjX9Qm9N9gflPGMaJNVa4mvpsjz6LJzjaW3nJ2yCoirbaeJyCrful6cCiwMWMaDMuiBDPKa2ovVTy0Sw.w0Nkjxl0T9HHNu4R.suRZaYu6Ui05Z3-vsw.akknMr3Dl4L0VVTGPUszcA"
247 | );
248 | assert.ok(JSON.stringify(resp) === JSON.stringify({ foo: "bar" }));
249 | });
250 |
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 |
258 | });
259 |
260 | describe("#readPublicCertificate", () => {
261 | it("not valid key", () => {
262 | const readPublicCertificate = Crypto.__get__("readPublicCertificate");
263 | assert.throws(() => {
264 | readPublicCertificate("./test/res/empty.key");
265 | }, /Public certificate content is not valid/);
266 | });
267 | });
268 |
269 | describe("#computePublicFingerprint", () => {
270 | const computePublicFingerprint = Crypto.__get__("computePublicFingerprint");
271 | let crypto;
272 | before(() => {
273 | crypto = new Crypto(testConfig);
274 | });
275 |
276 | it("not valid config", () => {
277 | assert.ok(!computePublicFingerprint());
278 | });
279 |
280 | it("not valid publicKeyFingerprintType", () => {
281 | assert.ok(!computePublicFingerprint({}));
282 | });
283 |
284 | it("compute public fingerprint: certificate", () => {
285 | assert.ok(
286 | "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
287 | computePublicFingerprint.call(crypto, {
288 | publicKeyFingerprintType: "certificate",
289 | })
290 | );
291 | });
292 |
293 | it("compute public fingerprint: publicKey", () => {
294 | assert.ok(
295 | "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
296 | computePublicFingerprint.call(crypto, {
297 | publicKeyFingerprintType: "publicKey",
298 | })
299 | );
300 | });
301 |
302 | it("compute public fingerprint: defaults to publicKey with publicKeyFingerprintType set", () => {
303 | const strippedConfig = JSON.parse(JSON.stringify(testConfig));
304 | delete strippedConfig["publicKeyFingerprintType"];
305 | delete strippedConfig["dataEncoding"];
306 |
307 | assert.ok(
308 | "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
309 | computePublicFingerprint.call(crypto, {
310 | strippedConfig
311 | })
312 | );
313 | });
314 | });
315 | });
316 |
--------------------------------------------------------------------------------
/lib/mcapi/utils/utils.js:
--------------------------------------------------------------------------------
1 | const forge = require("node-forge");
2 | const fs = require("fs");
3 | const c = require("../utils/constants");
4 |
5 | /**
6 | * Utils module
7 | * @Module Utils
8 | * @version 1.0.0
9 | */
10 |
11 | module.exports.isSet = function (value) {
12 | return isSet(value);
13 | };
14 |
15 | function isSet(value) {
16 | return typeof value !== "undefined" && value !== null && value.length !== 0;
17 | }
18 |
19 | /**
20 | * Converts a 'binary' encoded string of bytes to (hex or base64) encoded string.
21 | *
22 | * @param bytes Bytes to convert
23 | * @param dataEncoding encoding to use (hex or base64)
24 | * @returns {string} encoded string
25 | */
26 | module.exports.bytesToString = function (bytes, dataEncoding) {
27 | return bytesToString(bytes, dataEncoding);
28 | };
29 |
30 | function bytesToString(bytes, dataEncoding) {
31 | if (typeof bytes === "undefined" || bytes === null) {
32 | throw new Error("Input not valid");
33 | }
34 | switch (dataEncoding.toLowerCase()) {
35 | case c.HEX:
36 | return forge.util.bytesToHex(bytes);
37 | case c.BASE64:
38 | default:
39 | return forge.util.encode64(bytes);
40 | }
41 | }
42 |
43 | /**
44 | * Converts a (hex or base64) string into a 'binary' encoded string of bytes.
45 | *
46 | * @param value string to convert
47 | * @param dataEncoding encoding to use (hex or base64)
48 | * @returns binary encoded string of bytes
49 | */
50 | module.exports.stringToBytes = function (value, dataEncoding) {
51 | if (typeof value === "undefined" || value === null) {
52 | throw new Error("Input not valid");
53 | }
54 | switch (dataEncoding.toLowerCase()) {
55 | case c.HEX:
56 | return forge.util.hexToBytes(value);
57 | case c.BASE64:
58 | default:
59 | return forge.util.decode64(value);
60 | }
61 | };
62 |
63 | module.exports.toByteArray = function (value, fromFormat) {
64 | return Buffer.from(value, fromFormat);
65 | };
66 |
67 | /**
68 | * Convert a json object or json string to string
69 | *
70 | * @param {Object|string} data Json string or Json obj
71 | * @returns {string}
72 | */
73 | module.exports.jsonToString = function (data) {
74 | if (typeof data === "undefined" || data === null) {
75 | throw new Error("Input not valid");
76 | }
77 | if (data === "") throw new Error("Json not valid");
78 | try {
79 | if (typeof data === "string" || data instanceof String) {
80 | return isJson(data) ? JSON.stringify(JSON.parse(data)) : data.valueOf();
81 | } else {
82 | return JSON.stringify(data);
83 | }
84 | } catch (e) {
85 | throw new Error("Json not valid");
86 | }
87 | };
88 |
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 |
111 | function isJson(str) {
112 | try {
113 | JSON.parse(str);
114 | } catch (e) {
115 | return false;
116 | }
117 | return true;
118 | }
119 |
120 | module.exports.mutateObjectProperty = function (
121 | path,
122 | value,
123 | obj,
124 | srcPath,
125 | properties
126 | ) {
127 | let tmp = obj;
128 | let prev = null;
129 | if (path) {
130 | if (srcPath !== null && srcPath !== undefined) {
131 | this.deleteNode(srcPath, obj, properties); // delete src
132 | }
133 | const paths = path.split(".");
134 | paths.forEach((e) => {
135 | if (!Object.prototype.hasOwnProperty.call(tmp, e)) {
136 | tmp[e] = {};
137 | }
138 | prev = tmp;
139 | tmp = tmp[e];
140 | });
141 | const elem = path.split(".").pop();
142 | if (elem === "$") {
143 | obj = value; // replace root
144 | } else if (typeof value === "object" && !(value instanceof Array)) {
145 | // decrypted value
146 | if (typeof prev[elem] !== "object") {
147 | prev[elem] = {};
148 | }
149 | overrideProperties(prev[elem], value);
150 | } else {
151 | prev[elem] = value;
152 | }
153 | }
154 | return obj;
155 | };
156 |
157 | module.exports.deleteNode = function (path, obj, properties) {
158 | properties = properties || [];
159 | let prev = obj;
160 | if (
161 | path !== null &&
162 | path !== undefined &&
163 | obj !== null &&
164 | obj !== undefined
165 | ) {
166 | const paths = path.split(".");
167 | const toDelete = paths[paths.length - 1];
168 | paths.forEach((e, index) => {
169 | prev = obj;
170 | if (Object.prototype.hasOwnProperty.call(obj, e)) {
171 | obj = obj[e];
172 | if (obj && index === paths.length - 1) {
173 | delete prev[toDelete];
174 | }
175 | }
176 | });
177 | deleteRoot(obj, paths, properties);
178 | }
179 | };
180 |
181 | function overrideProperties(target, obj) {
182 | for (const k in obj) {
183 | target[k] = obj[k];
184 | }
185 | }
186 |
187 | function deleteRoot(obj, paths, properties) {
188 | if (paths.length === 1) {
189 | properties = paths[0] === "$" ? Object.keys(obj) : properties;
190 | properties.forEach((e) => {
191 | delete obj[e];
192 | });
193 | }
194 | }
195 |
196 | module.exports.getPrivateKey = function (config) {
197 | if (config.privateKey) {
198 | return getUnencryptedPrivateKey(config.privateKey);
199 | }
200 |
201 | if (config.keyStore) {
202 | if (config.keyStore.includes(".p12") || (config.keyStoreAlias && config.keyStorePassword)) {
203 | return getPrivateKey12(
204 | config.keyStore,
205 | config.keyStoreAlias,
206 | config.keyStorePassword
207 | );
208 | }
209 |
210 | return getUnencryptedPrivateKey(config.keyStore);
211 | }
212 |
213 | return null;
214 | };
215 |
216 | function getUnencryptedPrivateKey(filePath) {
217 | const fileContent = fs.readFileSync(filePath);
218 |
219 | if (!fileContent || fileContent.length < 1) {
220 | throw new Error("private key file content is empty");
221 | }
222 |
223 | // key is PEM
224 | if(fileContent.toString('utf-8').startsWith("-----BEGIN")) {
225 | return forge.pki.privateKeyFromPem(fileContent.toString('utf-8'));
226 | }
227 |
228 | // attempt to read DER
229 | return forge.pki.privateKeyFromAsn1(forge.asn1.fromDer(fileContent.toString('binary')));
230 | }
231 |
232 | function getPrivateKey12(p12Path, alias, password) {
233 | const p12Content = fs.readFileSync(p12Path, "binary");
234 |
235 | if (!p12Content || p12Content.length <= 1) {
236 | throw new Error("p12 keystore content is empty");
237 | }
238 |
239 | if (!isSet(alias)) {
240 | throw new Error("Key alias is not set");
241 | }
242 |
243 | if (!isSet(password)) {
244 | throw new Error("Keystore password is not set");
245 | }
246 |
247 | // Get asn1 from DER
248 | const p12Asn1 = forge.asn1.fromDer(p12Content, false);
249 |
250 | // Get p12 using the password
251 | const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, password);
252 |
253 | // Get Key from p12
254 | const keyObj = p12.getBags({
255 | friendlyName: alias,
256 | bagType: forge.pki.oids.pkcs8ShroudedKeyBag,
257 | }).friendlyName[0];
258 |
259 | if (!isSet(keyObj)) {
260 | throw new Error("No key found for alias [" + alias + "]");
261 | }
262 |
263 | return keyObj.key;
264 | }
265 |
266 | module.exports.readPublicCertificate = function (publicCertificatePath) {
267 | const certificateContent = fs.readFileSync(publicCertificatePath);
268 | if (!certificateContent || certificateContent.length <= 1) {
269 | throw new Error("Public certificate content is not valid");
270 | }
271 | return forge.pki.certificateFromPem(certificateContent);
272 | };
273 |
274 | module.exports.computePublicFingerprint = function (
275 | config,
276 | encryptionCertificate,
277 | encoding
278 | ) {
279 | let fingerprint = null;
280 | if (config && config.publicKeyFingerprintType) {
281 | switch (config.publicKeyFingerprintType) {
282 | case "certificate":
283 | fingerprint = publicCertificateFingerprint(
284 | encryptionCertificate,
285 | encoding
286 | );
287 | break;
288 | case "publicKey":
289 | fingerprint = this.publicKeyFingerprint(encryptionCertificate);
290 | break;
291 | }
292 | }
293 | return fingerprint;
294 | };
295 |
296 | function publicCertificateFingerprint(publicCertificate, encoding) {
297 | const bytes = forge.asn1
298 | .toDer(forge.pki.certificateToAsn1(publicCertificate))
299 | .getBytes();
300 | const md = createMessageDigest("SHA-256");
301 | md.update(bytes);
302 | return bytesToString(md.digest().getBytes(), encoding);
303 | }
304 |
305 | module.exports.publicKeyFingerprint = function(publicCertificate) {
306 | return forge.pki.getPublicKeyFingerprint(publicCertificate.publicKey, {
307 | type: "SubjectPublicKeyInfo",
308 | md: createMessageDigest("SHA-256"),
309 | encoding: c.HEX,
310 | });
311 | };
312 |
313 | function createMessageDigest(digest) {
314 | switch (digest.toUpperCase()) {
315 | case "SHA-256":
316 | case "SHA256":
317 | return forge.md.sha256.create();
318 | case "SHA-512":
319 | case "SHA512":
320 | return forge.md.sha512.create();
321 | }
322 | }
323 |
324 | module.exports.nodeVersionSupportsJWE = function () {
325 | const nodeMajorVersion = parseInt(process.version.substring(1, 3));
326 | return nodeMajorVersion > 12;
327 | };
328 |
329 | module.exports.checkConfigFieldsArePopulated = function(config, propertiesBasic, propertiesField, propertiesHeader) {
330 | const contains = (props) => {
331 | return props.every((elem) => {
332 | return config[elem] !== null && typeof config[elem] !== "undefined";
333 | });
334 | };
335 |
336 | // eslint-disable-next-line eqeqeq
337 | if (typeof config !== "object" || config == null) {
338 | throw Error("Config not valid: config should be an object.");
339 | }
340 | if (!Array.isArray(config.paths)
341 | ) {
342 | throw Error("Config not valid: paths should be an array of path element.");
343 | }
344 | if (
345 | !contains(propertiesBasic)
346 | ) {
347 | throw Error(
348 | "Config not valid: please check that all the properties are defined."
349 | );
350 | }
351 | if (
352 | propertiesField && propertiesHeader && !contains(propertiesField) && !contains(propertiesHeader)
353 | ) {
354 | throw Error(
355 | "Config not valid: please check that all the properties are defined."
356 | );
357 | }
358 | if (config["paths"].length === 0) {
359 | throw Error("Config not valid: paths should be not empty.");
360 | }
361 | return contains;
362 | };
363 |
364 | module.exports.validateRootMapping = function(config) {
365 | function multipleRoots(elem) {
366 | return (
367 | elem.length !== 1 &&
368 | elem.some((item) => {
369 | return item.obj === "$" || item.element === "$";
370 | })
371 | );
372 | }
373 |
374 | config.paths.forEach((path) => {
375 | if (multipleRoots(path.toEncrypt) || multipleRoots(path.toDecrypt)) {
376 | throw Error(
377 | "Config not valid: found multiple configurations encrypt/decrypt with root mapping"
378 | );
379 | }
380 | });
381 | };
382 |
383 | module.exports.hasConfig = function(config, endpoint) {
384 | if (config && endpoint) {
385 | endpoint = endpoint.split("?").shift();
386 | const conf = config.paths.find((elem) => {
387 | const regex = new RegExp(elem.path, "g");
388 | return endpoint.match(regex);
389 | });
390 | return conf ? conf : null;
391 | }
392 | return null;
393 | };
394 |
395 | module.exports.elemFromPath = function (path, obj) {
396 | try {
397 | let parent = null;
398 | const paths = path.split(".");
399 | if (path && paths.length > 0) {
400 | paths.forEach((e) => {
401 | parent = obj;
402 | obj = isJsonRoot(e) ? obj : obj[e];
403 | });
404 | }
405 | return {
406 | node: obj,
407 | parent: parent,
408 | };
409 | } catch (e) {
410 | return null;
411 | }
412 | };
413 |
414 | module.exports.isJsonRoot = function(elem) {
415 | return isJsonRoot(elem);
416 | };
417 |
418 | function isJsonRoot(elem){
419 | return elem === "$";
420 | }
421 |
422 | module.exports.computeBody = function (configParam, body, bodyMap) {
423 | return hasEncryptionParam(configParam, bodyMap) ? bodyMap.pop() : body;
424 | };
425 |
426 | function hasEncryptionParam(encParams, bodyMap) {
427 | return encParams && encParams.length === 1 && bodyMap && bodyMap[0];
428 | }
429 |
430 | module.exports.addEncryptedDataToBody = function(encryptedData, path, encryptedValueFieldName, body) {
431 | if (
432 | !isJsonRoot(path.obj) &&
433 | path.element !== path.obj + "." + encryptedValueFieldName
434 | ) {
435 | this.deleteNode(path.element, body);
436 | }
437 | if (encryptedValueFieldName === path.obj) {
438 | encryptedData = encryptedData[encryptedValueFieldName];
439 | }
440 | body = this.mutateObjectProperty(path.obj, encryptedData, body);
441 | return body;
442 | };
443 |
444 | module.exports.readPublicCertificateContent = function (config) {
445 | if (!config.encryptionCertificate || config.encryptionCertificate.length <= 1) {
446 | throw new Error("Public certificate content is not valid");
447 | }
448 | return forge.pki.certificateFromPem(config.encryptionCertificate);
449 | };
450 |
451 | module.exports.getPrivateKeyFromContent = function(config) {
452 | if (config.privateKey) {
453 | return forge.pki.privateKeyFromPem(config.privateKey);
454 | }
455 | return null;
456 | };
457 |
--------------------------------------------------------------------------------
/test/field-level-crypto.test.js:
--------------------------------------------------------------------------------
1 | const assert = require("assert");
2 | const rewire = require("rewire");
3 | const Crypto = rewire("../lib/mcapi/crypto/field-level-crypto");
4 | const utils = require("../lib/mcapi/utils/utils");
5 |
6 | const testConfig = require("./mock/config");
7 | const testConfigHeader = require("./mock/config-header");
8 |
9 | const iv = "6f38f3ecd8b92c2fd2537a7235deb9a8";
10 | const secretKey = "bab78b5ec588274a4dd2a60834efcf60";
11 | const encryptionCertificateText = '-----BEGIN CERTIFICATE-----'+
12 | 'MIIDITCCAgmgAwIBAgIJANLIazc8xI4iMA0GCSqGSIb3DQEBBQUAMCcxJTAjBgNV'+
13 | 'BAMMHHd3dy5qZWFuLWFsZXhpcy1hdWZhdXZyZS5jb20wHhcNMTkwMjIxMDg1MTM1'+
14 | 'WhcNMjkwMjE4MDg1MTM1WjAnMSUwIwYDVQQDDBx3d3cuamVhbi1hbGV4aXMtYXVm'+
15 | 'YXV2cmUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9Mp6gEFp'+
16 | '9E+/1SS5XrUyYKMbE7eU0dyJCfmJPz8YOkOYV7ohqwXQvjlaP/YazZ6bbmYfa2WC'+
17 | 'raOpW0o2BYijHgQ7z2a2Az87rKdAtCpZSKFW82Ijnsw++lx7EABI3tFF282ZV7LT'+
18 | '13n9m4th5Kldukk9euy+TuJqCvPu4xzE/NE+l4LFMr8rfD47EPQkrun5w/TXwkmJ'+
19 | 'rdnG9ejl3BLQO06Ns6Bs516geiYZ7RYxtI8Xnu0ZC0fpqDqjCPZBTORkiFeLocEP'+
20 | 'RbTgo1H+0xQFNdsMH1/0F1BI+hvdxlbc3+kHZFZFoeBMkR3jC8jDXOXNCMNWb13T'+
21 | 'in6HqPReO0KW8wIDAQABo1AwTjAdBgNVHQ4EFgQUDtqNZacrC6wR53kCpw/BfG2C'+
22 | 't3AwHwYDVR0jBBgwFoAUDtqNZacrC6wR53kCpw/BfG2Ct3AwDAYDVR0TBAUwAwEB'+
23 | '/zANBgkqhkiG9w0BAQUFAAOCAQEAJ09tz2BDzSgNOArYtF4lgRtjViKpV7gHVqtc'+
24 | '3xQT9ujbaxEgaZFPbf7/zYfWZfJggX9T54NTGqo5AXM0l/fz9AZ0bOm03rnF2I/F'+
25 | '/ewhSlHYzvKiPM+YaswaRo1M1UPPgKpLlRDMO0u5LYiU5ICgCNm13TWgjBlzLpP6'+
26 | 'U4z2iBNq/RWBgYxypi/8NMYZ1RcCrAVSt3QnW6Gp+vW/HrE7KIlAp1gFdme3Xcx1'+
27 | 'vDRpA+MeeEyrnc4UNIqT/4bHGkKlIMKdcjZgrFfEJVFav3eJ4CZ7ZSV6Bx+9yRCL'+
28 | 'DPGlRJLISxgwsOTuUmLOxjotRxO8TdR5e1V+skEtfEctMuSVYA=='+
29 | '-----END CERTIFICATE-----'
30 |
31 | describe("Field Level Crypto", () => {
32 | describe("#new Crypto", () => {
33 | it("with empty config", () => {
34 | assert.throws(
35 | () => new Crypto({}),
36 | /Config not valid: paths should be an array of path element./
37 | );
38 | });
39 |
40 | it("with string config", () => {
41 | assert.throws(
42 | () => new Crypto(""),
43 | /Config not valid: config should be an object./
44 | );
45 | });
46 |
47 | it("with wrong encoding", () => {
48 | const config = JSON.parse(JSON.stringify(testConfig));
49 | config["dataEncoding"] = "foo";
50 | assert.throws(
51 | () => new Crypto(config),
52 | /Config not valid: dataEncoding should be 'hex' or 'base64'/
53 | );
54 | });
55 |
56 | it("with one property not defined", () => {
57 | const config = JSON.parse(JSON.stringify(testConfig));
58 | delete config["ivFieldName"];
59 | assert.throws(
60 | () => new Crypto(config),
61 | /Config not valid: please check that all the properties are defined./
62 | );
63 | });
64 |
65 | it("with empty paths", () => {
66 | const config = JSON.parse(JSON.stringify(testConfig));
67 | config.paths = [];
68 | assert.throws(
69 | () => new Crypto(config),
70 | /Config not valid: paths should be not empty./
71 | );
72 | });
73 |
74 | it("with valid config", () => {
75 | assert.doesNotThrow(() => new Crypto(testConfig));
76 | });
77 |
78 | it("with valid config with private keystore", () => {
79 | const config = JSON.parse(JSON.stringify(testConfig));
80 | delete config.privateKey;
81 | config.keyStore = "./test/res/keys/pkcs12/test_key.p12";
82 | config.keyStoreAlias = "mykeyalias";
83 | config.keyStorePassword = "Password1";
84 | assert.doesNotThrow(() => {
85 | new Crypto(config);
86 | });
87 | });
88 |
89 | it("with valid config with private pkcs1 pem keystore", () => {
90 | const config = JSON.parse(JSON.stringify(testConfig));
91 | delete config.privateKey;
92 | config.keyStore = "./test/res/keys/pkcs1/test_key.pem";
93 | assert.doesNotThrow(() => {
94 | new Crypto(config);
95 | });
96 | });
97 |
98 | it("with valid config with private pkcs8 pem keystore", () => {
99 | const config = JSON.parse(JSON.stringify(testConfig));
100 | delete config.privateKey;
101 | config.keyStore = "./test/res/keys/pkcs8/test_key.pem";
102 | assert.doesNotThrow(() => {
103 | new Crypto(config);
104 | });
105 | });
106 |
107 | it("with valid config with private pkcs8 der keystore", () => {
108 | const config = JSON.parse(JSON.stringify(testConfig));
109 | delete config.privateKey;
110 | config.keyStore = "./test/res/keys/pkcs8/test_key.der";
111 | assert.doesNotThrow(() => {
112 | new Crypto(config);
113 | });
114 | });
115 |
116 | it("with valid config header", () => {
117 | assert.doesNotThrow(() => new Crypto(testConfigHeader));
118 | });
119 |
120 | it("with config header missing iv", () => {
121 | const config = JSON.parse(JSON.stringify(testConfigHeader));
122 | delete config["ivHeaderName"];
123 | assert.throws(
124 | () => new Crypto(config),
125 | /Config not valid: please check that all the properties are defined./
126 | );
127 | });
128 |
129 | it("without publicKeyFingerprintType", () => {
130 | const config = JSON.parse(JSON.stringify(testConfig));
131 | delete config["publicKeyFingerprintType"];
132 | assert.throws(
133 | () => new Crypto(config),
134 | /Config not valid: publicKeyFingerprintType should be: 'certificate' or 'publicKey'/
135 | );
136 | });
137 |
138 | it("without publicKeyFingerprintType, but providing the publicKeyFingerprint", () => {
139 | const config = JSON.parse(JSON.stringify(testConfig));
140 | delete config["publicKeyFingerprintType"];
141 | config.publicKeyFingerprint = "abc";
142 | assert.ok((new Crypto(config).publicKeyFingerprint = "abc"));
143 | });
144 |
145 | it("with wrong publicKeyFingerprintType", () => {
146 | const config = JSON.parse(JSON.stringify(testConfig));
147 | config.publicKeyFingerprintType = "foobar";
148 | assert.throws(
149 | () => new Crypto(config),
150 | /Config not valid: publicKeyFingerprintType should be: 'certificate' or 'publicKey'/
151 | );
152 | });
153 |
154 | it("with right publicKeyFingerprintType: certificate", () => {
155 | const config = JSON.parse(JSON.stringify(testConfig));
156 | config.publicKeyFingerprintType = "certificate";
157 | assert.doesNotThrow(() => new Crypto(config));
158 | });
159 |
160 | it("with right publicKeyFingerprintType: publicKey", () => {
161 | const config = JSON.parse(JSON.stringify(testConfig));
162 | config.publicKeyFingerprintType = "publicKey";
163 | assert.doesNotThrow(() => new Crypto(config));
164 | });
165 |
166 | it("without keyStore and privateKey", () => {
167 | const config = JSON.parse(JSON.stringify(testConfig));
168 | delete config["privateKey"];
169 | delete config["keyStore"];
170 | assert.doesNotThrow(() => new Crypto(config));
171 | });
172 |
173 | it("with multiple roots (encrypt)", () => {
174 | const config = JSON.parse(JSON.stringify(testConfig));
175 | config.paths[2].toEncrypt.push(config.paths[2].toEncrypt[0]);
176 | assert.throws(
177 | () => new Crypto(config),
178 | /Config not valid: found multiple configurations encrypt\/decrypt with root mapping/
179 | );
180 | });
181 |
182 | it("with multiple roots (decrypt)", () => {
183 | const config = JSON.parse(JSON.stringify(testConfig));
184 | config.paths[2].toDecrypt.push(config.paths[2].toDecrypt[0]);
185 | config.paths[2].toDecrypt[1].obj = "abc";
186 | assert.throws(
187 | () => new Crypto(config),
188 | /Config not valid: found multiple configurations encrypt\/decrypt with root mapping/
189 | );
190 | });
191 |
192 | it("With useCertificateContent enabled, without valid encryption certificate content", () => {
193 | const config = JSON.parse(JSON.stringify(testConfig));
194 | config.useCertificateContent = true;
195 | assert.throws(
196 | () => new Crypto(config),
197 | /Invalid PEM formatted message./
198 | );
199 | });
200 |
201 | it("With useCertificateContent enabled, with valid encryption certificate content and without private key", () => {
202 | const config = JSON.parse(JSON.stringify(testConfig));
203 | config.useCertificateContent = true;
204 | config.encryptionCertificate = encryptionCertificateText;
205 | delete config["privateKey"];
206 | assert.doesNotThrow(() => new Crypto(config));
207 | });
208 |
209 | it("With useCertificateContent enabled, without encryptionCertificate", () => {
210 | const config = JSON.parse(JSON.stringify(testConfig));
211 | config.useCertificateContent = true;
212 | config.encryptionCertificate = null;
213 | assert.throws(
214 | () => new Crypto(config),
215 | /Config not valid: please check that all the properties are defined/
216 | );
217 | });
218 | });
219 |
220 | describe("#encryptData()", () => {
221 | let crypto;
222 | before(() => {
223 | crypto = new Crypto(testConfig);
224 | });
225 |
226 | it("with empty string", () => {
227 | assert.throws(() => {
228 | crypto.encryptData({ data: "" });
229 | }, /Json not valid/);
230 | });
231 |
232 | it("with valid object, iv and secretKey", () => {
233 | const data = JSON.stringify({ text: "message" });
234 | const resp = crypto.encryptData({
235 | data: data,
236 | iv: utils.stringToBytes(iv, "hex"),
237 | secretKey: utils.stringToBytes(secretKey, "hex"),
238 | });
239 | assert.ok(
240 | resp.encryptedData ===
241 | "3590b63d1520a57bd4cd1414a7a75f47d65f99e1427d6cfe744d72ee60f2b232"
242 | );
243 | assert.ok(
244 | resp.publicKeyFingerprint ===
245 | "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279"
246 | );
247 | });
248 |
249 | it("without publicKeyFingerprint", () => {
250 | const crypto = new Crypto(testConfig);
251 | crypto.publicKeyFingerprint = null;
252 | const data = JSON.stringify({ text: "message" });
253 | const resp = crypto.encryptData({
254 | data: data,
255 | });
256 | assert.ok(resp);
257 | assert.ok(!resp.publicKeyFingerprint);
258 | });
259 |
260 | it("with valid object", () => {
261 | const data = JSON.stringify({ text: "message" });
262 | const resp = crypto.encryptData({
263 | data: data,
264 | });
265 | assert.ok(resp);
266 | assert.ok(resp.encryptedKey);
267 | assert.ok(resp.encryptedData);
268 | assert.ok(resp.iv);
269 | assert.ok(resp.oaepHashingAlgorithm);
270 | });
271 |
272 | it("with custom ivFieldName object", () => {
273 | const config = JSON.parse(JSON.stringify(testConfig));
274 | config.ivFieldName = 'IvCustomName';
275 | const crypto = new Crypto(config);
276 | const data = JSON.stringify({ text: "message with custom ivFieldName" });
277 | const resp = crypto.encryptData({
278 | data: data,
279 | });
280 | assert.ok(resp);
281 | assert.ok(resp.encryptedKey);
282 | assert.ok(resp.encryptedData);
283 | assert.ok(resp.IvCustomName);
284 | assert.ok(!resp.iv);
285 | assert.ok(resp.oaepHashingAlgorithm);
286 | });
287 |
288 | it("with useCertificateContent", () => {
289 | const config = JSON.parse(JSON.stringify(testConfig));
290 | config.ivFieldName = 'IvCustomName';
291 | config.useCertificateContent = true;
292 | config.encryptionCertificate = encryptionCertificateText;
293 | delete config["privateKey"];
294 | const crypto = new Crypto(config);
295 | const data = JSON.stringify({ text: "message with custom ivFieldName" });
296 | const resp = crypto.encryptData({
297 | data: data,
298 | });
299 | assert.ok(resp);
300 | assert.ok(resp.encryptedKey);
301 | assert.ok(resp.encryptedData);
302 | assert.ok(resp.IvCustomName);
303 | assert.ok(!resp.iv);
304 | assert.ok(resp.oaepHashingAlgorithm);
305 | });
306 |
307 |
308 | });
309 |
310 |
311 | describe("#decryptData()", () => {
312 | let crypto;
313 | before(() => {
314 | crypto = new Crypto(testConfig);
315 | });
316 |
317 | it("with null", () => {
318 | assert.throws(() => {
319 | crypto.decryptData(null);
320 | }, /Input not valid/);
321 | });
322 |
323 | it("with empty string", () => {
324 | assert.throws(() => {
325 | crypto.decryptData("");
326 | }, /Input not valid/);
327 | });
328 |
329 | it("with valid object", () => {
330 | const resp = crypto.decryptData(
331 | "3590b63d1520a57bd4cd1414a7a75f47d65f99e1427d6cfe744d72ee60f2b232",
332 | iv,
333 | "SHA-512",
334 | "e283a661efa235fbc5e7243b7b78914a7f33574eb66cc1854829f7debfce4163f3ce86ad2c3ed2c8fe97b2258ab8a158281147698b7fddf5e82544b0b637353d2c204798f014112a5e278db0b29ad852b1417dc761593fad3f0a1771797771796dc1e8ae916adaf3f4486aa79af9d4028bc8d17399d50c80667ea73a8a5d1341a9160f9422aaeb0b4667f345ea637ac993e80a452cb8341468483b7443f764967264aaebb2cad4513e4922d076a094afebcf1c71b53ba3cfedb736fa2ca5de5c1e2aa88b781d30c27debd28c2f5d83e89107d5214e3bb3fe186412d78cefe951e384f236e55cd3a67fb13c0d6950f097453f76e7679143bd4e62d986ce9dc770"
335 | );
336 | assert.ok(JSON.stringify(resp) === JSON.stringify({ text: "message" }));
337 | });
338 | });
339 |
340 | describe("#generateSecretKey", () => {
341 | it("not valid algorithm", () => {
342 | const generateSecretKey = Crypto.__get__("generateSecretKey");
343 | assert.throws(() => {
344 | generateSecretKey("ABC");
345 | }, /Unsupported symmetric algorithm/);
346 | });
347 | });
348 |
349 | describe("#newEncryptionParams", () => {
350 | let crypto;
351 | before(() => {
352 | crypto = new Crypto(testConfig);
353 | });
354 | it("without options", () => {
355 | const params = crypto.newEncryptionParams();
356 | assert.ok(params.iv);
357 | assert.ok(params.secretKey);
358 | assert.ok(params.encryptedKey);
359 | assert.ok(params.oaepHashingAlgorithm);
360 | assert.ok(params.publicKeyFingerprint);
361 | assert.ok(params.encoded);
362 | assert.ok(params.encoded.iv);
363 | assert.ok(params.encoded.secretKey);
364 | assert.ok(params.encoded.encryptedKey);
365 | });
366 | });
367 |
368 | describe("#createOAEPOptions", () => {
369 | const createOAEPOptions = Crypto.__get__("createOAEPOptions");
370 |
371 | it("not valid asymmetricCipher", () => {
372 | assert.ok(!createOAEPOptions("foobar"));
373 | });
374 |
375 | it("valid asymmetricCipher and oaepHashingAlgorithm", () => {
376 | const opts = createOAEPOptions("OAEP", "SHA-256");
377 | assert.ok(opts.md);
378 | assert.ok(opts.mgf1);
379 | });
380 | });
381 | });
382 |
--------------------------------------------------------------------------------
/test/utils.test.js:
--------------------------------------------------------------------------------
1 | const assert = require("assert");
2 | const utils = require("../lib/mcapi/utils/utils");
3 | const testConfig = require("./mock/config");
4 |
5 | describe("Utils", () => {
6 | describe("#isSet", () => {
7 | it("when null", () => {
8 | assert.ok(utils.isSet(null) === false);
9 | });
10 |
11 | it("when undefined", () => {
12 | assert.ok(utils.isSet(undefined) === false);
13 | });
14 |
15 | it("when obj defined", () => {
16 | assert.ok(utils.isSet({}) === true);
17 | });
18 |
19 | it("when string defined and empty", () => {
20 | assert.ok(utils.isSet("") === false);
21 | });
22 |
23 | it("when string defined and not empty", () => {
24 | assert.ok(utils.isSet("not empty") === true);
25 | });
26 | });
27 |
28 | describe("#bytesToString", () => {
29 | it("when null", () => {
30 | assert.throws(() => {
31 | utils.bytesToString(null, "hex");
32 | });
33 | });
34 |
35 | it("when undefined", () => {
36 | assert.throws(() => {
37 | utils.bytesToString(undefined, "hex");
38 | });
39 | });
40 |
41 | it("when empty obj", () => {
42 | const res = utils.bytesToString({}, "hex");
43 | assert.ok(res === "");
44 | });
45 |
46 | it("when empty str", () => {
47 | const res = utils.bytesToString("", "hex");
48 | assert.ok(res === "");
49 | });
50 |
51 | it("when wrong encoding", () => {
52 | const res = utils.bytesToString("abc", "XXX");
53 | assert.ok(res === "YWJj"); // default is base64
54 | });
55 |
56 | it("when test str hex", () => {
57 | const res = utils.bytesToString("test", "hex");
58 | assert.ok(res === "74657374");
59 | });
60 |
61 | it("when test str base64", () => {
62 | const res = utils.bytesToString("test", "base64");
63 | assert.ok(res === "dGVzdA==");
64 | });
65 | });
66 |
67 | describe("#stringToBytes", () => {
68 | it("when null", () => {
69 | assert.throws(() => {
70 | utils.stringToBytes(null, "hex");
71 | });
72 | });
73 |
74 | it("when undefined", () => {
75 | assert.throws(() => {
76 | utils.stringToBytes(undefined, "hex");
77 | });
78 | });
79 |
80 | it("when empty obj", () => {
81 | const res = utils.stringToBytes({}, "hex");
82 | assert.ok(res === "");
83 | });
84 |
85 | it("when empty str", () => {
86 | const res = utils.stringToBytes("", "hex");
87 | assert.ok(res === "");
88 | });
89 |
90 | it("when wrong encoding", () => {
91 | const res = utils.stringToBytes("dGVzdGluZ3V0aWxz", "XXX");
92 | assert.ok(res === "testingutils");
93 | });
94 |
95 | it("when test str hex", () => {
96 | const res = utils.stringToBytes("74657374696E677574696C73", "hex");
97 | assert.ok(res === "testingutils");
98 | });
99 |
100 | it("when test str base64", () => {
101 | const res = utils.stringToBytes("dGVzdGluZ3V0aWxz", "base64");
102 | assert.ok(res === "testingutils");
103 | });
104 |
105 | it("encoding and decoding base64", () => {
106 | const str = "dGVzdGluZ3V0aWxz";
107 | const res = utils.stringToBytes(str, "base64");
108 | assert.ok(utils.bytesToString(res, "base64") === str);
109 | });
110 | });
111 |
112 | describe("#jsonToString", () => {
113 | it("when null", () => {
114 | assert.throws(() => {
115 | utils.jsonToString(null);
116 | });
117 | });
118 |
119 | it("when undefined", () => {
120 | assert.throws(() => {
121 | utils.jsonToString(undefined);
122 | });
123 | });
124 |
125 | it("when empty obj", () => {
126 | const res = utils.jsonToString({});
127 | assert.ok(res === "{}");
128 | });
129 |
130 | it("when empty str", () => {
131 | assert.throws(() => utils.jsonToString(""));
132 | });
133 |
134 | it("correct json from object", () => {
135 | const res = utils.jsonToString({ field: "value" });
136 | assert.strictEqual(res, '{"field":"value"}');
137 | });
138 |
139 | it("correct json from string", () => {
140 | const res = utils.jsonToString('{"field": "value"}');
141 | assert.strictEqual(res, '{"field":"value"}');
142 | });
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 | });
195 | });
196 |
197 | describe("#mutateObjectProperty", () => {
198 | it("change object value", () => {
199 | const obj = {
200 | first: {
201 | second: {
202 | third: {
203 | field: "value",
204 | },
205 | },
206 | },
207 | };
208 | const path = "first.second.third";
209 | utils.mutateObjectProperty(path, "changed", obj);
210 | assert.ok(obj.first.second.third === "changed");
211 | });
212 |
213 | it("change object value, change it but not delete", () => {
214 | const obj = {
215 | first: {
216 | second: {
217 | third: {
218 | field: "value",
219 | },
220 | },
221 | },
222 | };
223 | const path = "first.second.third";
224 | utils.mutateObjectProperty(path, "changed", obj);
225 | assert.ok(obj.first.second.third === "changed");
226 | });
227 |
228 | it("field not found, create it", () => {
229 | const obj = {
230 | first: {
231 | second: {
232 | third: {
233 | field: "value",
234 | },
235 | },
236 | },
237 | };
238 | const objStr = JSON.stringify({
239 | first: { second: { third: { field: "value" }, not_exists: "changed" } },
240 | });
241 | const path = "first.second.not_exists";
242 | utils.mutateObjectProperty(path, "changed", obj);
243 | assert.ok(objStr === JSON.stringify(obj));
244 | });
245 |
246 | it("field not found, create it, long path", () => {
247 | const obj = {
248 | first: {
249 | second: {
250 | third: {
251 | field: "value",
252 | },
253 | },
254 | },
255 | };
256 | const objStr = JSON.stringify({
257 | first: {
258 | second: {
259 | third: {
260 | field: "value",
261 | },
262 | },
263 | },
264 | foo: {
265 | bar: {
266 | yet: {
267 | another: {
268 | foo: {
269 | bar: "changed",
270 | },
271 | },
272 | },
273 | },
274 | },
275 | });
276 | const path = "foo.bar.yet.another.foo.bar";
277 | utils.mutateObjectProperty(path, "changed", obj);
278 | assert.ok(objStr === JSON.stringify(obj));
279 | });
280 |
281 | it("first part of path is correct, but field not found, create it", () => {
282 | const obj = {
283 | first: {
284 | second: {
285 | third: {
286 | field: "value",
287 | },
288 | },
289 | },
290 | };
291 | const objStr = JSON.stringify({
292 | first: {
293 | second: {
294 | third: {
295 | field: "value",
296 | },
297 | },
298 | foo: {
299 | third: "changed",
300 | },
301 | },
302 | });
303 | const path = "first.foo.third";
304 | utils.mutateObjectProperty(path, "changed", obj);
305 | assert.ok(objStr === JSON.stringify(obj));
306 | });
307 |
308 | it("path is null", () => {
309 | const obj = {
310 | first: {
311 | second: {
312 | third: {
313 | field: "value",
314 | },
315 | },
316 | },
317 | };
318 | const objStr = JSON.stringify(obj);
319 | utils.mutateObjectProperty(null, "changed", obj);
320 | assert.ok(objStr === JSON.stringify(obj));
321 | });
322 | });
323 |
324 | describe("#deleteNode", () => {
325 | it("with nulls", () => {
326 | assert.doesNotThrow(() => {
327 | utils.deleteNode(null, null, null);
328 | utils.deleteNode("path.to.foo", null, null);
329 | const body = {};
330 | utils.deleteNode("path.to.foo", body);
331 | assert.ok(JSON.stringify(body) === JSON.stringify({}));
332 | });
333 | });
334 |
335 | it("not found path, shouldn't remove it", () => {
336 | const body = { path: { to: { foo: { field: "value" } } } };
337 | const str = JSON.stringify(body);
338 | utils.deleteNode("path.to.notfound", body, null);
339 | assert.ok(str === JSON.stringify(body));
340 | });
341 |
342 | it("path found, should remove it", () => {
343 | const body = { path: { to: { foo: { field: "value" } } } };
344 | utils.deleteNode("path.to.foo", body, null);
345 | assert.ok(JSON.stringify({ path: { to: {} } }) === JSON.stringify(body));
346 | });
347 |
348 | it("root path, without properties, shouldn't remove", () => {
349 | const body = { path: { to: { foo: { field: "value" } } } };
350 | const str = JSON.stringify(body);
351 | utils.deleteNode("", body, null);
352 | assert.ok(str === JSON.stringify(body));
353 | });
354 |
355 | it("root path, with properties, should remove the properties", () => {
356 | const body = {
357 | path: { to: { foo: { field: "value" } } },
358 | prop: "prop",
359 | prop2: "prop2",
360 | };
361 | utils.deleteNode("", body, ["prop", "prop2"]);
362 | assert.ok(
363 | JSON.stringify({ path: { to: { foo: { field: "value" } } } }) ===
364 | JSON.stringify(body)
365 | );
366 | });
367 | });
368 |
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 |
456 | describe("#getPrivateKey", () => {
457 | it("empty p12 file", () => {
458 | assert.throws(() => {
459 | utils.getPrivateKey({
460 | keyStore: "./test/res/empty.p12",
461 | keyStoreAlias: "mykeyalias",
462 | keyStorePassword: "Password1", });
463 | }, /p12 keystore content is empty/);
464 | });
465 |
466 | it("empty p12 file unknown extension", () => {
467 | assert.throws(() => {
468 | utils.getPrivateKey({
469 | keyStore: "./test/res/keys/pkcs12/empty_key_container.unknown_extension",
470 | keyStoreAlias: "mykeyalias",
471 | keyStorePassword: "Password1", });
472 | }, /p12 keystore content is empty/);
473 | });
474 |
475 | it("empty alias", () => {
476 | assert.throws(() => {
477 | utils.getPrivateKey({
478 | keyStore: "./test/res/keys/pkcs12/test_key_container.p12",
479 | });
480 | }, /Key alias is not set/);
481 | });
482 |
483 | it("empty password", () => {
484 | assert.throws(() => {
485 | utils.getPrivateKey({
486 | keyStore: "./test/res/keys/pkcs12/test_key_container.p12",
487 | keyStoreAlias: "keyalias",
488 | });
489 | }, /Keystore password is not set/);
490 | });
491 |
492 | it("valid p12", () => {
493 | const pk = utils.getPrivateKey({
494 | keyStore: "./test/res/keys/pkcs12/test_key_container.p12",
495 | keyStoreAlias: "mykeyalias",
496 | keyStorePassword: "Password1",
497 | });
498 | assert.ok(pk);
499 | });
500 |
501 | it("valid p12, alias not found", () => {
502 | assert.throws(() => {
503 | utils.getPrivateKey({
504 | keyStore: "./test/res/keys/pkcs12/test_key_container.p12",
505 | keyStoreAlias: "mykeyalias1",
506 | keyStorePassword: "Password1",
507 | });
508 | }, /No key found for alias \[mykeyalias1\]/);
509 | });
510 |
511 | it("valid p12, unknown extension", () => {
512 | const pk = utils.getPrivateKey({
513 | keyStore: "./test/res/keys/pkcs12/test_key_container.unknown_extension",
514 | keyStoreAlias: "mykeyalias",
515 | keyStorePassword: "Password1",
516 | });
517 | assert.ok(pk);
518 | });
519 |
520 | it("empty unencrypted file keyStore", () => {
521 | assert.throws(() => {
522 | utils.getPrivateKey({ keyStore: "./test/res/empty.pem" });
523 | }, /private key file content is empty/);
524 | });
525 |
526 | it("valid pkcs8 pem in keyStore", () => {
527 | const pk = utils.getPrivateKey({
528 | keyStore: "./test/res/keys/pkcs8/test_key.pem",
529 | });
530 | assert.ok(pk);
531 | });
532 |
533 | it("valid pkcs1 pem in keyStore", () => {
534 | const pk = utils.getPrivateKey({
535 | keyStore: "./test/res/keys/pkcs1/test_key.pem",
536 | });
537 | assert.ok(pk);
538 | });
539 |
540 | it("valid pkcs8 der in keyStore", () => {
541 | const pk = utils.getPrivateKey({
542 | keyStore: "./test/res/keys/pkcs8/test_key.der",
543 | });
544 | assert.ok(pk);
545 | });
546 |
547 | it("valid pkcs1 der in keyStore", () => {
548 | const pk = utils.getPrivateKey({
549 | keyStore: "./test/res/keys/pkcs1/test_key.der",
550 | });
551 | assert.ok(pk);
552 | });
553 |
554 | it("empty unencrypted file in privateKey", () => {
555 | assert.throws(() => {
556 | utils.getPrivateKey({ privateKey: "./test/res/empty.pem" });
557 | }, /private key file content is empty/);
558 | });
559 |
560 | it("valid pkcs8 pem in privateKey", () => {
561 | const pk = utils.getPrivateKey({
562 | privateKey: "./test/res/keys/pkcs8/test_key.pem",
563 | });
564 | assert.ok(pk);
565 | });
566 |
567 | it("valid pkcs1 pem in privateKey", () => {
568 | const pk = utils.getPrivateKey({
569 | privateKey: "./test/res/keys/pkcs1/test_key.pem",
570 | });
571 | assert.ok(pk);
572 | });
573 |
574 | it("valid pkcs8 der in privateKey", () => {
575 | const pk = utils.getPrivateKey({
576 | privateKey: "./test/res/keys/pkcs8/test_key.der",
577 | });
578 | assert.ok(pk);
579 | });
580 |
581 | it("valid pkcs1 der in privateKey", () => {
582 | const pk = utils.getPrivateKey({
583 | privateKey: "./test/res/keys/pkcs1/test_key.der",
584 | });
585 | assert.ok(pk);
586 | });
587 | });
588 |
589 | describe("#readPublicCertificate", () => {
590 | it("not valid key", () => {
591 | assert.throws(() => {
592 | utils.readPublicCertificate("./test/res/empty.key");
593 | }, /Public certificate content is not valid/);
594 | });
595 | });
596 |
597 | describe("#ToEncodedString", () => {
598 | it("not valid key", () => {
599 | assert.throws(() => {
600 | utils.readPublicCertificate("./test/res/empty.key");
601 | }, /Public certificate content is not valid/);
602 | });
603 | });
604 |
605 | describe("#utils.hasConfig", () => {
606 |
607 | it("when valid config, not found endpoint", () => {
608 | const ret = utils.hasConfig(testConfig, "/endpoint");
609 | assert.ok(ret === null);
610 | });
611 |
612 | it("when valid config, found endpoint", () => {
613 | const ret = utils.hasConfig(testConfig, "/resource");
614 | assert.ok(ret);
615 | });
616 |
617 | it("when config is null", () => {
618 | const ret = utils.hasConfig(null, "/resource");
619 | assert.ok(ret == null);
620 | });
621 |
622 | it("when path has wildcard", () => {
623 | const ret = utils.hasConfig(
624 | testConfig,
625 | "https://api.example.com/mappings/0123456"
626 | );
627 | assert.ok(ret.toEncrypt[0].element === "elem2.encryptedData");
628 | assert.ok(ret);
629 | });
630 | });
631 |
632 | describe("#utils.elemFromPath", () => {
633 | it("valid path", () => {
634 | const res = utils.elemFromPath("elem1.elem2", { elem1: { elem2: "test" } });
635 | assert.ok(res.node === "test");
636 | assert.ok(
637 | JSON.stringify(res.parent) === JSON.stringify({ elem2: "test" })
638 | );
639 | });
640 |
641 | it("not valid path", () => {
642 | const res = utils.elemFromPath("elem1.elem2", { elem2: "test" });
643 | assert.ok(!res);
644 | });
645 | });
646 |
647 | });
648 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # client-encryption-nodejs
2 |
3 |
4 |
5 |
6 |
7 |
8 | [](https://github.com/Mastercard/client-encryption-nodejs/actions?query=workflow%3A%22Build+%26+Test%22)
9 | [](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-nodejs)
10 | [](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-nodejs)
11 | [](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-nodejs)
12 | [](https://github.com/Mastercard/client-encryption-nodejs/actions?query=workflow%3A%22broken+links%3F%22)
13 | [](http://www.npmjs.com/package/mastercard-client-encryption)
14 | [](https://github.com/Mastercard/client-encryption-nodejs/blob/master/LICENSE)
15 |
16 | ## Table of Contents
17 |
18 | - [Overview](#overview)
19 | - [Compatibility](#compatibility)
20 | - [References](#references)
21 | - [Versioning and Deprecation Policy](#versioning)
22 | - [Usage](#usage)
23 | - [Prerequisites](#prerequisites)
24 | - [Adding the Library to Your Project](#adding-the-libraries-to-your-project)
25 | - [Performing Field Level Encryption and Decryption](#performing-field-level-encryption-and-decryption)
26 | - [Performing JWE Encryption and Decryption](#performing-jwe-encryption-and-decryption)
27 | - [Integrating with OpenAPI Generator API Client Libraries](#integrating-with-openapi-generator-api-client-libraries)
28 |
29 | ## Overview
30 |
31 | NodeJS library for Mastercard API compliant payload encryption/decryption.
32 |
33 | ### Compatibility
34 | - NodeJS 17+
35 |
36 | There shouldn't be any Node compatibility issues with this package, but it's a good idea to keep your Node versions up-to-date. It is recommended that you use one of the LTS Node.js releases, or one of the more general recent releases. A Node version manager such as `nvm` (_Mac_ and _Linux_) or `nvm-windows` is a good way to stay on top of this.
37 |
38 | ### References
39 |
40 |
41 |
42 | ### Versioning and Deprecation Policy
43 | * [Mastercard Versioning and Deprecation Policy](https://github.com/Mastercard/.github/blob/main/CLIENT_LIBRARY_DEPRECATION_POLICY.md)
44 |
45 | ## Usage
46 |
47 | ### Prerequisites
48 |
49 | Before using this library, you will need to set up a project in the [Mastercard Developers Portal](https://developer.mastercard.com).
50 |
51 | As part of this set up, you'll receive:
52 |
53 | - A public request encryption certificate (aka _Client Encryption Keys_)
54 | - A private response decryption key (aka _Mastercard Encryption Keys_)
55 |
56 | ### Installation
57 |
58 | If you want to use **mastercard-client-encryption** with [Node.js](https://nodejs.org/), it is available through `npm`:
59 |
60 | - [http://npmjs.org/package/mastercard-client-encryption](http://npmjs.org/package/mastercard-client-encryption)
61 |
62 | Adding the library to your project:
63 |
64 | ```shell
65 | npm install mastercard-client-encryption
66 | ```
67 |
68 | You can then use it as a regular module:
69 |
70 | ```js
71 | const clientEncryption = require("mastercard-client-encryption");
72 | ```
73 |
74 | ### Performing Field Level Encryption and Decryption
75 |
76 | - [Introduction](#introduction)
77 | - [Configuring the Field Level Encryption](#configuring-the-field-level-encryption)
78 | - [Performing Encryption](#performing-encryption)
79 | - [Performing Decryption](#performing-decryption)
80 |
81 | #### Introduction
82 |
83 | The core methods responsible for payload encryption and decryption are `encryptData` and `decryptData` in the `FieldLevelEncryption` class.
84 |
85 | - `encrypt()` usage:
86 |
87 | ```js
88 | const fle = new clientEncryption.FieldLevelEncryption(config);
89 | // …
90 | let encryptedRequestPayload = fle.encrypt(endpoint, header, body);
91 | ```
92 |
93 | - `decrypt()` usage:
94 |
95 | ```js
96 | const fle = new clientEncryption.FieldLevelEncryption(config);
97 | // …
98 | let responsePayload = fle.decrypt(encryptedResponsePayload);
99 | ```
100 |
101 | #### Configuring the Field Level Encryption
102 |
103 | `FieldLevelEncryption` needs a config object to instruct how to decrypt/decrypt the payloads. Example:
104 |
105 | ```js
106 | const config = {
107 | paths: [
108 | {
109 | path: "/resource",
110 | toEncrypt: [
111 | {
112 | /* path to element to be encrypted in request json body */
113 | element: "path.to.foo",
114 | /* path to object where to store encryption fields in request json body */
115 | obj: "path.to.encryptedFoo",
116 | },
117 | ],
118 | toDecrypt: [
119 | {
120 | /* path to element where to store decrypted fields in response object */
121 | element: "path.to.encryptedFoo",
122 | /* path to object with encryption fields */
123 | obj: "path.to.foo",
124 | },
125 | ],
126 | },
127 | ],
128 | ivFieldName: "iv",
129 | encryptedKeyFieldName: "encryptedKey",
130 | encryptedValueFieldName: "encryptedData",
131 | dataEncoding: "hex",
132 | encryptionCertificate: "./path/to/public.cert",
133 | privateKey: "./path/to/your/private.key",
134 | oaepPaddingDigestAlgorithm: "SHA-256",
135 | };
136 | ```
137 |
138 | #### Performing Encryption
139 |
140 | Call `FieldLevelEncryption.encrypt()` with a JSON request payload, and optional `header` object.
141 |
142 | Example using the configuration [above](#configuring-the-field-level-encryption):
143 |
144 | ```js
145 | const payload = {
146 | path: {
147 | to: {
148 | foo: {
149 | sensitive: "this is a secret!",
150 | sensitive2: "this is a super-secret!",
151 | },
152 | },
153 | },
154 | };
155 | const fle = new (require("mastercard-client-encryption").FieldLevelEncryption)(
156 | config
157 | );
158 | // …
159 | let responsePayload = fle.encrypt("/resource1", header, payload);
160 | ```
161 |
162 | Output:
163 |
164 | ```json
165 | {
166 | "path": {
167 | "to": {
168 | "encryptedFoo": {
169 | "iv": "7f1105fb0c684864a189fb3709ce3d28",
170 | "encryptedKey": "67f467d1b653d98411a0c6d3c…ffd4c09dd42f713a51bff2b48f937c8",
171 | "encryptedData": "b73aabd267517fc09ed72455c2…dffb5fa04bf6e6ce9ade1ff514ed6141",
172 | "publicKeyFingerprint": "80810fc13a8319fcf0e2e…82cc3ce671176343cfe8160c2279",
173 | "oaepHashingAlgorithm": "SHA256"
174 | }
175 | }
176 | }
177 | }
178 | ```
179 |
180 | #### Performing Decryption
181 |
182 | Call `FieldLevelEncryption.decrypt()` with an (encrypted) `response` object with the following fields:
183 |
184 | - `body`: json payload
185 | - `request.url`: requesting url
186 | - `header`: _optional_, header object
187 |
188 | Example using the configuration [above](#configuring-the-field-level-encryption):
189 |
190 | ```js
191 | const response = {};
192 | response.request = { url: "/resource1" };
193 | response.body = {
194 | path: {
195 | to: {
196 | encryptedFoo: {
197 | iv: "e5d313c056c411170bf07ac82ede78c9",
198 | encryptedKey:
199 | "e3a56746c0f9109d18b3a2652b76…f16d8afeff36b2479652f5c24ae7bd",
200 | encryptedData:
201 | "809a09d78257af5379df0c454dcdf…353ed59fe72fd4a7735c69da4080e74f",
202 | oaepHashingAlgorithm: "SHA256",
203 | publicKeyFingerprint: "80810fc13a8319fcf0e2e…3ce671176343cfe8160c2279",
204 | },
205 | },
206 | },
207 | };
208 | const fle = new (require("mastercard-client-encryption").FieldLevelEncryption)(
209 | config
210 | );
211 | let responsePayload = fle.decrypt(response);
212 | ```
213 |
214 | Output:
215 |
216 | ```json
217 | {
218 | "path": {
219 | "to": {
220 | "foo": {
221 | "sensitive": "this is a secret",
222 | "sensitive2": "this is a super secret!"
223 | }
224 | }
225 | }
226 | }
227 | ```
228 |
229 | ### Performing JWE Encryption and Decryption
230 |
231 | #### JWE Encryption and Decryption
232 |
233 | - [Introduction](#jwe-introduction)
234 | - [Configuring the JWE Encryption](#configuring-the-jwe-encryption)
235 | - [Performing JWE Encryption](#performing-jwe-encryption)
236 | - [Performing JWE Decryption](#performing-jwe-decryption)
237 | - [Encrypting Entire Payloads](#encrypting-entire-payloads-jwe)
238 | - [Decrypting Entire Payloads](#decrypting-entire-payloads-jwe)
239 | - [First Level Field Encryption and Decryption](#encrypting-decrypting-first-level-field-jwe)
240 |
241 | ##### • Introduction
242 |
243 | This library uses [JWE compact serialization](https://datatracker.ietf.org/doc/html/rfc7516#section-7.1) for the encryption of sensitive data.
244 | The core methods responsible for payload encryption and decryption are `encryptData` and `decryptData` in the `JweEncryption` class.
245 |
246 | - `encryptPayload` usage:
247 |
248 | ```js
249 | const jwe = new clientEncryption.JweEncryption(config);
250 | // …
251 | let encryptedRequestPayload = jwe.encrypt(endpoint, header, body);
252 | ```
253 |
254 | - `decryptPayload` usage:
255 |
256 | ```js
257 | const jwe = new clientEncryption.JweEncryption(config);
258 | // …
259 | let responsePayload = jwe.decrypt(encryptedResponsePayload);
260 | ```
261 |
262 | ##### • Configuring the JWE Encryption
263 |
264 | `JweEncryption` needs a config object to instruct how to decrypt/decrypt the payloads. Example:
265 |
266 | ```js
267 | const config = {
268 | paths: [
269 | {
270 | path: "/resource1",
271 | toEncrypt: [
272 | {
273 | /* path to element to be encrypted in request json body */
274 | element: "path.to.foo",
275 | /* path to object where to store encryption fields in request json body */
276 | obj: "path.to.encryptedFoo",
277 | },
278 | ],
279 | toDecrypt: [
280 | {
281 | /* path to element where to store decrypted fields in response object */
282 | element: "path.to.encryptedFoo",
283 | /* path to object with encryption fields */
284 | obj: "path.to.foo",
285 | },
286 | ],
287 | },
288 | ],
289 | mode: "JWE",
290 | encryptedValueFieldName: "encryptedData",
291 | encryptionCertificate: "./path/to/public.cert",
292 | privateKey: "./path/to/your/private.key",
293 | };
294 | ```
295 |
296 | Mode must be set to JWE to use JWE encryption
297 |
298 | ##### • Performing JWE Encryption
299 |
300 | Call `JweEncryption.encrypt()` with a JSON request payload, and optional `header` object.
301 |
302 | Example using the configuration [above](#configuring-the-field-level-encryption):
303 |
304 | ```js
305 | const payload = {
306 | path: {
307 | to: {
308 | foo: {
309 | sensitive: "this is a secret!",
310 | sensitive2: "this is a super-secret!",
311 | },
312 | },
313 | },
314 | };
315 | const jwe = new (require("mastercard-client-encryption").JweEncryption)(config);
316 | // …
317 | let responsePayload = jwe.encrypt("/resource1", header, payload);
318 | ```
319 |
320 | Output:
321 |
322 | ```json
323 | {
324 | "path": {
325 | "to": {
326 | "encryptedFoo": {
327 | "encryptedData": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM….Y+oPYKZEMTKyYcSIVEgtQw"
328 | }
329 | }
330 | }
331 | }
332 | ```
333 |
334 | ##### • Performing JWE Decryption
335 |
336 | Call `JweEncryption.decrypt()` with an (encrypted) `response` object with the following fields:
337 |
338 | Example using the configuration [above](#configuring-the-jwe-encryption):
339 |
340 | ```js
341 | const response = {};
342 | response.request = { url: "/resource1" };
343 | response.body = JSON.parse(
344 | "{" +
345 | ' "path": {' +
346 | ' "to": {' +
347 | ' "encryptedFoo": {' +
348 | ' "encryptedData": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM….Y+oPYKZEMTKyYcSIVEgtQw"' +
349 | " }" +
350 | " }" +
351 | " }" +
352 | "}");
353 | const jwe = new (require("mastercard-client-encryption").JweEncryption)(config);
354 | let responsePayload = jwe.decrypt(response);
355 | ```
356 |
357 | Output:
358 |
359 | ```json
360 | {
361 | "path": {
362 | "to": {
363 | "foo": {
364 | "sensitive": "this is a secret",
365 | "sensitive2": "this is a super secret!"
366 | }
367 | }
368 | }
369 | }
370 | ```
371 |
372 | ##### • Encrypting Entire Payloads
373 |
374 | Entire payloads can be encrypted using the "$" operator as encryption path:
375 |
376 | ```js
377 | const config = {
378 | paths: [
379 | {
380 | path: "/resource1",
381 | toEncrypt: [
382 | {
383 | /* path to element to be encrypted in request json body */
384 | element: "$",
385 | /* path to object where to store encryption fields in request json body */
386 | obj: "$",
387 | },
388 | ],
389 | toDecrypt: [],
390 | },
391 | ],
392 | mode: "JWE",
393 | encryptedValueFieldName: "encryptedData",
394 | encryptionCertificate: "./path/to/public.cert",
395 | privateKey: "./path/to/your/private.key",
396 | };
397 | ```
398 |
399 | Example:
400 |
401 | ```js
402 | const payload = JSON.parse(
403 | "{" +
404 | ' "sensitive": "this is a secret",' +
405 | ' "sensitive2": "this is a super secret!"' +
406 | "}");
407 | const jwe = new (require("mastercard-client-encryption").JweEncryption)(config);
408 | // …
409 | let responsePayload = jwe.encrypt("/resource1", header, payload);
410 | ```
411 |
412 | Output:
413 |
414 | ```json
415 | {
416 | "encryptedData": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM….Y+oPYKZEMTKyYcSIVEgtQw"
417 | }
418 | ```
419 |
420 | ##### • Decrypting Entire Payloads
421 |
422 | Entire payloads can be decrypted using the "$" operator as decryption path:
423 |
424 | ```js
425 | const config = {
426 | paths: [
427 | {
428 | path: "/resource1",
429 | toEncrypt: [],
430 | toDecrypt: [
431 | {
432 | /* path to element where to store decrypted fields in response object */
433 | element: "$",
434 | /* path to object with encryption fields */
435 | obj: "$",
436 | },
437 | ],
438 | },
439 | ],
440 | mode: "JWE",
441 | encryptedValueFieldName: "encryptedData",
442 | encryptionCertificate: "./path/to/public.cert",
443 | privateKey: "./path/to/your/private.key",
444 | };
445 | ```
446 |
447 | Example:
448 |
449 | ```js
450 | const encryptedPayload = JSON.parse(
451 | "{" +
452 | ' "encryptedData": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM….Y+oPYKZEMTKyYcSIVEgtQw"' +
453 | "}");
454 | const jwe = new (require("mastercard-client-encryption").JweEncryption)(config);
455 | let responsePayload = jwe.decrypt(encryptedPayload);
456 | ```
457 |
458 | Output:
459 |
460 | ```json
461 | {
462 | "sensitive": "this is a secret",
463 | "sensitive2": "this is a super secret!"
464 | }
465 | ```
466 |
467 | ##### • First Level Field Encryption and Decryption
468 |
469 | 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):
470 |
471 | Example of configuration:
472 |
473 | ```js
474 | const config = {
475 | paths: [
476 | {
477 | path: "/resource1",
478 | toEncrypt: [
479 | {
480 | /* path to element to be encrypted in request json body */
481 | element: "sensitive",
482 | /* path to object where to store encryption fields in request json body */
483 | obj: "encryptedData",
484 | },
485 | ],
486 | toDecrypt: [
487 | {
488 | /* path to element where to store decrypted fields in response object */
489 | element: "encryptedData",
490 | /* path to object with encryption fields */
491 | obj: "sensitive",
492 | },
493 | ],
494 | },
495 | ],
496 | mode: "JWE",
497 | encryptedValueFieldName: "encryptedData",
498 | encryptionCertificate: "./path/to/public.cert",
499 | privateKey: "./path/to/your/private.key",
500 | };
501 | ```
502 |
503 | Example of encryption:
504 |
505 | ```js
506 | const payload = {
507 | sensitive: "this is a secret!",
508 | notSensitive: "not a secret",
509 | };
510 | const jwe = new (require("mastercard-client-encryption").JweEncryption)(config);
511 | // …
512 | let responsePayload = jwe.encrypt("/resource1", header, payload);
513 | ```
514 |
515 | Output:
516 |
517 | ```json
518 | {
519 | "encryptedData": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM….Y+oPYKZEMTKyYcSIVEgtQw",
520 | "notSensitive": "not a secret"
521 | }
522 | ```
523 |
524 | Example of decryption:
525 |
526 | ```js
527 | const response = {};
528 | response.request = { url: "/resource1" };
529 | response.body = JSON.parse(
530 | "{" +
531 | ' "encryptedData": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM….Y+oPYKZEMTKyYcSIVEgtQw",' +
532 | ' "notSensitive": "not a secret"' +
533 | "}");
534 | const jwe = new (require("mastercard-client-encryption").JweEncryption)(config);
535 | let responsePayload = jwe.decrypt(response);
536 | ```
537 |
538 | Output:
539 |
540 | ```json
541 | {
542 | "sensitive": "this is a secret",
543 | "notSensitive": "not a secret"
544 | }
545 | ```
546 |
547 | ### Integrating with OpenAPI Generator API Client Libraries
548 |
549 | [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) generates API client libraries from [OpenAPI Specs](https://github.com/OAI/OpenAPI-Specification).
550 | It provides generators and library templates for supporting multiple languages and frameworks.
551 |
552 | The **client-encryption-nodejs** library provides the `Service` decorator object you can use with the OpenAPI generated client. This class will take care of encrypting request and decrypting response payloads, but also of updating HTTP headers when needed, automatically, without manually calling `encrypt()`/`decrypt()` functions for each API request or response.
553 |
554 | ##### OpenAPI Generator
555 |
556 | OpenAPI client can be generated, starting from your OpenAPI Spec / Swagger using the following command:
557 |
558 | ```shell
559 | openapi-generator-cli generate -i openapi-spec.yaml -l javascript -o out
560 | ```
561 |
562 | Client library will be generated in the `out` folder.
563 |
564 | See also:
565 |
566 | - [OpenAPI Generator CLI Installation](https://openapi-generator.tech/docs/installation/)
567 |
568 | ##### Usage of the `mcapi-service`:
569 |
570 | To use it:
571 |
572 | 1. Generate the OpenAPI client, as [above](#openapi-generator)
573 |
574 | 2. Import the **mastercard-client-encryption** library
575 |
576 | ```js
577 | const mcapi = require("mastercard-client-encryption");
578 | ```
579 |
580 | 3. Import the OpenAPI Client using the `Service` decorator object:
581 |
582 | ```js
583 | const openAPIClient = require("./path/to/generated/openapi/client");
584 | const config = {
585 | /* service configuration object */
586 | };
587 |
588 | const service = new mcapi.Service(openAPIClient, config);
589 | ```
590 |
591 | 4. Use the `service` object as you are using the `openAPIClient` to make API requests.
592 |
593 | Example:
594 |
595 | ```js
596 | let api = service.ServiceApi();
597 | let merchant =
598 | /* … */
599 | api.createMerchants(merchant, (error, data, response) => {
600 | // requests and responses will be automatically encrypted and decrypted
601 | // accordingly with the configuration used to instantiate the mcapi.Service.
602 | /* use response/data object here */
603 | });
604 | ```
605 |
--------------------------------------------------------------------------------