├── 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 | mastercard developers logo 6 | 7 | 8 | [![](https://github.com/Mastercard/client-encryption-nodejs/workflows/Build%20&%20Test/badge.svg)](https://github.com/Mastercard/client-encryption-nodejs/actions?query=workflow%3A%22Build+%26+Test%22) 9 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_client-encryption-nodejs&metric=alert_status)](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-nodejs) 10 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_client-encryption-nodejs&metric=coverage)](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-nodejs) 11 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_client-encryption-nodejs&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-nodejs) 12 | [![](https://github.com/Mastercard/client-encryption-nodejs/workflows/broken%20links%3F/badge.svg)](https://github.com/Mastercard/client-encryption-nodejs/actions?query=workflow%3A%22broken+links%3F%22) 13 | [![](http://img.shields.io/npm/v/mastercard-client-encryption.svg)](http://www.npmjs.com/package/mastercard-client-encryption) 14 | [![](https://img.shields.io/badge/license-MIT-yellow.svg)](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 | Encryption of sensitive data 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 | --------------------------------------------------------------------------------