├── Gemfile
├── test
├── res
│ ├── test_key.der
│ ├── test_key.p12
│ ├── test_key_container.p12
│ ├── test_certificate.cert
│ ├── test_certificate-2048.pem
│ ├── pub_cert_1.pem
│ ├── pub_cert_0.pem
│ ├── test_key.pem
│ ├── test_key_pkcs1-2048.pem
│ └── test_key_pkcs8-2048.pem
├── test_helper.rb
├── utils.rb
├── mock
│ ├── jwe-response-a128gcm.json
│ ├── jwe-response-a192gcm.json
│ ├── jwe-response-cbc.json
│ ├── jwe-response-a256gcm.json
│ ├── jwe-response-interceptor.json
│ ├── config-header.json
│ ├── config-interceptor.json
│ ├── jwe-response.json
│ ├── config-readme.json
│ ├── response-interceptor.json
│ ├── response-root.json
│ ├── response.json
│ ├── response-readme.json
│ ├── response-header.json
│ ├── jwe-config.json
│ └── config.json
├── test_rsa_oaep.rb
├── test_crypto_config.rb
├── test_crypto_cryptography.rb
├── test_jwe_encryption.rb
├── test_interceptor.rb
├── test_payload_encryption.rb
├── test_utils.rb
└── test_field_level_encryption.rb
├── .rspec
├── sonar-project.properties
├── lib
└── mcapi
│ └── encryption
│ ├── utils
│ ├── hash.ext.rb
│ ├── openssl_rsa_oaep.rb
│ └── utils.rb
│ ├── jwe_encryption.rb
│ ├── openapi_interceptor.rb
│ ├── crypto
│ ├── jwe-crypto.rb
│ └── crypto.rb
│ └── field_level_encryption.rb
├── .gitignore
├── Rakefile
├── .github
├── workflows
│ ├── broken-links.yml
│ ├── test.yml
│ └── sonar.yml
├── pull_request_template.md
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── mastercard-client-encryption.gemspec
├── LICENSE
├── .rubocop.yml
└── README.md
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 | gemspec
5 |
--------------------------------------------------------------------------------
/test/res/test_key.der:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard/client-encryption-ruby/HEAD/test/res/test_key.der
--------------------------------------------------------------------------------
/test/res/test_key.p12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard/client-encryption-ruby/HEAD/test/res/test_key.p12
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format documentation
3 | --backtrace
4 | --order random
5 | --warnings
6 | --require spec_helper
--------------------------------------------------------------------------------
/test/res/test_key_container.p12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard/client-encryption-ruby/HEAD/test/res/test_key_container.p12
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'simplecov'
4 | SimpleCov.start do
5 | add_filter '/test/'
6 | end
7 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=Mastercard_client-encryption-ruby
2 | sonar.organization=mastercard
3 | sonar.projectName=client-encryption-ruby
4 | sonar.host.url=https://sonarcloud.io
5 |
--------------------------------------------------------------------------------
/lib/mcapi/encryption/utils/hash.ext.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | #
4 | # Hash extension
5 | #
6 | class Hash
7 | #
8 | # Parse the current hash as json
9 | #
10 | def json
11 | JSON.parse(to_json)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/utils.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/autorun'
4 | require 'minitest/mock'
5 |
6 | def assert_exp_equals(exp, msg = '')
7 | if block_given?
8 | assert_equal(assert_raises(exp) do
9 | yield
10 | end.message, msg)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 | .idea/*
10 | .idea
11 | /.idea/workspace.xml
12 | *.iml
13 | *.xml
14 | *.gem
15 | /sonar-scanner-3.3.0.1492/
16 | publishSonar.sh
17 | /.scannerwork/
18 | /Gemfile.lock
19 | /generate.sh
20 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rubygems'
4 | require 'rake'
5 | require 'rake/clean'
6 | require 'rake/testtask'
7 |
8 | desc 'Run tests'
9 | task default: 'test'
10 | Rake::TestTask.new do |t|
11 | t.libs << 'test'
12 | t.test_files = FileList['test/test_*.rb']
13 | # Load SimpleCov before starting the tests
14 | t.ruby_opts = ['-r "./test/test_helper"']
15 | t.verbose = true
16 | t.warning = false
17 | end
18 |
19 | Dir['tasks/**/*.rake'].each { |t| load t }
20 |
--------------------------------------------------------------------------------
/.github/workflows/broken-links.yml:
--------------------------------------------------------------------------------
1 | 'on':
2 | push:
3 | branches:
4 | - "**"
5 | schedule:
6 | - cron: 0 16 * * *
7 | workflow_dispatch:
8 | name: broken links?
9 | jobs:
10 | linkChecker:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Link Checker
15 | id: lc
16 | uses: peter-evans/link-checker@v1.2.2
17 | with:
18 | args: '-v -r *.md'
19 | - name: Fail?
20 | run: 'exit ${{ steps.lc.outputs.exit_code }}'
21 |
--------------------------------------------------------------------------------
/test/mock/jwe-response-a128gcm.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "url": "/resource"
4 | },
5 | "body": {
6 | "encrypted_data":"eyJlbmMiOiJBMTI4R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.WtvYljbsjdEv-Ttxx1p6PgyIrOsLpj1FMF9NQNhJUAHlKchAo5QImgEgIdgJE7HC2KfpNcHiQVqKKZq_y201FVzpicDkNzlPJr5kIH4Lq-oC5iP0agWeou9yK5vIxFRP__F_B8HSuojBJ3gDYT_KdYffUIHkm_UysNj4PW2RIRlafJ6RKYanVzk74EoKZRG7MIr3pTU6LIkeQUW41qYG8hz6DbGBOh79Nkmq7Oceg0ZwCn1_MruerP-b15SGFkuvOshStT5JJp7OOq82gNAOkMl4fylEj2-vADjP7VSK8GlqrA7u9Tn-a4Q28oy0GOKr1Z-HJgn_CElknwkUTYsWbg.PKl6_kvZ4_4MjmjW.AH6pGFkn7J49hBQcwg.zdyD73TcuveImOy4CRnVpw"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/test/mock/jwe-response-a192gcm.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "url": "/resource"
4 | },
5 | "body": {
6 | "encrypted_data":"eyJlbmMiOiJBMTkyR0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.FWC8PVaZoR2TRKwKO4syhSJReezVIvtkxU_yKh4qODNvlVr8t8ttvySJ-AjM8xdI6vNyIg9jBMWASG4cE49jT9FYuQ72fP4R-Td4vX8wpB8GonQj40yLqZyfRLDrMgPR20RcQDW2ThzLXsgI55B5l5fpwQ9Nhmx8irGifrFWOcJ_k1dUSBdlsHsYxkjRKMENu5x4H6h12gGZ21aZSPtwAj9msMYnKLdiUbdGmGG_P8a6gPzc9ih20McxZk8fHzXKujjukr_1p5OO4o1N4d3qa-YI8Sns2fPtf7xPHnwi1wipmCC6ThFLU80r3173RXcpyZkF8Y3UacOS9y1f8eUfVQ.JRE7kZLN4Im1Rtdb.eW_lJ-U330n0QHqZnQ._r5xYVvMCrvICwLz4chjdw"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/test/mock/jwe-response-cbc.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "url": "/resource"
4 | },
5 | "body": {
6 | "encrypted_data":"eyJraWQiOiI3NjFiMDAzYzFlYWRlM2E1NDkwZTUwMDBkMzc4ODdiYWE1ZTZlYzBlMjI2YzA3NzA2ZTU5OTQ1MWZjMDMyYTc5IiwiY3R5IjoiYXBwbGljYXRpb25cL2pzb24iLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.5bsamlChk0HR3Nqg2UPJ2Fw4Y0MvC2pwWzNv84jYGkOXyqp1iwQSgETGaplIa7JyLg1ZWOqwNHEx3N7gsN4nzwAnVgz0eta6SsoQUE9YQ-5jek0COslUkoqIQjlQYJnYur7pqttDibj87fcw13G2agle5fL99j1QgFPjNPYqH88DMv481XGFa8O3VfJhW93m73KD2gvE5GasOPOkFK9wjKXc9lMGSgSArp3Awbc_oS2Cho_SbsvuEQwkhnQc2JKT3IaSWu8yK7edNGwD6OZJLhMJzWJlY30dUt2Eqe1r6kMT0IDRl7jHJnVIr2Qpe56CyeZ9V0aC5RH1mI5dYk4kHg.yI0CS3NdBrz9CCW2jwBSDw.6zr2pOSmAGdlJG0gbH53Eg.UFgf3-P9UjgMocEu7QA_vQ"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.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/jwe-response-a256gcm.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "url": "/resource"
4 | },
5 | "body": {
6 | "encrypted_data":"eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiNzYxYjAwM2MxZWFkZTNhNTQ5MGU1MDAwZDM3ODg3YmFhNWU2ZWMwZTIyNmMwNzcwNmU1OTk0NTFmYzAzMmE3OSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24ifQ.GXFw4ELZLBJaQHyoEHoaR9Hcr4eWQG9IXWBh5luUDzzHZKawOp5HXuu91r-_gDA7lA-kJfP6uDhAlA7ijbk8rKjTnwszO-20IepjHI51SciMniV-LUsAl7D_yPxDkWGK1y7iyyYBVa-R6SjFC3fvSWfjgaZBsdzyu1LLwkYO_tcbwd82amSkfg5HZfiUAwNvMaX-Bp8yMW8vGiWS7trRS92MRCXuXlbef1qp86B4MvYCrqI0brgOM2UTslfkct13lcykuZLO23VQMoqcKLJhJY8Rf91mUn7sgIzvV-wym7orGJiUa9we5wuUXQfziFiJfW_6B3QDw5GgzHGmU8ugzg.rCAneIopRVMKL5ma.rYmFsND5yCpXQjiG5GUU9paPyC2tqws9j8Tu4iuP3Fj5zUvuEYDwp4ngWieQxH5i36jk8E0U71LNpIsqdxtaazMfENOMQF6NsAA-cAAjaiCSAMcObbw0DYzSvgJ6EAgRenH7Q3yA2rI.0mRHj7jyv_bfgrdM0Wai2A"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/test/mock/jwe-response-interceptor.json:
--------------------------------------------------------------------------------
1 | {
2 | "mapping":{
3 | "merchant":{
4 | "name":"LAWN MOWER SERVICE",
5 | "mastercard_assigned_id":"354315",
6 | "merchant_category_code":"4563"
7 | }
8 | },
9 | "encrypted_payload":{
10 | "encrypted_data":"eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiNzYxYjAwM2MxZWFkZTNhNTQ5MGU1MDAwZDM3ODg3YmFhNWU2ZWMwZTIyNmMwNzcwNmU1OTk0NTFmYzAzMmE3OSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24ifQ.AggQtaROLfO9Wu_TvkC_AmjdqpLHgUa9MZf8qTroMCuOcPav0WNCC6CAsfRNiDxTmJp_MPOuWoWX0dQrDVZNVPiOlujv8UDPYZun7DpN8OlkXqJFfSpahpt-QkMXkQ1b-vn6qedYlblEGeIa6aCnubNsHmOOgIjD--NKEPXu5sROVUHwcE3BfAKHWGoQS9qPAy_1XDVmV-rCi1GM4FESjgVIjQ7YXBOSp8uzfz7vUOkFp5no0jHZ27IL-gTwCyCa1bMRx7MH3bHssz_DomI0ksJjeVUEs-iMDFRdkvAVlEs1nOR6hhrcU1Z-L6VkfGXU_yxonO4AAnUFvxVgJXkI3Q.wQbHbkDzmt2nUhl0.Kop-3aai2mv5lgzo.55nUzTctsf8sB2r8aJn2cw"
11 | }
12 | }
--------------------------------------------------------------------------------
/test/mock/config-header.json:
--------------------------------------------------------------------------------
1 | {
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 | "oaepPaddingDigestAlgorithm": "SHA-512",
20 | "ivHeaderName": "x-iv",
21 | "encryptedKeyHeaderName": "x-encrypted-key",
22 | "oaepHashingAlgorithmHeaderName": "x-oaep-hashing-algorithm",
23 | "publicKeyFingerprintHeaderName": "x-public-key-fingerprint",
24 | "publicKeyFingerprintType": "certificate",
25 | "encryptedValueFieldName": "data",
26 | "dataEncoding": "hex",
27 | "encryptionCertificate": "./test/res/test_certificate.cert",
28 | "privateKey": "./test/res/test_key.der"
29 | }
30 |
--------------------------------------------------------------------------------
/mastercard-client-encryption.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = 'mastercard-client-encryption'
5 | spec.version = '1.3.2'
6 | spec.authors = ['Mastercard']
7 | spec.required_ruby_version = '>= 2.4.4'
8 |
9 | spec.summary = 'Mastercard encryption library'
10 | spec.description = 'Library for Mastercard API compliant payload encryption/decryption.'
11 | spec.homepage = 'https://github.com/Mastercard/client-encryption-ruby'
12 | spec.license = 'MIT'
13 |
14 | spec.files = Dir['{lib}/**/*']
15 | spec.require_paths = ['lib']
16 |
17 | spec.add_dependency "hamster"
18 |
19 | spec.add_development_dependency 'bundler', '>= 1.5'
20 | spec.add_development_dependency 'minitest', '~> 5.0'
21 | spec.add_development_dependency 'rake', '>= 12.3.3'
22 | spec.add_development_dependency 'simplecov', '~> 0.16.1'
23 | end
24 |
--------------------------------------------------------------------------------
/test/mock/config-interceptor.json:
--------------------------------------------------------------------------------
1 | {
2 | "paths": [
3 | {
4 | "path": "/mappings/*",
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 | "oaepPaddingDigestAlgorithm": "SHA-256",
20 | "ivHeaderName": "x-iv",
21 | "encryptedKeyHeaderName": "x-encrypted-key",
22 | "oaepHashingAlgorithmHeaderName": "x-oaep-hashing-algorithm",
23 | "publicKeyFingerprintHeaderName": "x-public-key-fingerprint",
24 | "encryptedValueFieldName": "data",
25 | "dataEncoding": "base64",
26 | "publicKeyFingerprintType": "publicKey",
27 | "encryptionCertificate": "./test/res/test_certificate.cert",
28 | "privateKey": "./test/res/test_key.der"
29 | }
30 |
--------------------------------------------------------------------------------
/test/mock/jwe-response.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "url": "/mappings"
4 | },
5 | "body": {
6 | "mapping": {
7 | "customer_name":{
8 | "first_name":"John",
9 | "last_name":"Doe"
10 | }
11 | },
12 | "encrypted_payload":{
13 | "encrypted_data":"eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiNzYxYjAwM2MxZWFkZTNhNTQ5MGU1MDAwZDM3ODg3YmFhNWU2ZWMwZTIyNmMwNzcwNmU1OTk0NTFmYzAzMmE3OSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24ifQ.sK8BJFiBR8Oc-_nZsmQB1neIaPuq_mBXZ2vFejEWL4RBa0ukE-VMEMV8fESGgUqGQydTfddl_PSTquFJLqpAXbcBqF_n1kGL08hMPPBDB3ppiF_yEc4uRUCR3-8F8qJpGeomRd2Q-jSpPzk_6Gb7IF4oIxoQK3-xI2j6bOnfgo_RkHAXocCSGqbsLc0CnyHRyM7MzGdKJ2PvyC8PMqDbKJI-Ga9i0QxVKMDKEMwXDYF7qv_3aMpDq_F0ccMNWbEbmz7xAin17rM-YeGzPQC1_p0ksKFLv4hzPnADp4Zz_77tE_pRMV872D_afWPOchFGlswiBgkGRj1psWEGpPt_Fw.FMj5jInYY2q_TrcD.TjfmLsTWGlVKqPMd.pcKou3iQRay9OyRyi4-a7Q"
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/mock/config-readme.json:
--------------------------------------------------------------------------------
1 | {
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 | "ivFieldName": "iv",
20 | "encryptedKeyFieldName": "encryptedKey",
21 | "encryptedValueFieldName": "encryptedData",
22 | "oaepHashingAlgorithmFieldName": "oaepHashingAlgorithm",
23 | "publicKeyFingerprintFieldName": "publicKeyFingerprint",
24 | "publicKeyFingerprintType": "certificate",
25 | "dataEncoding": "hex",
26 | "encryptionCertificate": "./test/res/test_certificate.cert",
27 | "privateKey": "./test/res/test_key.der",
28 | "oaepPaddingDigestAlgorithm": "SHA-256"
29 | }
--------------------------------------------------------------------------------
/.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 | test:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | rvm:
18 | - 2.4.4
19 | - truffleruby
20 | - 2.4
21 | - 2.5
22 | - 2.6
23 | - 2.7
24 | - 3.0
25 | steps:
26 | - uses: actions/checkout@v2
27 | - name: Set up Ruby
28 | uses: ruby/setup-ruby@v1
29 | with:
30 | ruby-version: '${{ matrix.rvm }}'
31 | - name: Install dependencies
32 | run: bundle install --jobs=3 --retry=3
33 | - name: Run tests
34 | run: |
35 | gem build *.gemspec
36 | gem install *.gem
37 | rake test
38 |
--------------------------------------------------------------------------------
/test/mock/response-interceptor.json:
--------------------------------------------------------------------------------
1 | {
2 | "encrypted_payload": {
3 | "data": "nVI8jjqCuCnRVF/bS7RwWPGmV34dnNKM0xBSXmWcZUGu0VW2CMgQgh4kHMxasJ8z+/S0gZ2RTESeODkRAKN5RTAoPL/d5wOFeNzWc4/1D6+Y2OoVL6Lhj8YX7+lz4UmBGe//wXInbFTzody2d1x4hxGawf9t4/ggYevdr6WMfhSObS23q/QUAe7RNmwWpFuOIS5nSnzfVTwxTuHqbPsVPg==\\\"}}\", :header_params=>{\"Accept\"=>\"application/json\", \"Content-Type\"=>\"application/json\", \"x-encrypted-key\"=>\"z9hICyBnN+X857KoYn1Ft3GOmMt+GGBcNvI+QlRmKZ2DVJgfaA9YoB96tE9SNRMux+9ZbAEARGXsnURFrcG3+xDf0XUvzwmcXqC5tZw4Xrw8LNXjZBPnHGCe7S1MY+x90BBK+pxD+LEkCIlsHYugButd8SgHehb34FQ35lJA/RuMQoQgbbqBAVdPNoFLap8HkzIMk83Kfuqe7vlZtEinFALs78JTDIah2Ytybwq83a+NXstKj8o8PvyKZEmD5QgrMnuxozSsWZso/OdS6Po5WFxpPaEIvOtsPw79SvujPlE2v1WK7yAvwNbBRvuxtXdz4mWNUNsmvpxD+Cvw1/2AQA==\", \"x-iv\"=>\"4+vu1fcirK916IukdAfn7A==\", \"x-oaep-hashing-algorithm\"=>\"SHA256\", \"x-public-key-fingerprint\"=>\"761b003c1eade3a5490e5000d37887baa5e6ec0e226c07706e599451fc032a79"
4 | }
5 | }
--------------------------------------------------------------------------------
/test/mock/response-root.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "url": "/resource"
4 | },
5 | "body": {
6 | "encryptedData": "3097e36bf8b71637a0273abe69c23752d6157464ce49f6f35120d28bedfb63a1f2c8087be3a3bc9775592db41db87a8c",
7 | "iv": "22507f596fffb45b15244356981d7ea1",
8 | "encryptedKey": "d4714161898b8bc5c54a63f71ae7c7a40734e4f7c7e27d121ac5e85a3fa47946aa3546027abe0874d751d5ae701491a7f572fc30fa08dd671d358746ffe8709cba36010f97864105b175c51b6f32d36d981287698a3f6f8707aedf980cce19bfe7c5286ddba87b7f3e5abbfa88a980779037c0b7902d340d73201cf3f0b546c2ad9f54e4b71a43504da947a3cb7af54d61717624e636a90069be3c46c19b9ae8b76794321b877544dd03f0ca816288672ef361c3e8f14d4a1ee96ba72d21e3a36c020aa174635a8579b0e9af761d96437e1fa167f00888ff2532292e7a220f5bc948f8159dea2541b8c6df6463213de292b4485076241c90706efad93f9b98ea",
9 | "publicKeyFingerprint": "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
10 | "oaepHashingAlgorithm": "SHA512",
11 | "notDelete": "this field should be deleted"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/mock/response.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "url": "/resource"
4 | },
5 | "body": {
6 | "foo": {
7 | "elem1": {
8 | "encryptedData": "3097e36bf8b71637a0273abe69c23752d6157464ce49f6f35120d28bedfb63a1f2c8087be3a3bc9775592db41db87a8c",
9 | "iv": "22507f596fffb45b15244356981d7ea1",
10 | "encryptedKey": "d4714161898b8bc5c54a63f71ae7c7a40734e4f7c7e27d121ac5e85a3fa47946aa3546027abe0874d751d5ae701491a7f572fc30fa08dd671d358746ffe8709cba36010f97864105b175c51b6f32d36d981287698a3f6f8707aedf980cce19bfe7c5286ddba87b7f3e5abbfa88a980779037c0b7902d340d73201cf3f0b546c2ad9f54e4b71a43504da947a3cb7af54d61717624e636a90069be3c46c19b9ae8b76794321b877544dd03f0ca816288672ef361c3e8f14d4a1ee96ba72d21e3a36c020aa174635a8579b0e9af761d96437e1fa167f00888ff2532292e7a220f5bc948f8159dea2541b8c6df6463213de292b4485076241c90706efad93f9b98ea",
11 | "publicKeyFingerprint": "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
12 | "oaepHashingAlgorithm": "SHA512"
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/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/response-readme.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "url": "/resource"
4 | },
5 | "body": {
6 | "path": {
7 | "to": {
8 | "encryptedFoo": {
9 | "encryptedData": "7d8753f5e1c813fd20fccf487801a7e53e01341b65286de3694d42784f37a0af2d966a822a974c71b5e9f4a942519ab48d4b78e06ad83f61d2fe0f871842302ec507b85e947a74f2904b522ab70cddae",
10 | "iv": "e2e6dbdbeebba4650030b99274e8a31c",
11 | "encryptedKey": "8816d1bce3b5e41b9d870d165abcda53cb6545c170f5fe3a46759969f9d6b65722fa3aed0f13856bd9011cb2855e1f282c7d1eb336d57992688eaf24f81a65b885948d7e08d882d6189ec082e50272c921e78afc9129c645b4372990c05b57838f4f6d94fa706fc58e55af13238b83a2f9b02c126c42b331edac547d378ea80428b2b8215caa3c0e37ecdede7259565ed4aaffed19b9634d8038a6117d788085f1955ffaa500a22a14c24d2675f73a1012723137d6ea3458aa220350fac50ca58d6c25d85e316b7e5cebcbfb525f014018336062b3abff5eeee9d0df798a5fab0c30c7873cac1c8c6741e89445f8c0b58c1347cc5da0ecf304a9f9e6aac6b44d",
12 | "publicKeyFingerprint": "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279",
13 | "oaepHashingAlgorithm": "SHA256"
14 | }
15 | }
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/test/mock/response-header.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "url": "/resource"
4 | },
5 | "headers": {
6 | "x-iv": [
7 | "3c6c68c75ba89394bbdf2d4bf2cd2e23"
8 | ],
9 | "x-encrypted-key": [
10 | "7aabb9dcf562a784fd8a578a8ee7b89191564a46c2beb0dfc654d36e0f5595367e765ca986c917ac457168933873017c57a2cd84163c52f26f3fe22f7282f626bd55a34629ea927df2d78857d4ae03d0c4b8ae8efcdbaac659ad00bdc55fe8230338b835612634cb049ec0d878bab2d4330d5dd53a03d5e2abd2b630cd1ba2a45fb5e150e5396a74777fdefea752e0cc3951d093ebd7a1b45dbe118e4564f46a3e61639decab46e6823c43e2c2d20bcea3f86fbe3405e17d5b7cf2d70660c459d03d7b21a07bb007c60db24d36fd51ce34e730fb0c0112e778e2d8c166a2fdda0e4d156cc6356b78a87cadd61450901dc11ad78a920140bf2d53815998803222"
11 | ],
12 | "x-public-key-fingerprint": [
13 | "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279"
14 | ],
15 | "x-oaep-hashing-algorithm": [
16 | "SHA512"
17 | ]
18 | },
19 | "body": {
20 | "encrypted_payload": {
21 | "data": "bad909eceaa572bd2157288637ab18a5e85d5e99a05f2a217338f92bab5bae7ee7ec91c3c82ea60b48206eb897672119bfbc65c513b35044c663d28675f3153479a6aa737227b6d2767a868055c3972f"
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/test/mock/jwe-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "paths": [
3 | {
4 | "path": "/mappings/*",
5 | "toEncrypt": [
6 | {
7 | "element": "mapping.customer_identifier",
8 | "obj": "encrypted_payload"
9 | }
10 | ],
11 | "toDecrypt": [
12 | {
13 | "element": "encrypted_payload",
14 | "obj": "mapping.customer_identifier"
15 | }
16 | ]
17 | },
18 | {
19 | "path": "/resource",
20 | "toEncrypt": [
21 | {
22 | "element": "$",
23 | "obj": "$"
24 | }
25 | ],
26 | "toDecrypt": [
27 | {
28 | "element": "$",
29 | "obj": "$"
30 | }
31 | ]
32 | },
33 | {
34 | "path": "/arrays",
35 | "toEncrypt": [
36 | {
37 | "element": "$",
38 | "obj": "$"
39 | }
40 | ],
41 | "toDecrypt": [
42 | {
43 | "element": "$",
44 | "obj": "$"
45 | }
46 | ]
47 | }
48 | ],
49 | "encryptedValueFieldName": "encrypted_data",
50 | "encryptionCertificate": "./test/res/test_certificate-2048.pem",
51 | "privateKey": "./test/res/test_key_pkcs8-2048.pem"
52 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/res/test_certificate-2048.pem:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | TargetRubyVersion: 2.4.4
3 | DisplayCopNames: true
4 |
5 | #
6 | # Metrics
7 | #
8 |
9 | Metrics/AbcSize:
10 | Max: 25
11 |
12 | Metrics/BlockLength:
13 | Enabled: false
14 |
15 | Metrics/ClassLength:
16 | Max: 128
17 |
18 | Metrics/LineLength:
19 | Max: 130
20 |
21 | Metrics/MethodLength:
22 | CountComments: false
23 | Max: 50
24 |
25 | Metrics/CyclomaticComplexity:
26 | Max: 15
27 |
28 | Metrics/PerceivedComplexity:
29 | Max: 15
30 |
31 | Metrics/ModuleLength:
32 | Max: 120
33 |
34 | #
35 | # Naming
36 | #
37 | Naming/UncommunicativeMethodParamName:
38 | Enabled: false
39 |
40 | #
41 | # Performance
42 | #
43 | Performance/UnfreezeString:
44 | Enabled: false
45 |
46 | #
47 | # Style
48 | #
49 | Style/AccessModifierDeclarations:
50 | Enabled: false
51 |
52 | Style/GlobalVars:
53 | Enabled: false
54 |
55 | Style/SafeNavigation:
56 | Enabled: false
57 |
58 | Style/StringLiterals:
59 | EnforcedStyle: single_quotes
60 |
61 | Layout/IndentHash:
62 | IndentationWidth: 4
63 |
64 | Layout/MultilineOperationIndentation:
65 | Enabled: false
66 |
67 | Style/PercentLiteralDelimiters:
68 | PreferredDelimiters:
69 | '%i': '[]'
70 | '%w': '[]'
71 | '%W': '[]'
72 |
73 | Style/Documentation:
74 | Enabled: false
--------------------------------------------------------------------------------
/.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/res/pub_cert_1.pem:
--------------------------------------------------------------------------------
1 | Bag Attributes
2 | localKeyID: 76 4F 60 9E E3 97 A9 9F AE 51 E0 C2 AE 22 A0 49 51 47 CE F9
3 | friendlyName: keyalias-mek
4 | subject=/CN=MasterCardKey/OU=MasterCard API/O=MasterCard
5 | issuer=/CN=MasterCardKey/OU=MasterCard API/O=MasterCard
6 | -----BEGIN CERTIFICATE-----
7 | MIIDBTCCAe2gAwIBAgIBATANBgkqhkiG9w0BAQsFADBGMRYwFAYDVQQDEw1NYXN0
8 | ZXJDYXJkS2V5MRcwFQYDVQQLEw5NYXN0ZXJDYXJkIEFQSTETMBEGA1UEChMKTWFz
9 | dGVyQ2FyZDAeFw0xODA5MTQwNzQyMTJaFw0xOTA5MTQwNzQyMTJaMEYxFjAUBgNV
10 | BAMTDU1hc3RlckNhcmRLZXkxFzAVBgNVBAsTDk1hc3RlckNhcmQgQVBJMRMwEQYD
11 | VQQKEwpNYXN0ZXJDYXJkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
12 | gRCUsfmLJDWN4WAyABArTDxQ4pTHUW6WF92X51uiprO34RSfZ1scwmrH5PYbUlR+
13 | SZmQRsTWExBi5tWjtGzRBIDgWz4HlBGAJNfU327mnjwSao/HelrQxCMzBeri6f2R
14 | V+NI9VX/3qH7SGqKm8g1ZwR+HwG5ej9ywKc3JxhdFvXZ0s6Pce43aT7vFEnJTbCp
15 | R+Dy3Uqb0gVW6ublWKUlsPZI086h/qyI0Al8ZoYhy6tSf4nsIMVQyTCrbw03l5vp
16 | MU3K8OQ7AJFHFj3IL8mYm4CxwjKeOSFlGg/0qSCH+YB3KGHd66LCk9Gc4XwhAcYe
17 | s/FoqQj+y5VTgT8qCgsA6wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB3dx88cJ+X
18 | 68bwzbwJZvBLVZxK1eQDsoZewtiLbYRZV1T/d5DYvY0DQGMsyHLpz4BP0RMQ1h8C
19 | IWj5g3UYhPy36HOY/ipnyQAvXRCn8aJpTgxFEpfBRwSzYWJy+VFwsCSJCmJc/TZb
20 | 2CEfi1Jy9uRIprM+rQ3Mwa82G9+/osjZ1yKnFMzYG9WpHb3/hCLVh4SP6uO9y8+g
21 | aQFVYVQztcHZrYyslDANsWsbwxnAKdVd7RwPfXsF8ad6udXMKu9OKHURDsOvTa9f
22 | gcRLMH4Q/ExwiRYfGAEtvxULbxk8q6FfMsKPS20KwUrTDxRD2Bk8O+o2YfWRImEM
23 | vmAPak9KIbbI
24 | -----END CERTIFICATE-----
25 |
--------------------------------------------------------------------------------
/test/res/pub_cert_0.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIEITCCAwmgAwIBAgIRAJSDmOtSp9whEE4qNHJY/EMwDQYJKoZIhvcNAQELBQAw
3 | gZAxCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRNYXN0ZXJDYXJkIFdvcmxkd2lkZTEb
4 | MBkGA1UECxMSQ29ycG9yYXRlIFNlY3VyaXR5MUUwQwYDVQQDEzxNYXN0ZXJDYXJk
5 | IFBSRCBPcGVuQVBJIEluYm91bmQgRmllbGQgTGV2ZWwgRW5jcnlwdGlvbiBTdWIg
6 | Q0EwHhcNMTkwMzA0MTU0NzUwWhcNMjAwMzA0MTU0NzUwWjCBpDETMBEGCgmSJomT
7 | 8ixkARkWA2NvbTEaMBgGCgmSJomT8ixkARkWCm1hc3RlcmNhcmQxCzAJBgNVBAYT
8 | AlVTMS0wKwYDVQQKEyRNYXN0ZXJDYXJkIFdvcmxkd2lkZSAtIEluYm91bmQgKFBS
9 | RCkxEDAOBgNVBAsTB09wZW5BUEkxIzAhBgNVBAMTGkFsZXhEMTQtaW5ib3VuZC1l
10 | bmNyeXB0aW9uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlJnU3ina
11 | 1GzZDt+SsWBvWbrT+2rvhyKQ2vbaruCMwTgu6lCAg4m4ehcDX9zdBgGHOEy7iH3G
12 | cMIUjgdkguxNiUkh1mlcwG7alKmfbTrj8oHBbaTvzexuhMO47iq9Y3zv6pdswJl9
13 | ELAG9bUHRrKFixUWzdnI2a8CMmXngKsVWxtUiIgLzMcUIKtRpHh8DZCdNAIF2hIc
14 | tKM80gjwuNEVq50Iz4CsWDFTCt8fJBWDnPZcJp9JV08rfvq9Abr/IEjON2eFGB1m
15 | 2wCc3Za9LoSCRCvhI75vQ92Fp3/JgqgV5VpG41aXgYqP5a0MTbuVPS/PhWu/HwGf
16 | wFgVqMs7eIvgVwIDAQABo2AwXjAfBgNVHSMEGDAWgBTCzQJmYIHGXuER5duiDh7O
17 | fwea/TAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIFIDAdBgNVHQ4EFgQU95+g
18 | xouDsVhNT6xFkcYJ7k5wAtAwDQYJKoZIhvcNAQELBQADggEBACVVmW1mZFkh+5Eb
19 | ya/HGZJDOcUkpkFy8Y5eVPW3b50bythsK/p7YyH0a4UzBdbKgu56D1754AyRjP0n
20 | yqtWq/ORYL+Lntf4W3hjIK/HjinrVstunkmx/gmhLydPEvijzr0z3SWhk25DvDUC
21 | PDi5FlW6Qq+crpLF8RCDXLU2ODFjHsdRG9yUqXNzHABhaNs1LOD+RRe4CPY4GorF
22 | XwXUREgiJECjNB1DpHayvLN5ybJsBpwqp8Zv5QsTvuyad0+CAUcdEGmJGeiZ8fqD
23 | cA2PeIfdAaBKcA0XA1pqMtlAqzoijsqNunPSX5V6tskTG9zilaBIFCb1fkhsgnC8
24 | b6FYG7w=
25 | -----END CERTIFICATE-----
26 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/res/test_key_pkcs1-2048.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpQIBAAKCAQEAzCT8ABttdAfW851VVNMSkpgcWZ1yFTgxNu+coPpL8Ug1OZgR
3 | YOgg+fcj/A8MTYs/yXgIJhI1goXVF/9NZPfryrBPxeFIOJe+CGePGMgXv3F7o9uV
4 | 010zcyImgKntv3NrLCVw9trzIIaKNgvwgmPDhegHsWpVMr7Lk2DL5QmPE28J8ekJ
5 | nYfHJzGUSlJYJpCoCVtO7bPPDCcWgrlOzoagTujtpyBRWAPRJoA1J2b7uvzrqmuU
6 | Gf8Yqg8MIeN7Uuxs4jSk3rsDXMCE5cCVKa+bfoTV1XRQJ2PABdNuvwH/wqosRg3Y
7 | TgFdWM46AY4DgIzzqY/xSeLibd1Hzg2wHQTM6wIDAQABAoIBAQDLmjtnk/NXHRaK
8 | RCm9/wHwCRuFWV1VwoR7KQGLH/er/ntvJLZ4cyuogo92Lj/z+uS0eC2QYurRcc81
9 | LuCuygF2VuBJGEXig5z5Xue+LJpaysEojLHia3sL4kyKWHCRWHjUP8dpvLdtgiHI
10 | g6HtObjhDajWjpnIkbgSFiFlHmJ/WqA7IjEOehGiqTjrfyXpL8rbcGt+chJb2z0s
11 | RdlABjl1MT2s9cCHZLwz6x1eDQDDYyw2pRRmEddMZ5VWtAd37I8RWl2NHrMsggca
12 | JzIA5LnddsRqmMVw7+1qFIIK0ZHOTknvvgQ8+U7P+r8v7+3mufvX4JakPingj543
13 | slbOGapBAoGBAO1F4REeKfCpGMo7kWZsAASAkEb+5Fcu4jrEzZkf2jk5WC4zkWlm
14 | SAqay1WLIEGP25LCo0o8vTEfx0tONukJMmEJewVi551Nxz+clcrbiJRcX6P8RTCe
15 | cJtQjUOqwHDvKNNpcAE8zz0YOLotJxhCH5ST3aE07sc159K9EGMzedrhAoGBANxB
16 | vbghMxN7lTuSvxMOwNQOWgKfWzV+fTXLQ7SgFhICqEV67nYHm0r/j9lN2Vtxuh6L
17 | hqZ2r1khzEOhyHz7YBINAYUqjjoFAVUfsHZ7auM7sdQBZx1VwS2XRPglgpso9wEh
18 | TEz75C7LH4/2nu1BIVAduE2cc95wKPEUexps/E1LAoGBAIppxFSvCvpIOpzmyPg9
19 | snjt4rx3vw6Y3AI6glF8Qlo1eJpjHMWmlAoTqOA7K9LzL7zabFVHP3qjtifY9bFV
20 | 2xy+YhSPUNvz3nLeToerL26UwHoyFM667qe8AtxhhKec7Gz/ygX+ykoykg0RgAfn
21 | svKCm7yJ2208pgLKpf+orMIhAoGBALrpMxWRXuW2pzKR2oJSr8KEl0/Iab9gouLG
22 | pqMegvwvsxqbMseItvkTHMB8tupJ/Xa0UsTqzOznqI7wONIPBDztOpAGSAHmg3X4
23 | WWiCXXeODd9qfVXAkxmcWBP4yPfg8JPN7REbZU1sZFFoKQAPmDSDtAZwsUdfiO7k
24 | wX7wY783AoGAQ646bcqPKXmNCc2oo4O4VgcC2afzNuxoUfLgFehVREj/tbhtOpN4
25 | NwhAsQhNz4uh1UmlulKTTGZ67VWikAiQ8ip5HSBMRVT/A4ZCUh5ondU1yVxH6Q0+
26 | eyQ+FF+jTgnAdMp0smLw7yem6HcekksgdNwhDKifFTI13mKWUh8gBew=
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/test/mock/config.json:
--------------------------------------------------------------------------------
1 | {
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_pkcs8-2048.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD0ynqAQWn0T7/V
3 | JLletTJgoxsTt5TR3IkJ+Yk/Pxg6Q5hXuiGrBdC+OVo/9hrNnptuZh9rZYKto6lb
4 | SjYFiKMeBDvPZrYDPzusp0C0KllIoVbzYiOezD76XHsQAEje0UXbzZlXstPXef2b
5 | i2HkqV26ST167L5O4moK8+7jHMT80T6XgsUyvyt8PjsQ9CSu6fnD9NfCSYmt2cb1
6 | 6OXcEtA7To2zoGznXqB6JhntFjG0jxee7RkLR+moOqMI9kFM5GSIV4uhwQ9FtOCj
7 | Uf7TFAU12wwfX/QXUEj6G93GVtzf6QdkVkWh4EyRHeMLyMNc5c0Iw1ZvXdOKfoeo
8 | 9F47QpbzAgMBAAECggEAK3dMmzuCSdxjTsCPnc6E3H35z914Mm97ceb6RN26OpZI
9 | FcO6OLj2oOBkMxlLFxnDta2yhIpo0tZNuyUJRKBHfov35tLxHNB8kyK7rYIbincD
10 | joHtm0PfJuuG+odiaRY11lrCkLzzOr6xlo4AWu7r8qkQnqQtAqrXc4xu7artG4rf
11 | MIunGnjjWQGzovtey1JgZctO97MU4Wvw18vgYBI6JM4eHJkZxgEhVQblBTKZs4Of
12 | iWk6MRHchgvqnWugwl213FgCzwy9cnyxTP13i9QKaFzL29TYmmN6bRWBH95z41M8
13 | IAa0CGahrSJjudZCFwsFh413YWv/pdqdkKHg1sqseQKBgQD641RYQkMn4G9vOiwB
14 | /is5M0OAhhUdWH1QtB8vvhY5ISTjFMqgIIVQvGmqDDk8QqFMOfFFqLtnArGn8HrK
15 | mBXMpRigS4ae/QgHEz34/RFjNDQ9zxIf/yoCRH5PmnPPU6x8j3bj/vJMRQA6/yng
16 | oca+9qvi3R32AtC5DUELnwyzNwKBgQD5x1iEV+albyCNNyLoT/f6LSH1NVcO+0IO
17 | vIaAVMtfy+hEEXz7izv3/AgcogVZzRARSK0qsQ+4WQN6Q2WG5cQYSyB92PR+Vgwh
18 | nagVvA+QHNDL988xoMhB5r2D2IVSRuTB2EOg7LiWHUHIExaxVkbADODDj7YV2aQC
19 | JVv0gbDQJQKBgQCaABix5Fqci6NbPvXsczvM7K6uoZ8sWDjz5NyPzbqObs3ZpdWK
20 | 3Ot4V270tnQbjTq9M4PqIlyGKp0qXO7ClQAskdq/6hxEU0UuMp2DzLNzlYPLvON/
21 | SH1czvZJnqEfzli+TMHJyaCpOGGf1Si7fhIk/f0cUGYnsCq2rHAU1hhRmQKBgE/B
22 | JTRs1MqyJxSwLEc9cZLCYntnYrr342nNLK1BZgbalvlVFDFFjgpqwTRTT54S6jR6
23 | nkBpdPmKAqBBcOOX7ftL0b4dTkQguZLqQkdeWyHK8aiPIetYyVixkoXM1xUkadqz
24 | cTSrIW1dPiniXnaVc9XSxtnqw1tKuSGuSCRUXN65AoGBAN/AmT1S4PAQpSWufC8N
25 | UJey8S0bURUNNjd52MQ7pWzGq2QC00+dBLkTPj3KOGYpXw9ScZPbxOthBFzHOxER
26 | Wo16AFw3OeRtn4VB1QJ9XvoA/oz4lEhJKbwUfuFGGvSpYvg3vZcOHF2zlvcUu7C0
27 | ub/WhOjV9jZvU5B2Ev8x1neb
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/.github/workflows/sonar.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: Set up Ruby
24 | uses: ruby/setup-ruby@v1
25 | with:
26 | ruby-version: 2.7
27 | - name: Setup java
28 | uses: actions/setup-java@v1
29 | with:
30 | java-version: '11'
31 | - name: Install dependencies
32 | run: >
33 | wget
34 | https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-3.3.0.1492.zip
35 |
36 | unzip sonar-scanner-cli-3.3.0.1492.zip
37 |
38 | bundle install --jobs=3 --retry=3
39 | - name: Run tests
40 | run: |
41 | gem build *.gemspec
42 | gem install *.gem
43 | rake test
44 | - name: Sonar
45 | env:
46 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
47 | SONAR_TOKEN: '${{ secrets.SONAR_TOKEN }}'
48 | run: |
49 | sonar-scanner-3.3.0.1492/bin/sonar-scanner \
50 | -Dsonar.sources=./lib \
51 | -Dsonar.tests=./test \
52 | -Dsonar.ruby.coverage.reportPaths=coverage/.resultset.json
53 |
--------------------------------------------------------------------------------
/test/test_rsa_oaep.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/autorun'
4 | require 'minitest/mock'
5 | require_relative '../lib/mcapi/encryption/utils/openssl_rsa_oaep'
6 | require_relative '../lib/mcapi/encryption/utils/utils'
7 | require_relative './utils'
8 | require 'json'
9 |
10 | class TestRsaOAEP < Minitest::Test
11 | def test_add_oaep_mgf1
12 | res = OpenSSL::PKCS1.add_oaep_mgf1(McAPI::Utils.decode('944cec7d0a6d0299c35cbb73c47a6874', 'hex'), 256)
13 | assert res
14 | end
15 |
16 | def test_add_oaep_mgf1_too_large
17 | assert_exp_equals(OpenSSL::PKey::RSAError, 'data too large for key size') do
18 | OpenSSL::PKCS1.add_oaep_mgf1(McAPI::Utils.decode('944cec7d0a6d0299c35cbb73c47a6874', 'hex'), 57)
19 | end
20 | end
21 |
22 | def test_add_oaep_mgf1_key_size_too_small
23 | assert_exp_equals(OpenSSL::PKey::RSAError, 'key size too small') do
24 | OpenSSL::PKCS1.add_oaep_mgf1(McAPI::Utils.decode('944cec7d0a6d0299c35cbb73c47a6874', 'hex'), 40)
25 | end
26 | end
27 |
28 | def test_check_oaep_mgf1_error
29 | assert_exp_equals(OpenSSL::PKey::RSAError, 'OpenSSL::PKey::RSAError') do
30 | OpenSSL::PKCS1.check_oaep_mgf1(McAPI::Utils.decode('944cec7d0a6d0299c35cbb73c47a6874', 'hex'))
31 | end
32 | end
33 |
34 | def test_check_oaep_mgf1_error_good_zero
35 | assert_exp_equals(OpenSSL::PKey::RSAError, 'OpenSSL::PKey::RSAError') do
36 | padded = '00afff96bbebbd3c284edaf683d79641b20b593dde51e7d15b69e8f9f2cde3fb6acb96da9138187286b5f9266de7000ee5a9ec71cdff9658fbfd1d0c569cefc91f9cba28e9cee6bdd17624360191e7c7f15d4d4d72fa6c49e7bff01406b481e1cf4ca7bc8a3e4c8076dbde2e59ea4c5845a421ef4c3a8276492e6d867587f9a46b900b1a6d9617ef53710c25a1eb051dcf6994b0240121515ccd19a20c8ab7c55117060dfeec17d001d5d6fc3df1c5772c36524ca7982626fab4fb5cdc7b3c368da88637c02ab99f23f32f27cb4d16d841d91d259a636ed77c3050d6f0a16fbb224be6335e749cc0c80390ec180ae46b9d4afdedc5d68a846149778b91c88215'
37 | OpenSSL::PKCS1.check_oaep_mgf1(McAPI::Utils.decode(padded, 'hex'), '', OpenSSL::Digest::SHA256, OpenSSL::Digest::SHA256)
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/test/test_crypto_config.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/autorun'
4 | require 'minitest/mock'
5 | require_relative '../lib/mcapi/encryption/crypto/crypto'
6 | require_relative './utils'
7 | require 'json'
8 |
9 | class TestCryptoConfig < Minitest::Test
10 | def setup
11 | @test_config = JSON.parse(File.read('./test/mock/config.json'))
12 | @crypto = McAPI::Encryption::Crypto.new(@test_config)
13 | end
14 |
15 | def test_config_is_nil
16 | assert_equal(assert_raises(Exception) do
17 | McAPI::Encryption::Crypto.new(nil)
18 | end.message, 'Config not valid: config should be an Hash.')
19 | end
20 |
21 | def test_config_is_not_hash
22 | assert_equal(assert_raises(Exception) do
23 | McAPI::Encryption::Crypto.new('')
24 | end.message, 'Config not valid: config should be an Hash.')
25 | end
26 |
27 | def test_config_is_hash
28 | assert_equal(assert_raises(Exception) do
29 | McAPI::Encryption::Crypto.new({})
30 | end.message, 'Config not valid: paths should be an array of path element.')
31 | end
32 |
33 | def test_config_all_props_defined
34 | assert !@crypto.nil?
35 | end
36 |
37 | def test_fingerprint_public_key_ok
38 | config = @test_config.dup
39 | config['encryptionCertificate'] = './test/res/pub_cert_0.pem'
40 | crypto = McAPI::Encryption::Crypto.new(config)
41 | fingerprint = crypto.send :compute_public_fingerprint, 'publicKey'
42 | assert_equal '4bf20ad3389076f6404d37f0efef488eebe2304ea48d0aa0b6b372ab9b5f0f9d', fingerprint
43 | config['dataEncoding'] = 'base64'
44 | crypto = McAPI::Encryption::Crypto.new(config)
45 | fingerprint = crypto.send :compute_public_fingerprint, 'publicKey'
46 | assert_equal '4bf20ad3389076f6404d37f0efef488eebe2304ea48d0aa0b6b372ab9b5f0f9d', fingerprint
47 | end
48 |
49 | def test_fingerprint_public_certificate_ok
50 | config = @test_config.dup
51 | config['encryptionCertificate'] = './test/res/pub_cert_1.pem'
52 | crypto = McAPI::Encryption::Crypto.new(config)
53 | fingerprint = crypto.send :compute_public_fingerprint, 'certificate'
54 | assert_equal '67e80e19b8a50da945726e32672623d69aff375a9d83c4181026ec4efbb7c800', fingerprint
55 | end
56 |
57 | def test_fingerprint_public_certificate_ok__b64
58 | config = @test_config.dup
59 | config['dataEncoding'] = 'base64'
60 | config['encryptionCertificate'] = './test/res/pub_cert_1.pem'
61 | crypto = McAPI::Encryption::Crypto.new(config)
62 | fingerprint = crypto.send :compute_public_fingerprint, 'certificate'
63 | assert_equal 'Z+gOGbilDalFcm4yZyYj1pr/N1qdg8QYECbsTvu3yAA=', fingerprint
64 | end
65 |
66 | def test_fingerprint_wrong_type
67 | crypto = McAPI::Encryption::Crypto.new(@test_config)
68 | assert_exp_equals(RuntimeError, 'Selected public fingerprint not supported') do
69 | crypto.send :compute_public_fingerprint, 'wrongtype'
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/lib/mcapi/encryption/jwe_encryption.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'crypto/jwe-crypto'
4 | require_relative 'utils/hash.ext'
5 | require 'json'
6 |
7 | module McAPI
8 | module Encryption
9 | #
10 | # Performs JWE encryption on HTTP payloads.
11 | #
12 | class JweEncryption
13 | #
14 | # Create a new instance with the provided configuration
15 | #
16 | # @param [Hash] config Configuration object
17 | #
18 | def initialize(config)
19 | @config = config
20 | @crypto = McAPI::Encryption::JweCrypto.new(config)
21 | end
22 |
23 | #
24 | # Encrypt parts of a HTTP request using the given config
25 | #
26 | # @param [String] endpoint HTTP URL for the current call
27 | # @param [Object|nil] header HTTP header
28 | # @param [String,Hash] body HTTP body
29 | #
30 | # @return [Hash] Hash with two keys:
31 | # * :header header with encrypted value (if configured with header)
32 | # * :body encrypted body
33 | #
34 | def encrypt(endpoint, body)
35 | body = JSON.parse(body) if body.is_a?(String)
36 | config = McAPI::Utils.config?(endpoint, @config)
37 | body_map = body
38 | if config
39 | body_map = config['toEncrypt'].map do |v|
40 | encrypt_with_body(v, body)
41 | end
42 | end
43 | { body: config ? McAPI::Utils.compute_body(config['toEncrypt'], body_map) { body.json } : body.json }
44 | end
45 |
46 | #
47 | # Decrypt part of the HTTP response using the given config
48 | #
49 | # @param [Object] response object as obtained from the http client
50 | #
51 | # @return [Object] response object with decrypted fields
52 | #
53 | def decrypt(response)
54 | response = JSON.parse(response)
55 | config = McAPI::Utils.config?(response['request']['url'], @config)
56 | body_map = response
57 | if config
58 | body_map = config['toDecrypt'].map do |v|
59 | decrypt_with_body(v, response['body'])
60 | end
61 | end
62 | response['body'] = McAPI::Utils.compute_body(config['toDecrypt'], body_map) { response['body'] } unless config.nil?
63 | JSON.generate(response)
64 | end
65 |
66 | private
67 |
68 | def encrypt_with_body(path, body)
69 | elem = McAPI::Utils.elem_from_path(path['element'], body)
70 | return unless elem && elem[:node]
71 |
72 | encrypted_data = @crypto.encrypt_data(data: JSON.generate(elem[:node]))
73 | body = McAPI::Utils.mutate_obj_prop(path['obj'], encrypted_data, body)
74 | unless McAPI::Utils.json_root?(path['obj']) || path['element'] == "#{path['obj']}.#{@config['encryptedValueFieldName']}"
75 | McAPI::Utils.delete_node(path['element'], body)
76 | end
77 | body
78 | end
79 |
80 | def decrypt_with_body(path, body)
81 | elem = McAPI::Utils.elem_from_path(path['element'], body)
82 | return unless elem && elem[:node]
83 |
84 | decrypted = @crypto.decrypt_data(encrypted_data: elem[:node][@config['encryptedValueFieldName']])
85 | begin
86 | decrypted = JSON.parse(decrypted)
87 | rescue JSON::ParserError
88 | # ignored
89 | end
90 |
91 | McAPI::Utils.mutate_obj_prop(path['obj'], decrypted, body, path['element'], @encryption_response_properties)
92 | end
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/test/test_crypto_cryptography.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/autorun'
4 | require 'minitest/mock'
5 | require_relative '../lib/mcapi/encryption/crypto/crypto'
6 | require 'json'
7 |
8 | class TestCryptoCryptography < Minitest::Test
9 | def setup
10 | @test_config = JSON.parse(File.read('./test/mock/config.json'))
11 | @crypto = McAPI::Encryption::Crypto.new(@test_config)
12 | end
13 |
14 | def test_encrypt_valid_obj_key_and_iv
15 | iv = ['6f38f3ecd8b92c2fd2537a7235deb9a8'].pack('H*')
16 | secret_key = ['bab78b5ec588274a4dd2a60834efcf60'].pack('H*')
17 | assert_equal @crypto.encrypt_data(data: '{"text":"message"}', iv: iv, secret_key: secret_key)['encryptedData'], '3590b63d1520a57bd4cd1414a7a75f47d65f99e1427d6cfe744d72ee60f2b232'
18 | end
19 |
20 | def test_decrypt_valid_obj
21 | resp = @crypto.decrypt_data('3590b63d1520a57bd4cd1414a7a75f47d65f99e1427d6cfe744d72ee60f2b232',
22 | '6f38f3ecd8b92c2fd2537a7235deb9a8',
23 | 'e283a661efa235fbc5e7243b7b78914a7f33574eb66cc1854829f7debfce4163f3ce86ad2c3ed2c8fe97b2258ab8a158281147698b7fddf5e82544b0b637353d2c204798f014112a5e278db0b29ad852b1417dc761593fad3f0a1771797771796dc1e8ae916adaf3f4486aa79af9d4028bc8d17399d50c80667ea73a8a5d1341a9160f9422aaeb0b4667f345ea637ac993e80a452cb8341468483b7443f764967264aaebb2cad4513e4922d076a094afebcf1c71b53ba3cfedb736fa2ca5de5c1e2aa88b781d30c27debd28c2f5d83e89107d5214e3bb3fe186412d78cefe951e384f236e55cd3a67fb13c0d6950f097453f76e7679143bd4e62d986ce9dc770',
24 | 'SHA-512')
25 | assert_equal resp, '{"text":"message"}'
26 | end
27 |
28 | def test_decrypt3_aes
29 | config = @test_config.dup
30 | config['encoding'] = 'hex'
31 | config['encryptionCertificate'] = './test/res/test_certificate.cert'
32 | config['privateKey'] = './test/res/test_key_pkcs1-2048.pem'
33 | config['oaepPaddingDigestAlgorithm'] = 'SHA-256'
34 | crypto = McAPI::Encryption::Crypto.new(config)
35 | resp = crypto.decrypt_data(
36 | # encrypted data
37 | '5ad04f6072f98dd53f06c1026339724543c8125582c120a02a193944fcb600c6411a60bc3942752fd1c2fd2176429094fae7194e6a3b5ce8e149d562d3fcab7593f5386edd556716e0c116a71894d609747d2d0b28a3ce1631329923f97f9a2d753142a74d313dfce9fa5e8add2de465302e486d6087a4da44bfaa7c2d4f3b3f0ac610842fc0f5303bf19e599c84fc7f844c80cdabf40080f74fb4f85a89b351712b36b9db0c20a22faa66e08051f1c0c0cd4e1e4a64f1773645caf4e90500d757215d91a353a3719793cdfbc2e8d52bc117ddcfa0b09bccab85d5245c0698f3613cce8fece99d2b8e5c95d5ab0f98f680ed95047e5a5b51177e8b7b775d5b8c90bd4fc0ff64e40517ab8b206ec9f71f51d13b34cd70ff1f6e32f7f8c5df4aca297ed33662879f9ba1d42cddfa1eebc8802a690b0ebba20b04a7c7ed6fcd211e6a60dd4688bfe4398c31da974819075c76895577157c67a6ede1372fab78d265a09b84923f9298592fb407260706ea5bb3f64d38e7bc7fd100833e5bdee89360510cc03980bae17d1ad3b2691111f43b4f3f61b9ed284abaa9fed4865a322390',
38 | # iv
39 | 'fb2057968fa06067b6ab4c732c32cbcd',
40 | # key
41 | '7686c2472f8d53175074dd2830b4f875753343e59eec16a131f26e9e8026c3052993d8c9ad6eba04048f6a54b64160a13da28333816dfc178db2ed30068519d211c84fd7edc79838b58e97bb688b46215614308760e49d2fec95bfdf0570ce9fc5cdf814dca0dfface3d67b24b743d6003a072a882c1662ee24a9adf8b4d5825b5be74e6b73f9d08a8a2099a3fb875240ada002397c47be8a71c74e864bf8b1654365ddd2efe7b2ee44a75e08979993bfc1727cb8304607e295cab2e2dd8a8776e9678e8b9653b7e831d7b50a08d5ed1ac8c15f2933bcefef8d5b160d3a296bbdeac9d355879c0f8fc97860e17537465534095581374e9f29b1c10c7e860a638',
42 | 'SHA-256'
43 | )
44 | resp = JSON.parse(resp)
45 | assert !resp['mapping'].nil?
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/test_jwe_encryption.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/autorun'
4 | require 'minitest/mock'
5 | require_relative '../lib/mcapi/encryption/jwe_encryption'
6 | require_relative '../lib/mcapi/encryption/utils/utils'
7 | require 'json'
8 |
9 | class TestJweEncryption < Minitest::Test
10 | def setup
11 | @test_config = JSON.parse(File.read('./test/mock/jwe-config.json'))
12 | end
13 |
14 | def test_encrypt_field_level
15 | request = JSON.generate(
16 | {
17 | "mapping": {
18 | "customer_identifier": "CUST_12345",
19 | "customer_name": {
20 | "first_name": "John",
21 | "last_name": "Doe"
22 | }
23 | }
24 | }
25 | )
26 |
27 | jwe = McAPI::Encryption::JweEncryption.new(@test_config)
28 | res = jwe.encrypt('/mappings', request)
29 | assert res
30 | assert res[:body]['encrypted_payload']['encrypted_data']
31 | assert !res[:body]['mapping']['customer_identifier']
32 | end
33 |
34 | def test_encrypt_full_payload
35 | request = JSON.generate(
36 | {
37 | "mapping": {
38 | "customer_identifier": "CUST_12345",
39 | "customer_name": {
40 | "first_name": "John",
41 | "last_name": "Doe"
42 | }
43 | }
44 | }
45 | )
46 |
47 | jwe = McAPI::Encryption::JweEncryption.new(@test_config)
48 | res = jwe.encrypt('/resource', request)
49 | assert res
50 | assert res[:body]['encrypted_data']
51 | assert !res[:body]['mapping']
52 | end
53 |
54 | def test_encrypt_decrypt_root_array
55 | jwe = McAPI::Encryption::JweEncryption.new(@test_config)
56 | request = JSON.generate([{}, []])
57 |
58 | res = jwe.encrypt('/arrays', request)
59 |
60 | resp = JSON.generate(request: { url: '/arrays' }, body: res[:body])
61 | decrypted_resp = JSON.parse(jwe.decrypt(resp))
62 |
63 | assert_equal JSON.generate(decrypted_resp['body']), request
64 | end
65 |
66 | def test_encrypt_config_not_found
67 | jwe = McAPI::Encryption::JweEncryption.new(@test_config)
68 | request = JSON.generate(
69 | elem1: {
70 | encryptedData: {
71 | accountNumber: '5123456789012345'
72 | }
73 | }
74 | )
75 | res = jwe.encrypt('/not-exists', request)
76 | assert_nil res[:header]
77 | assert_equal request, JSON.generate(res[:body])
78 | end
79 |
80 | def test_decrypt_field_level
81 | resp = File.read('./test/mock/jwe-response.json')
82 | jwe = McAPI::Encryption::JweEncryption.new(@test_config)
83 | decrypted = JSON.parse(jwe.decrypt(resp))
84 | assert_equal decrypted['body']['mapping']['customer_identifier'], 'CUST_12345'
85 | assert !decrypted['body']['encrypted_payload']
86 | end
87 |
88 | def test_decrypt_a256gcm
89 | resp = File.read('./test/mock/jwe-response-a256gcm.json')
90 | jwe = McAPI::Encryption::JweEncryption.new(@test_config)
91 | decrypted = JSON.parse(jwe.decrypt(resp))
92 | assert_equal decrypted['body']['mapping']['customer_identifier'], 'CUST_12345'
93 | assert !decrypted['body']['encrypted_data']
94 | end
95 |
96 | def test_decrypt_a128gcm
97 | resp = File.read('./test/mock/jwe-response-a128gcm.json')
98 | jwe = McAPI::Encryption::JweEncryption.new(@test_config)
99 | decrypted = JSON.parse(jwe.decrypt(resp))
100 | assert_equal decrypted['body']['foo'], 'bar'
101 | assert !decrypted['body']['encrypted_data']
102 | end
103 |
104 | def test_decrypt_a192gcm
105 | resp = File.read('./test/mock/jwe-response-a192gcm.json')
106 | jwe = McAPI::Encryption::JweEncryption.new(@test_config)
107 | decrypted = JSON.parse(jwe.decrypt(resp))
108 | assert_equal decrypted['body']['foo'], 'bar'
109 | assert !decrypted['body']['encrypted_data']
110 | end
111 |
112 | def test_decrypt_cbc
113 | resp = File.read('./test/mock/jwe-response-cbc.json')
114 | jwe = McAPI::Encryption::JweEncryption.new(@test_config)
115 | decrypted = JSON.parse(jwe.decrypt(resp))
116 | assert !decrypted['body']['encrypted_data']
117 | end
118 | end
119 |
--------------------------------------------------------------------------------
/lib/mcapi/encryption/openapi_interceptor.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'field_level_encryption'
4 | require_relative 'jwe_encryption'
5 | require_relative 'utils/utils'
6 |
7 | module McAPI
8 | module Encryption
9 | ##
10 | # Service class that provide interceptor facilities for OpenApi swagger client
11 | #
12 | class OpenAPIInterceptor
13 | class << self
14 | #
15 | # Install the field level encryption in the OpenAPI HTTP client
16 | # adding encryption/decryption capabilities for the request/response payload.
17 | #
18 | # @param [Object] swagger_client OpenAPI service client (it can be generated by the swagger code generator)
19 | # @param [Hash] config configuration object describing which field to enable encryption/decryption
20 | #
21 | def install_field_level_encryption(swagger_client, config)
22 | fle = McAPI::Encryption::FieldLevelEncryption.new(config)
23 | # Hooking ApiClient#call_api
24 | hook_call_api fle
25 | # Hooking ApiClient#deserialize
26 | hook_deserialize fle
27 | McAPI::Encryption::OpenAPIInterceptor.init_call_api swagger_client
28 | McAPI::Encryption::OpenAPIInterceptor.init_deserialize swagger_client
29 | end
30 |
31 | #
32 | # Install the JWE encryption in the OpenAPI HTTP client
33 | # adding encryption/decryption capabilities for the request/response payload.
34 | #
35 | # @param [Object] swagger_client OpenAPI service client (it can be generated by the swagger code generator)
36 | # @param [Hash] config configuration object describing which field to enable encryption/decryption
37 | #
38 | def install_jwe_encryption(swagger_client, config)
39 | jwe = McAPI::Encryption::JweEncryption.new(config)
40 | # Hooking ApiClient#call_api
41 | hook_call_api jwe
42 | # Hooking ApiClient#deserialize
43 | hook_deserialize jwe
44 | McAPI::Encryption::OpenAPIInterceptor.init_call_api swagger_client
45 | McAPI::Encryption::OpenAPIInterceptor.init_deserialize swagger_client
46 | end
47 |
48 | private
49 |
50 | def hook_call_api(enc)
51 | self.class.send :define_method, :init_call_api do |client|
52 | client.define_singleton_method(:call_api) do |http_method, path, opts|
53 | if opts && opts[:body]
54 | if enc.instance_of? McAPI::Encryption::FieldLevelEncryption
55 | encrypted = enc.encrypt(path, opts[:header_params], opts[:body])
56 | opts[:body] = JSON.generate(encrypted[:body])
57 | else
58 | encrypted = enc.encrypt(path, opts[:body])
59 | opts[:body] = JSON.generate(encrypted[:body])
60 | end
61 | end
62 | # noinspection RubySuperCallWithoutSuperclassInspection
63 | super(http_method, path, opts)
64 | end
65 | end
66 | end
67 |
68 | def hook_deserialize(enc)
69 | self.class.send :define_method, :init_deserialize do |client|
70 | client.define_singleton_method(:deserialize) do |response, return_type|
71 | if response&.body
72 | endpoint = response.request.base_url.sub client.config.base_url, ''
73 | to_decrypt = { headers: McAPI::Utils.parse_header(response.options[:response_headers]),
74 | request: { url: endpoint },
75 | body: JSON.parse(response.body) }
76 |
77 | decrypted = enc.decrypt(JSON.generate(to_decrypt, symbolize_names: false))
78 | body = JSON.generate(JSON.parse(decrypted)['body'])
79 | if enc.instance_of? McAPI::Encryption::JweEncryption
80 | body = McAPI::Utils.parse_decrypted_payload(body)
81 | end
82 | response.options[:response_body] = JSON.generate(JSON.parse(body))
83 | end
84 | # noinspection RubySuperCallWithoutSuperclassInspection
85 | super(response, return_type)
86 | end
87 | end
88 | end
89 | end
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/lib/mcapi/encryption/utils/openssl_rsa_oaep.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'openssl'
4 |
5 | #
6 | # Extends the OpenSSL library with RSA OAEP MGF1 padding
7 | #
8 |
9 | module OpenSSL
10 | module PKey
11 | class RSA
12 | def public_encrypt_oaep(str, label = '', md = nil, mgf1md = nil)
13 | padded = PKCS1.add_oaep_mgf1(str, n.num_bytes, label, md, mgf1md)
14 | public_encrypt(padded, OpenSSL::PKey::RSA::NO_PADDING)
15 | end
16 |
17 | def private_decrypt_oaep(str, label = '', md = nil, mgf1md = nil)
18 | padded = private_decrypt(str, OpenSSL::PKey::RSA::NO_PADDING)
19 | PKCS1.check_oaep_mgf1(padded, label, md, mgf1md)
20 | end
21 | end
22 | end
23 |
24 | module PKCS1
25 | def add_oaep_mgf1(str, len, label = '', md = nil, mgf1md = nil)
26 | md ||= OpenSSL::Digest::SHA1
27 | mgf1md ||= md
28 |
29 | mdlen = md.new.digest_length
30 | z_len = len - str.bytesize - 2 * mdlen - 2
31 | raise OpenSSL::PKey::RSAError, 'key size too small' if len < 2 * mdlen + 1
32 | raise OpenSSL::PKey::RSAError, 'data too large for key size' if z_len.negative?
33 |
34 | l_hash = md.digest(label)
35 | db = l_hash + ([0] * z_len + [1]).pack('C*') + [str].pack('a*')
36 | seed = OpenSSL::Random.random_bytes(mdlen)
37 |
38 | masked_db = mgf1_xor(db, seed, mgf1md)
39 | masked_seed = mgf1_xor(seed, masked_db, mgf1md)
40 |
41 | [0, masked_seed, masked_db].pack('Ca*a*')
42 | end
43 |
44 | module_function :add_oaep_mgf1
45 |
46 | def check_oaep_mgf1(str, label = '', md = nil, mgf1md = nil)
47 | md ||= OpenSSL::Digest::SHA1
48 | mgf1md ||= md
49 |
50 | mdlen = md.new.digest_length
51 | em = str.bytes
52 | raise OpenSSL::PKey::RSAError if em.size < 2 * mdlen + 2
53 |
54 | # Keep constant calculation even if the text is invalid in order to avoid attacks.
55 | good = secure_byte_is_zero(em[0])
56 | masked_seed = em[1...1 + mdlen].pack('C*')
57 | masked_db = em[1 + mdlen...em.size].pack('C*')
58 |
59 | seed = mgf1_xor(masked_seed, masked_db, mgf1md)
60 | db = mgf1_xor(masked_db, seed, mgf1md)
61 | db_bytes = db.bytes
62 |
63 | l_hash = md.digest(label)
64 | good &= secure_hash_eq(l_hash.bytes, db_bytes[0...mdlen])
65 |
66 | one_index = 0
67 | found_one_byte = 0
68 | (mdlen...db_bytes.size).each do |i|
69 | equals1 = secure_byte_eq(db_bytes[i], 1)
70 | equals0 = secure_byte_is_zero(db_bytes[i])
71 | one_index = secure_select(~found_one_byte & equals1, i, one_index)
72 | found_one_byte |= equals1
73 | good &= (found_one_byte | equals0)
74 | end
75 |
76 | good &= found_one_byte
77 |
78 | raise OpenSSL::PKey::RSAError if good.zero?
79 |
80 | db_bytes[one_index + 1...db_bytes.size].pack('C*')
81 | end
82 |
83 | module_function :check_oaep_mgf1
84 |
85 | def mgf1_xor(out, seed, md)
86 | counter = 0
87 | out_bytes = out.bytes
88 | mask_bytes = []
89 | while mask_bytes.size < out_bytes.size
90 | mask_bytes += md.digest([seed, counter].pack('a*N')).bytes
91 | counter += 1
92 | end
93 | out_bytes.size.times do |i|
94 | out_bytes[i] ^= mask_bytes[i]
95 | end
96 | out_bytes.pack('C*')
97 | end
98 |
99 | module_function :mgf1_xor
100 |
101 | # Constant time comparistion utilities.
102 | def secure_byte_is_zero(v)
103 | v - 1 >> 8
104 | end
105 |
106 | def secure_byte_eq(v1, v2)
107 | secure_byte_is_zero(v1 ^ v2)
108 | end
109 |
110 | def secure_select(mask, eq, ne)
111 | (mask & eq) | (~mask & ne)
112 | end
113 |
114 | def secure_hash_eq(vs1, vs2)
115 | # Assumes the given hash values have the same size.
116 | # This check is not constant time, but should not depends on the texts.
117 | return 0 unless vs1.size == vs2.size
118 |
119 | res = secure_byte_is_zero(0)
120 | (0...vs1.size).each do |i|
121 | res &= secure_byte_eq(vs1[i], vs2[i])
122 | end
123 | res
124 | end
125 |
126 | module_function :secure_byte_is_zero, :secure_byte_eq, :secure_select, :secure_hash_eq
127 | end
128 | end
129 |
--------------------------------------------------------------------------------
/lib/mcapi/encryption/utils/utils.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'base64'
4 |
5 | module McAPI
6 | #
7 | # Utils module
8 | module Utils
9 | #
10 | # Data encoding
11 | #
12 | def self.encode(data, encoding)
13 | return unless encoding
14 |
15 | case encoding.downcase
16 | when 'hex'
17 | data.each_byte.map { |b| format('%02x', b.to_i) }.join
18 | when 'base64'
19 | Base64.encode64(data).delete("\n")
20 | else
21 | raise 'Encoding not supported'
22 | end
23 | end
24 |
25 | #
26 | # Data decoding
27 | #
28 | def self.decode(data, encoding)
29 | return unless encoding
30 |
31 | case encoding.downcase
32 | when 'hex'
33 | [data].pack('H*')
34 | when 'base64'
35 | Base64.decode64(data)
36 | else
37 | raise 'Encoding not supported'
38 | end
39 | end
40 |
41 | #
42 | # Create Digest object for the provided digest string
43 | #
44 | def self.create_message_digest(digest)
45 | return unless digest
46 |
47 | case digest.upcase
48 | when 'SHA-256', 'SHA256'
49 | OpenSSL::Digest::SHA256
50 | when 'SHA-512', 'SHA512'
51 | OpenSSL::Digest::SHA512
52 | else
53 | raise 'Digest algorithm not supported'
54 | end
55 | end
56 |
57 | def self.contains(config, props)
58 | props.any? do |i|
59 | config.key? i
60 | end
61 | end
62 |
63 | #
64 | # Perform JSON object properties manipulations
65 | #
66 | def self.mutate_obj_prop(path, value, obj, src_path = nil, properties = [])
67 | tmp = obj
68 | prev = nil
69 | return obj unless path
70 |
71 | delete_node(src_path, obj, properties) if src_path
72 | paths = path.split('.')
73 | unless path == '$'
74 | paths.each do |e|
75 | tmp[e] = {} unless tmp[e]
76 | prev = tmp
77 | tmp = tmp[e]
78 | end
79 | end
80 | elem = path.split('.').pop
81 | if elem == '$'
82 | obj = value # replace root
83 | elsif value.is_a?(Hash) && !value.is_a?(Array)
84 | prev[elem] = {} unless prev[elem].is_a?(Hash)
85 | override_props(prev[elem], value)
86 | else
87 | prev[elem] = value
88 | end
89 | obj
90 | end
91 |
92 | def self.override_props(target, obj)
93 | obj.each do |k, _|
94 | target[k] = obj[k]
95 | end
96 | end
97 |
98 | #
99 | # Delete node from JSON object
100 | #
101 | def self.delete_node(path, obj, properties = [])
102 | return unless path && obj
103 |
104 | paths = path.split('.')
105 | to_delete = paths[paths.size - 1]
106 | paths.each_with_index do |e, index|
107 | prev = obj
108 | next unless obj[e]
109 |
110 | obj = obj[e]
111 | prev.delete(to_delete) if obj && index == paths.size - 1
112 | end
113 | obj.keys.each { |e| obj.delete(e) } if paths.length == 1 && paths[0] == '$'
114 | properties.each { |e| obj.delete(e) } if paths.empty?
115 | end
116 |
117 | #
118 | # Parse raw HTTP Header
119 | #
120 | def self.parse_header(raw)
121 | raw = raw.partition("\n").last
122 | header = Hash.new([].freeze)
123 | field = nil
124 | raw.each_line do |line|
125 | case line
126 | when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om
127 | field = Regexp.last_match(1)
128 | value = Regexp.last_match(2)
129 | field.downcase!
130 | header[field] = [] unless header.key?(field)
131 | header[field] << value
132 | when /^\s+(.*?)\s*\z/om
133 | value = Regexp.last_match(1)
134 | raise Exception, "bad header '#{line}'." unless field
135 |
136 | header[field][-1] << ' ' << value
137 | else
138 | raise Exception, "bad header '#{line}'."
139 | end
140 | end
141 | header.each do |_key, values|
142 | values.each do |value|
143 | value.strip!
144 | value.gsub!(/\s+/, ' ')
145 | end
146 | end
147 | header
148 | end
149 |
150 | #
151 | # Get an element from the JSON path
152 | #
153 | def self.elem_from_path(path, obj)
154 | parent = nil
155 | paths = path.split('.')
156 | if path && !paths.empty?
157 | paths.each do |e|
158 | parent = obj
159 | obj = json_root?(e) ? obj : obj[e]
160 | end
161 | end
162 | { node: obj, parent: parent }
163 | rescue StandardError
164 | nil
165 | end
166 |
167 | #
168 | # Check whether the encryption/decryption path refers to the root element
169 | #
170 | def self.json_root?(elem)
171 | elem == '$'
172 | end
173 |
174 | def self.config?(endpoint, config)
175 | return unless endpoint
176 |
177 | endpoint = endpoint.split('?').shift
178 | conf = config['paths'].select { |e| endpoint.match(e['path']) }
179 | conf.empty? ? nil : conf[0]
180 | end
181 |
182 | def self.compute_body(config_param, body_map)
183 | encryption_param?(config_param, body_map) ? body_map[0] : yield
184 | end
185 |
186 | def self.encryption_param?(enc_param, body_map)
187 | enc_param.length == 1 && body_map.length == 1
188 | end
189 |
190 | def self.parse_decrypted_payload(payload)
191 | parsed_payload = payload.gsub("\\u000f", "")
192 | parsed_payload = parsed_payload.gsub("\\", "")
193 | if parsed_payload[0] == "\""
194 | parsed_payload = parsed_payload.delete_prefix('"').delete_suffix('"')
195 | end
196 | parsed_payload
197 | end
198 | end
199 | end
200 |
--------------------------------------------------------------------------------
/lib/mcapi/encryption/crypto/jwe-crypto.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'json'
4 | require 'openssl'
5 | require 'base64'
6 | require 'securerandom'
7 | require_relative '../utils/utils'
8 | require_relative '../utils/openssl_rsa_oaep'
9 |
10 | module McAPI
11 | module Encryption
12 | #
13 | # JWE Crypto class provide RSA/AES encrypt/decrypt methods
14 | #
15 | class JweCrypto
16 | #
17 | # Create a new instance with the provided config
18 | #
19 | # @param [Hash] config configuration object
20 | #
21 | def initialize(config)
22 | @encoding = config['dataEncoding']
23 | @cert = OpenSSL::X509::Certificate.new(IO.binread(config['encryptionCertificate']))
24 | if config['privateKey']
25 | @private_key = OpenSSL::PKey.read(IO.binread(config['privateKey']))
26 | end
27 | @encrypted_value_field_name = config['encryptedValueFieldName'] || 'encryptedData'
28 | @public_key_fingerprint = compute_public_fingerprint
29 | end
30 |
31 | #
32 | # Perform data encryption:
33 | #
34 | # @param [String] data json string to encrypt
35 | #
36 | # @return [Hash] encrypted data
37 | #
38 | def encrypt_data(data:)
39 | cek = SecureRandom.random_bytes(32)
40 | iv = SecureRandom.random_bytes(12)
41 |
42 | md = OpenSSL::Digest::SHA256
43 | encrypted_key = @cert.public_key.public_encrypt_oaep(cek, '', md, md)
44 |
45 | header = generate_header('RSA-OAEP-256', 'A256GCM')
46 | json_hdr = header.to_json
47 | auth_data = jwe_encode(json_hdr)
48 |
49 | cipher = OpenSSL::Cipher.new('aes-256-gcm')
50 | cipher.encrypt
51 | cipher.key = cek
52 | cipher.iv = iv
53 | cipher.padding = 0
54 | cipher.auth_data = auth_data
55 | cipher_text = cipher.update(data) + cipher.final
56 |
57 | payload = generate_serialization(json_hdr, encrypted_key, cipher_text, iv, cipher.auth_tag)
58 | {
59 | @encrypted_value_field_name => payload
60 | }
61 | end
62 |
63 | #
64 | # Perform data decryption
65 | #
66 | # @param [String] encrypted_data encrypted data to decrypt
67 | #
68 | # @return [String] Decrypted JSON object
69 | #
70 | def decrypt_data(encrypted_data:)
71 | parts = encrypted_data.split('.')
72 | encrypted_header, encrypted_key, initialization_vector, cipher_text, authentication_tag = parts
73 |
74 | jwe_header = jwe_decode(encrypted_header)
75 | encrypted_key = jwe_decode(encrypted_key)
76 | iv = jwe_decode(initialization_vector)
77 | cipher_text = jwe_decode(cipher_text)
78 | cipher_tag = jwe_decode(authentication_tag)
79 |
80 | md = OpenSSL::Digest::SHA256
81 | cek = @private_key.private_decrypt_oaep(encrypted_key, '', md, md)
82 |
83 | enc_method = JSON.parse(jwe_header)['enc']
84 |
85 | if enc_method == "A256GCM"
86 | enc_string = "aes-256-gcm"
87 | elsif enc_method == "A128GCM"
88 | enc_string = "aes-128-gcm"
89 | elsif enc_method == "A192GCM"
90 | enc_string = "aes-192-gcm"
91 | elsif enc_method == "A128CBC-HS256"
92 | cek = cek.byteslice(16, cek.length)
93 | enc_string = "aes-128-cbc"
94 | else
95 | raise Exception, "Encryption method '#{enc_method}' not supported."
96 | end
97 |
98 | cipher = OpenSSL::Cipher.new(enc_string)
99 | cipher.decrypt
100 | cipher.key = cek
101 | cipher.iv = iv
102 | if enc_method == "A256GCM" || enc_method == "A128GCM" || enc_method == "A192GCM"
103 | cipher.auth_data = encrypted_header
104 | cipher.auth_tag = cipher_tag
105 | end
106 |
107 | cipher.update(cipher_text) + cipher.final
108 | end
109 |
110 | private
111 |
112 | #
113 | # Compute the fingerprint for the provided public key
114 | #
115 | # @return [String] the computed fingerprint encoded using the configured encoding
116 | #
117 | def compute_public_fingerprint
118 | OpenSSL::Digest::SHA256.new(@cert.public_key.to_der).to_s
119 | end
120 |
121 | #
122 | # Generate the JWE header for the provided encryption algorithm and encryption method
123 | #
124 | # @param [String] alg the cryptographic algorithm used to encrypt the value of the CEK
125 | # @param [String] enc the content encryption algorithm used to perform authenticated encryption on the plaintext
126 | #
127 | # @return [Hash] the JWE header
128 | #
129 | def generate_header(alg, enc)
130 | { alg: alg, enc: enc, kid: @public_key_fingerprint, cty: 'application/json' }
131 | end
132 |
133 | #
134 | # URL safe Base64 encode the provided value
135 | #
136 | # @param [String] value to be encoded
137 | #
138 | # @return [String] URL safe Base64 encoded value
139 | #
140 | def jwe_encode(value)
141 | ::Base64.urlsafe_encode64(value).delete('=')
142 | end
143 |
144 | #
145 | # URL safe Base64 decode the provided value
146 | #
147 | # @param [String] value to be decoded
148 | #
149 | # @return [String] URL safe Base64 decoded value
150 | #
151 | def jwe_decode(value)
152 | padlen = 4 - (value.length % 4)
153 | if padlen < 4
154 | pad = '=' * padlen
155 | value += pad
156 | end
157 | ::Base64.urlsafe_decode64(value)
158 | end
159 |
160 | #
161 | # Generate JWE compact payload from the provided values
162 | #
163 | # @param [String] hdr JWE header
164 | # @param [String] cek content encryption key
165 | # @param [String] content cipher text
166 | # @param [String] iv initialization vector
167 | # @param [String] tag cipher auth tag
168 | #
169 | # @return [String] URL safe Base64 decoded value
170 | #
171 | def generate_serialization(hdr, cek, content, iv, tag)
172 | [hdr, cek, iv, content, tag].map { |piece| jwe_encode(piece) }.join '.'
173 | end
174 | end
175 | end
176 | end
177 |
--------------------------------------------------------------------------------
/test/test_interceptor.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/autorun'
4 | require 'minitest/mock'
5 |
6 | require_relative '../lib/mcapi/encryption/openapi_interceptor'
7 |
8 | class TestInterceptor < Minitest::Test
9 | HEADER = JSON.parse('{"Accept":"application/json","Content-Type":"application/json"}')
10 | BODY = JSON.parse('{"mapping":{"customer_identifier":"CUST_12345","merchant":{"name":"LAWN MOWER SERVICE","mastercard_assigned_id":"354315","merchant_category_code":"4563"}}}')
11 |
12 | RAW_HEADER = 'HTTP/1.1 200 OK
13 | x-oaep-hashing-algorithm: SHA256
14 | x-encrypted-key: z9hICyBnN+X857KoYn1Ft3GOmMt+GGBcNvI+QlRmKZ2DVJgfaA9YoB96tE9SNRMux+9ZbAEARGXsnURFrcG3+xDf0XUvzwmcXqC5tZw4Xrw8LNXjZBPnHGCe7S1MY+x90BBK+pxD+LEkCIlsHYugButd8SgHehb34FQ35lJA/RuMQoQgbbqBAVdPNoFLap8HkzIMk83Kfuqe7vlZtEinFALs78JTDIah2Ytybwq83a+NXstKj8o8PvyKZEmD5QgrMnuxozSsWZso/OdS6Po5WFxpPaEIvOtsPw79SvujPlE2v1WK7yAvwNbBRvuxtXdz4mWNUNsmvpxD+Cvw1/2AQA==
15 | RequestId: 1c1059e2-2b91-4a08-a8f9-dfb65908242e
16 | x-iv: 4+vu1fcirK916IukdAfn7A==
17 | x-public-key-fingerprint: 761b003c1eade3a5490e5000d37887baa5e6ec0e226c07706e599451fc032a79
18 | Content-Type: application/json
19 |
20 | '
21 |
22 | class Request
23 | attr_accessor :base_url
24 | end
25 |
26 | class Response
27 | attr_reader :body
28 | attr_reader :request
29 | attr_accessor :options
30 |
31 | def initialize(req, body)
32 | @request = Request.new
33 | @request.base_url = req
34 | @body = body
35 | @options = {}
36 | end
37 | end
38 |
39 | class Config
40 | attr_accessor :base_url
41 | end
42 |
43 | class MockApiClient
44 | def initialize(config = nil)
45 | @config = config
46 | end
47 |
48 | attr_reader :config
49 |
50 | def call_api(_http_method, _path, opts)
51 | opts
52 | end
53 |
54 | def deserialize(response, _return_type)
55 | response
56 | end
57 | end
58 |
59 | def setup
60 | @config = JSON.parse(File.read('./test/mock/config-interceptor.json'))
61 | @jwe_config = JSON.parse(File.read('./test/mock/jwe-config.json'))
62 | end
63 |
64 | def test_intercept_request_nil_opts
65 | api_client = MockApiClient.new
66 | McAPI::Encryption::OpenAPIInterceptor.install_field_level_encryption(api_client, @config)
67 | resp = api_client.call_api('GET', '/resource', nil)
68 | assert_nil(resp)
69 | end
70 |
71 | def test_intercept_request_with_opts
72 | api_client = MockApiClient.new
73 | McAPI::Encryption::OpenAPIInterceptor.install_field_level_encryption(api_client, @config)
74 | opts = {}
75 | opts[:body] = BODY
76 | opts[:header_params] = HEADER
77 | resp = api_client.call_api('GET', '/mappings/mappingId', opts)
78 | assert resp[:body]
79 | assert !JSON.parse(resp[:body])['encrypted_payload']['data'].empty?
80 | end
81 |
82 | def test_intercept_response_nil
83 | api_client = MockApiClient.new
84 | McAPI::Encryption::OpenAPIInterceptor.install_field_level_encryption(api_client, @config)
85 | assert_nil api_client.deserialize(nil, nil)
86 | end
87 |
88 | def test_intercept_response
89 | config = Config.new
90 | config.base_url = 'https://api.mastercard.com/example_api'
91 | api_client = MockApiClient.new(config)
92 | McAPI::Encryption::OpenAPIInterceptor.install_field_level_encryption(api_client, @config)
93 | response_body = JSON.generate(JSON.parse(File.read('./test/mock/response-interceptor.json')))
94 | response = Response.new('https://api.mastercard.com/example_api/mappings/search', response_body)
95 | response.options[:response_headers] = RAW_HEADER
96 | decrypted = api_client.deserialize(response, Response)
97 | assert decrypted
98 | assert decrypted.options
99 | assert decrypted.options[:response_body]
100 | decrypted = JSON.parse(decrypted.options[:response_body])
101 | assert_equal decrypted['mapping']['merchant']['name'], 'LAWN MOWER SERVICE'
102 | end
103 |
104 | def test_intercept_jwe_request_nil_opts
105 | api_client = MockApiClient.new
106 | McAPI::Encryption::OpenAPIInterceptor.install_jwe_encryption(api_client, @jwe_config)
107 | resp = api_client.call_api('GET', '/resource', nil)
108 | assert_nil(resp)
109 | end
110 |
111 | def test_intercept_jwe_request_field_level
112 | api_client = MockApiClient.new
113 | McAPI::Encryption::OpenAPIInterceptor.install_jwe_encryption(api_client, @jwe_config)
114 | opts = {}
115 | opts[:body] = BODY
116 | resp = api_client.call_api('GET', '/mappings/mappingId', opts)
117 | assert resp[:body]
118 | assert !JSON.parse(resp[:body])['encrypted_payload']['encrypted_data'].empty?
119 | assert !JSON.parse(resp[:body])['mapping'].empty?
120 | end
121 |
122 | def test_intercept_jwe_request_entire_payload
123 | api_client = MockApiClient.new
124 | McAPI::Encryption::OpenAPIInterceptor.install_jwe_encryption(api_client, @jwe_config)
125 | opts = {}
126 | opts[:body] = BODY
127 | resp = api_client.call_api('GET', '/resource', opts)
128 | assert resp[:body]
129 | assert !JSON.parse(resp[:body])['encrypted_data'].empty?
130 | assert !JSON.parse(resp[:body])['mapping']
131 | end
132 |
133 | def test_intercept_jwe_response_nil
134 | api_client = MockApiClient.new
135 | McAPI::Encryption::OpenAPIInterceptor.install_jwe_encryption(api_client, @jwe_config)
136 | assert_nil api_client.deserialize(nil, nil)
137 | end
138 |
139 | def test_intercept_jwe_response
140 | config = Config.new
141 | config.base_url = 'https://api.mastercard.com/example_api'
142 | api_client = MockApiClient.new(config)
143 | McAPI::Encryption::OpenAPIInterceptor.install_jwe_encryption(api_client, @jwe_config)
144 | response_body = JSON.generate(JSON.parse(File.read('./test/mock/jwe-response-interceptor.json')))
145 | response = Response.new('https://api.mastercard.com/example_api/mappings/search', response_body)
146 | response.options[:response_headers] = RAW_HEADER
147 | decrypted = api_client.deserialize(response, Response)
148 | assert decrypted
149 | assert decrypted.options
150 | assert decrypted.options[:response_body]
151 | decrypted = JSON.parse(decrypted.options[:response_body])
152 | assert_equal decrypted['mapping']['customer_identifier'], 'CUST_12345'
153 | end
154 | end
155 |
--------------------------------------------------------------------------------
/lib/mcapi/encryption/field_level_encryption.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'crypto/crypto'
4 | require_relative 'utils/hash.ext'
5 | require_relative 'utils/utils'
6 | require 'json'
7 |
8 | module McAPI
9 | module Encryption
10 | #
11 | # Performs field level encryption on HTTP payloads.
12 | #
13 | class FieldLevelEncryption
14 | #
15 | # Create a new instance with the provided configuration
16 | #
17 | # @param [Hash] config Configuration object
18 | #
19 | def initialize(config)
20 | @config = config
21 | @crypto = McAPI::Encryption::Crypto.new(config)
22 | @is_with_header = config['ivHeaderName'] && config['encryptedKeyHeaderName']
23 | @encryption_response_properties = [@config['ivFieldName'], @config['encryptedKeyFieldName'],
24 | @config['publicKeyFingerprintFieldName'], @config['oaepHashingAlgorithmFieldName']]
25 | end
26 |
27 | #
28 | # Encrypt parts of a HTTP request using the given config
29 | #
30 | # @param [String] endpoint HTTP URL for the current call
31 | # @param [Object|nil] header HTTP header
32 | # @param [String,Hash] body HTTP body
33 | #
34 | # @return [Hash] Hash with two keys:
35 | # * :header header with encrypted value (if configured with header)
36 | # * :body encrypted body
37 | #
38 | def encrypt(endpoint, header, body)
39 | body = JSON.parse(body) if body.is_a?(String)
40 | config = McAPI::Utils.config?(endpoint, @config)
41 | body_map = body
42 | if config
43 | if !@is_with_header
44 | body_map = config['toEncrypt'].map do |v|
45 | encrypt_with_body(v, body)
46 | end
47 | else
48 | enc_params = @crypto.new_encryption_params
49 | body_map = config['toEncrypt'].map do |v|
50 | body = encrypt_with_header(v, enc_params, header, body)
51 | end
52 | end
53 | end
54 | { header: header, body: config ? McAPI::Utils.compute_body(config['toEncrypt'], body_map) { body.json } : body.json }
55 | end
56 |
57 | #
58 | # Decrypt part of the HTTP response using the given config
59 | #
60 | # @param [Object] response object as obtained from the http client
61 | #
62 | # @return [Object] response object with decrypted fields
63 | #
64 | def decrypt(response)
65 | response = JSON.parse(response)
66 | config = McAPI::Utils.config?(response['request']['url'], @config)
67 | body_map = response
68 | if config
69 | if !@is_with_header
70 | body_map = config['toDecrypt'].map do |v|
71 | decrypt_with_body(v, response['body'])
72 | end
73 | else
74 | config['toDecrypt'].each do |v|
75 | elem = McAPI::Utils.elem_from_path(v['obj'], response['body'])
76 | decrypt_with_header(v, elem, response) if elem[:node][v['element']]
77 | end
78 | end
79 | end
80 | response['body'] = McAPI::Utils.compute_body(config['toDecrypt'], body_map) { response['body'] } unless config.nil?
81 | JSON.generate(response)
82 | end
83 |
84 | private
85 |
86 | def encrypt_with_body(path, body)
87 | elem = McAPI::Utils.elem_from_path(path['element'], body)
88 | return unless elem && elem[:node]
89 |
90 | encrypted_data = @crypto.encrypt_data(data: JSON.generate(elem[:node]))
91 | body = McAPI::Utils.mutate_obj_prop(path['obj'], encrypted_data, body)
92 | unless McAPI::Utils.json_root?(path['obj']) || path['element'] == "#{path['obj']}.#{@config['encryptedValueFieldName']}"
93 | McAPI::Utils.delete_node(path['element'], body)
94 | end
95 | body
96 | end
97 |
98 | def encrypt_with_header(path, enc_params, header, body)
99 | elem = McAPI::Utils.elem_from_path(path['element'], body)
100 | return unless elem && elem[:node]
101 |
102 | encrypted_data = @crypto.encrypt_data(data: JSON.generate(elem[:node]), encryption_params: enc_params)
103 | body = { path['obj'] => { @config['encryptedValueFieldName'] => encrypted_data[@config['encryptedValueFieldName']] } }
104 | set_header(header, enc_params)
105 | body
106 | end
107 |
108 | def decrypt_with_body(path, body)
109 | elem = McAPI::Utils.elem_from_path(path['element'], body)
110 | return unless elem && elem[:node]
111 |
112 | decrypted = @crypto.decrypt_data(elem[:node][@config['encryptedValueFieldName']],
113 | elem[:node][@config['ivFieldName']],
114 | elem[:node][@config['encryptedKeyFieldName']],
115 | elem[:node][@config['oaepHashingAlgorithmFieldName']])
116 | begin
117 | decrypted = JSON.parse(decrypted)
118 | rescue JSON::ParserError
119 | # ignored
120 | end
121 |
122 | McAPI::Utils.mutate_obj_prop(path['obj'], decrypted, body, path['element'], @encryption_response_properties)
123 | end
124 |
125 | def decrypt_with_header(path, elem, response)
126 | encrypted_data = elem[:node][path['element']][@config['encryptedValueFieldName']]
127 | response['body'].clear
128 | response['body'] = JSON.parse(@crypto.decrypt_data(encrypted_data,
129 | response['headers'][@config['ivHeaderName']][0],
130 | response['headers'][@config['encryptedKeyHeaderName']][0],
131 | response['headers'][@config['oaepHashingAlgorithmHeaderName']][0]))
132 | end
133 |
134 | def set_header(header, params)
135 | header[@config['encryptedKeyHeaderName']] = params[:encoded][:encryptedKey]
136 | header[@config['ivHeaderName']] = params[:encoded][:iv]
137 | header[@config['oaepHashingAlgorithmHeaderName']] = params[:oaepHashingAlgorithm].sub('-', '')
138 | header[@config['publicKeyFingerprintHeaderName']] = params[:publicKeyFingerprint]
139 | end
140 | end
141 | end
142 | end
143 |
--------------------------------------------------------------------------------
/test/test_payload_encryption.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/autorun'
4 | require 'minitest/mock'
5 | require_relative '../lib/mcapi/encryption/field_level_encryption'
6 | require 'json'
7 |
8 | class TestPayloadEncryption < Minitest::Test
9 | def setup
10 | @test_config = JSON.parse(File.read('./test/mock/config.json'))
11 | @test_config['encryptedValueFieldName'] = 'encryptedValue'
12 | end
13 |
14 | def test_encrypt_with_sibling
15 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
16 | body = JSON.parse(JSON.generate(
17 | data: {
18 | field1: 'value1',
19 | field2: 'value2'
20 | },
21 | encryptedData: {}
22 | ))
23 | fle.send :encrypt_with_body, JSON.parse(JSON.generate(element: 'data', obj: 'encryptedData')), body
24 | # puts JSON.pretty_generate(body)
25 | assert !body['data']
26 | assert body['encryptedData']
27 | assert body['encryptedData']['encryptedValue']
28 | assert body['encryptedData']['iv']
29 | assert body['encryptedData']['encryptedKey']
30 | assert body['encryptedData']['publicKeyFingerprint']
31 | assert body['encryptedData']['oaepHashingAlgorithm']
32 | end
33 |
34 | def test_encrypt_dest_obj_not_exists
35 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
36 | body = JSON.parse(JSON.generate(
37 | itemsToEncrypt: {
38 | first: 'first',
39 | second: 'second'
40 | },
41 | dontEncrypt: {
42 | text: 'just text...'
43 | }
44 | ))
45 | fle.send :encrypt_with_body, JSON.parse(JSON.generate(element: 'itemsToEncrypt', obj: 'encryptedItems')), body
46 | assert body['dontEncrypt']
47 | assert !body['itemsToEncrypt']
48 | assert body['encryptedItems']
49 | assert body['encryptedItems']['encryptedValue']
50 | assert body['encryptedItems']['iv']
51 | assert body['encryptedItems']['encryptedKey']
52 | assert body['encryptedItems']['publicKeyFingerprint']
53 | assert body['encryptedItems']['oaepHashingAlgorithm']
54 | end
55 |
56 | def test_encrypt_nested_obj_to_encrypt
57 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
58 | body = JSON.parse(JSON.generate(
59 | path: {
60 | to: {
61 | encryptedData: {
62 | sensitive: 'secret',
63 | sensitive2: 'secret 2'
64 | }
65 | }
66 | }
67 | ))
68 | fle.send :encrypt_with_body, JSON.parse(JSON.generate(element: 'path.to.encryptedData', obj: 'path.to')), body
69 | assert body['path']
70 | assert body['path']['to']
71 | assert body['path']['to']['encryptedValue']
72 | assert body['path']['to']['iv']
73 | assert body['path']['to']['encryptedKey']
74 | assert body['path']['to']['publicKeyFingerprint']
75 | assert body['path']['to']['oaepHashingAlgorithm']
76 | assert !body['path']['to']['encryptedData']
77 | end
78 |
79 | def test_encrypt_nested_object_create_different_nested_object_and_delete_it
80 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
81 | body = JSON.parse(JSON.generate(
82 | path: {
83 | to: {
84 | foo: {
85 | sensitive: 'secret',
86 | sensitive2: 'secret 2'
87 | }
88 | }
89 | }
90 | ))
91 | fle.send :encrypt_with_body, JSON.parse(JSON.generate(element: 'path.to.foo', obj: 'path.to.encryptedFoo')), body
92 | assert body['path']
93 | assert body['path']['to']
94 | assert body['path']['to']['encryptedFoo']
95 | assert body['path']['to']['encryptedFoo']['encryptedValue']
96 | assert body['path']['to']['encryptedFoo']['iv']
97 | assert body['path']['to']['encryptedFoo']['encryptedKey']
98 | assert body['path']['to']['encryptedFoo']['publicKeyFingerprint']
99 | assert body['path']['to']['encryptedFoo']['oaepHashingAlgorithm']
100 | assert !body['path']['to']['foo']
101 | end
102 |
103 | def test_decrypt_nested_properties_create_new_obj
104 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
105 | body = JSON.parse(JSON.generate(
106 | path: {
107 | to: {
108 | encryptedFoo: {
109 | encryptedValue:
110 | '3097e36bf8b71637a0273abe69c23752d6157464ce49f6f35120d28bedfb63a1f2c8087be3a3bc9775592db41db87a8c',
111 | iv: '22507f596fffb45b15244356981d7ea1',
112 | encryptedKey:
113 | 'd4714161898b8bc5c54a63f71ae7c7a40734e4f7c7e27d121ac5e85a3fa47946aa3546027abe0874d751d5ae701491a7f572fc30fa08dd671d358746ffe8709cba36010f97864105b175c51b6f32d36d981287698a3f6f8707aedf980cce19bfe7c5286ddba87b7f3e5abbfa88a980779037c0b7902d340d73201cf3f0b546c2ad9f54e4b71a43504da947a3cb7af54d61717624e636a90069be3c46c19b9ae8b76794321b877544dd03f0ca816288672ef361c3e8f14d4a1ee96ba72d21e3a36c020aa174635a8579b0e9af761d96437e1fa167f00888ff2532292e7a220f5bc948f8159dea2541b8c6df6463213de292b4485076241c90706efad93f9b98ea',
114 | publicKeyFingerprint:
115 | '80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279',
116 | oaepHashingAlgorithm: 'SHA512'
117 | }
118 | }
119 | }
120 | ))
121 | fle.send :decrypt_with_body, JSON.parse(JSON.generate(element: 'path.to.encryptedFoo', obj: 'path.to.foo')), body
122 | assert body['path']
123 | assert body['path']['to']
124 | assert body['path']['to']['foo']
125 | assert_equal body['path']['to']['foo']['accountNumber'], '5123456789012345'
126 | assert !body['path']['to']['encryptedFoo']
127 | end
128 |
129 | def test_decrypt_primitive_type
130 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
131 | body = JSON.parse(JSON.generate(
132 | data: {
133 | encryptedValue:
134 | '982f5df1dfff3cc551a49091140cbb22',
135 | iv: '6f38f3ecd8b92c2fd2537a7235deb9a8',
136 | encryptedKey:
137 | 'aebc427a2ecefc748f7f42e34a1ea6592ea20a299c107df5655483bd6ae11f9de72cb407ea0342bcac7cc29e7e9bbfbfadf8209c1f7ae2429d6f8a914d161c8a91890b3d0363b1bdd80be64712fdd6ea35496649be05e0f87001185dd79fe7a7e23c716348afe27500aaacc2cbba89793a437ae5103170fd04f0e5b4f73089c0660b44506780346e84bfb6de183f15f49132bafac651f4b4e1fadc55205d3773877c4de02c0825d7ee0b44fd3f8bb4382b999237e190352e9199eceb209fe4a91ab88fa9b4988a1a9265be38582667784da83a8ec7c307e027884d49e76771dc4a4fb472e771324f72d24299f8f621d9501d6fd59de08ccce39f533a15e17022',
138 | publicKeyFingerprint:
139 | '80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279',
140 | oaepHashingAlgorithm: 'SHA512'
141 | }
142 | ))
143 | fle.send :decrypt_with_body, JSON.parse(JSON.generate(element: 'data', obj: 'data')), body
144 | assert body['data']
145 | assert_equal body['data'], 'string'
146 | end
147 | end
148 |
--------------------------------------------------------------------------------
/lib/mcapi/encryption/crypto/crypto.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'json'
4 | require 'openssl'
5 | require 'base64'
6 | require_relative '../utils/utils'
7 | require_relative '../utils/openssl_rsa_oaep'
8 |
9 | module McAPI
10 | module Encryption
11 | #
12 | # Crypto class provide RSA/AES encrypt/decrypt methods
13 | #
14 | class Crypto
15 | #
16 | # Create a new instance with the provided config
17 | #
18 | # @param [Hash] config configuration object
19 | #
20 | def initialize(config)
21 | valid_config?(config)
22 | @encoding = config['dataEncoding']
23 | @cert = OpenSSL::X509::Certificate.new(IO.binread(config['encryptionCertificate']))
24 | if config['privateKey']
25 | @private_key = OpenSSL::PKey.read(IO.binread(config['privateKey']))
26 | end
27 | @oaep_hashing_alg = config['oaepPaddingDigestAlgorithm']
28 | @encrypted_value_field_name = config['encryptedValueFieldName']
29 | @encrypted_key_field_name = config['encryptedKeyFieldName']
30 | @public_key_fingerprint = compute_public_fingerprint(config['publicKeyFingerprintType'])
31 | @public_key_fingerprint_field_name = config['publicKeyFingerprintFieldName']
32 | @oaep_hashing_alg_field_name = config['oaepHashingAlgorithmFieldName']
33 | end
34 |
35 | #
36 | # Generate encryption parameters.
37 | #
38 | # @param [String,nil] iv IV to use instead to generate a random IV
39 | # @param [String,nil] secret_key Secret Key to use instead to generate a random key
40 | #
41 | # @return [Hash] hash with the generated encryption parameters
42 | #
43 | def new_encryption_params(iv = nil, secret_key = nil)
44 | # Generate a secret key (should be 128 (or 256) bits)
45 | secret_key ||= OpenSSL::Random.random_bytes(16)
46 | # Generate a random initialization vector (IV)
47 | iv ||= OpenSSL::Random.random_bytes(16)
48 | md = Utils.create_message_digest(@oaep_hashing_alg)
49 | # Encrypt secret key with issuer key
50 | encrypted_key = @cert.public_key.public_encrypt_oaep(secret_key, '', md, md)
51 |
52 | {
53 | iv: iv,
54 | secretKey: secret_key,
55 | encryptedKey: encrypted_key,
56 | oaepHashingAlgorithm: @oaep_hashing_alg,
57 | publicKeyFingerprint: @public_key_fingerprint,
58 | encoded: {
59 | iv: Utils.encode(iv, @encoding),
60 | secretKey: Utils.encode(secret_key, @encoding),
61 | encryptedKey: Utils.encode(encrypted_key, @encoding)
62 | }
63 | }
64 | end
65 |
66 | #
67 | # Perform data encryption:
68 | # If +iv+, +secret_key+, +encryption_params+ and +encoding+ are not provided, randoms will be generated.
69 | #
70 | # @param [String] data json string to encrypt
71 | # @param [String,nil] (optional) iv Initialization vector to use to create the cipher, if not provided generate a random one
72 | # @param [String,nil] (optional) encryption_params encryption parameters
73 | # @param [String] encoding encoding to use for the encrypted bytes (hex or base64)
74 | #
75 | # @return [String] encrypted data
76 | #
77 | def encrypt_data(data:, iv: nil, secret_key: nil, encryption_params: nil, encoding: nil)
78 | encoding ||= @encoding
79 | encryption_params ||= new_encryption_params(iv, secret_key)
80 | # Create Symmetric Cipher: AES 128-bit
81 | aes = OpenSSL::Cipher::AES.new(128, :CBC)
82 | # Initialize for encryption mode
83 | aes.encrypt
84 | aes.iv = encryption_params[:iv]
85 | aes.key = encryption_params[:secretKey]
86 | encrypted = aes.update(data) + aes.final
87 | data = {
88 | @encrypted_value_field_name => Utils.encode(encrypted, encoding),
89 | 'iv' => Utils.encode(encryption_params[:iv], encoding)
90 | }
91 | data[@encrypted_key_field_name] = Utils.encode(encryption_params[:encryptedKey], encoding) if @encrypted_key_field_name
92 | data[@public_key_fingerprint_field_name] = @public_key_fingerprint if @public_key_fingerprint
93 | data[@oaep_hashing_alg_field_name] = @oaep_hashing_alg.sub('-', '') if @oaep_hashing_alg_field_name
94 | data
95 | end
96 |
97 | #
98 | # Perform data decryption
99 | #
100 | # @param [String] encrypted_data encrypted data to decrypt
101 | # @param [String] iv Initialization vector to use to create the Decipher
102 | # @param [String] encrypted_key Encrypted key to use to decrypt the data
103 | # (the key is the decrypted using the provided PrivateKey)
104 | # @param [String] oaep_hashing_alg OAEP Algorithm to use
105 | #
106 | # @return [String] Decrypted JSON object
107 | #
108 | def decrypt_data(encrypted_data, iv, encrypted_key, oaep_hashing_alg)
109 | md = Utils.create_message_digest(oaep_hashing_alg)
110 | decrypted_key = @private_key.private_decrypt_oaep(Utils.decode(encrypted_key, @encoding), '', md, md)
111 | aes = OpenSSL::Cipher::AES.new(decrypted_key.size * 8, :CBC)
112 | aes.decrypt
113 | aes.key = decrypted_key
114 | aes.iv = Utils.decode(iv, @encoding)
115 | aes.update(Utils.decode(encrypted_data, @encoding)) + aes.final
116 | end
117 |
118 | private
119 |
120 | #
121 | # Compute the fingerprint for the provided public key
122 | #
123 | # @param [Hash] type: +certificate+ or +publickey+
124 | #
125 | # @return [String] the computed fingerprint encoded using the configured encoding
126 | #
127 | def compute_public_fingerprint(type)
128 | return unless type
129 |
130 | case type.downcase
131 | when 'certificate'
132 | if @encoding == 'hex'
133 | OpenSSL::Digest::SHA256.new(@cert.to_der).to_s
134 | else
135 | Digest::SHA256.base64digest(@cert.to_der)
136 | end
137 | when 'publickey'
138 | OpenSSL::Digest::SHA256.new(@cert.public_key.to_der).to_s
139 | else
140 | raise 'Selected public fingerprint not supported'
141 | end
142 | end
143 |
144 | #
145 | # Check if the passed configuration is valid
146 | #
147 | def valid_config?(config)
148 | props_basic = %w[oaepPaddingDigestAlgorithm paths dataEncoding encryptionCertificate encryptedValueFieldName]
149 | props_field = %w[ivFieldName encryptedKeyFieldName]
150 | props_header = %w[ivHeaderName encryptedKeyHeaderName oaepHashingAlgorithmHeaderName]
151 | props_fingerprint = %w[publicKeyFingerprintType publicKeyFingerprintFieldName publicKeyFingerprintHeaderName]
152 | props_opt_fingerprint = %w[publicKeyFingerprint]
153 |
154 | raise 'Config not valid: config should be an Hash.' unless config.is_a?(Hash)
155 | raise 'Config not valid: paths should be an array of path element.' unless config['paths'] && config['paths'].is_a?(Array)
156 |
157 | check_props = !Utils.contains(config, props_basic) ||
158 | (!Utils.contains(config, props_field) && !Utils.contains(config, props_header))
159 | raise 'Config not valid: please check that all the properties are defined.' if check_props
160 |
161 | raise 'Config not valid: paths should be not empty.' if config['paths'].length.zero?
162 | raise "Config not valid: dataEncoding should be 'hex' or 'base64'" if config['dataEncoding'] != 'hex' &&
163 | config['dataEncoding'] != 'base64'
164 |
165 | check_finger = !Utils.contains(config, props_opt_fingerprint) &&
166 | (config[props_fingerprint[1]] || config[props_fingerprint[2]]) &&
167 | config[props_fingerprint[0]] != 'certificate' &&
168 | config[props_fingerprint[0]] != 'publicKey'
169 | raise "Config not valid: propertiesFingerprint should be: 'certificate' or 'publicKey'" if check_finger
170 | end
171 | end
172 | end
173 | end
174 |
--------------------------------------------------------------------------------
/test/test_utils.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/autorun'
4 | require 'minitest/mock'
5 | require_relative '../lib/mcapi/encryption/utils/utils'
6 | require_relative './utils'
7 | require 'json'
8 |
9 | class TestUtils < Minitest::Test
10 | def test_decode_wrong_encoding
11 | assert_equal(assert_raises(RuntimeError) do
12 | McAPI::Utils.decode('dGVzdGluZ3V0aWxz', 'XXX')
13 | end.message, 'Encoding not supported')
14 | end
15 |
16 | def test_decode_from_hex
17 | assert_equal 'testingutils', McAPI::Utils.decode('74657374696E677574696C73', 'hex')
18 | end
19 |
20 | def test_decode_from_base64
21 | assert_equal 'testingutils', McAPI::Utils.decode('dGVzdGluZ3V0aWxz', 'base64')
22 | end
23 |
24 | def test_encoding_decoding_hex
25 | str = 'testing utils'
26 | res = McAPI::Utils.encode(str, 'hex')
27 | assert_equal str, McAPI::Utils.decode(res, 'hex')
28 | end
29 |
30 | def test_encoding_decoding_base64
31 | str = 'testing utils'
32 | res = McAPI::Utils.encode(str, 'base64')
33 | assert_equal str, McAPI::Utils.decode(res, 'base64')
34 | end
35 |
36 | def test_encode_nil
37 | res = McAPI::Utils.encode('data', nil)
38 | assert_nil(res)
39 | end
40 |
41 | def test_encode_not_supported
42 | assert_exp_equals(RuntimeError, 'Encoding not supported') do
43 | McAPI::Utils.encode('data', 'someencoding')
44 | end
45 | end
46 |
47 | def test_decode_nil
48 | res = McAPI::Utils.decode('data', nil)
49 | assert_nil(res)
50 | end
51 |
52 | def test_decode_not_supported
53 | assert_exp_equals(RuntimeError, 'Encoding not supported') do
54 | McAPI::Utils.decode('data', 'some encoding')
55 | end
56 | end
57 |
58 | def test_create_message_digest_not_supported
59 | assert_exp_equals(RuntimeError, 'Digest algorithm not supported') do
60 | McAPI::Utils.create_message_digest('some digest')
61 | end
62 | end
63 |
64 | def test_mutate_obj_prop_change_obj_value
65 | obj = {
66 | first: {
67 | second: {
68 | third: {
69 | field: 'value'
70 | }
71 | }
72 | }
73 | }
74 | path = 'first.second.third'
75 | obj = JSON.parse(JSON.generate(obj))
76 | McAPI::Utils.mutate_obj_prop(path, 'changed', obj)
77 | assert_equal 'changed', obj['first']['second']['third']
78 | end
79 |
80 | def test_mutate_obj_prop_dont_change
81 | obj = {
82 | first: {
83 | second: {
84 | third: {
85 | field: 'value'
86 | }
87 | }
88 | }
89 | }
90 | path = 'first.second.not_exists'
91 | obj = JSON.parse(JSON.generate(obj))
92 | McAPI::Utils.mutate_obj_prop(path, 'changed', obj)
93 | assert_equal 'value', obj['first']['second']['third']['field']
94 | end
95 |
96 | def test_mutate_obj_prop_field_not_found
97 | obj = {
98 | first: {
99 | second: {
100 | third: {
101 | field: 'value'
102 | }
103 | }
104 | }
105 | }
106 | path = 'first.foo.third'
107 | obj = JSON.parse(JSON.generate(obj))
108 | McAPI::Utils.mutate_obj_prop(path, 'changed', obj)
109 | assert_equal 'value', obj['first']['second']['third']['field']
110 | end
111 |
112 | def test_mutate_obj_prop_field_not_found_create_it_long_path
113 | obj = {
114 | first: {
115 | second: {
116 | third: {
117 | field: 'value'
118 | }
119 | }
120 | }
121 | }
122 | path = 'foo.bar.yet.another.foo.bar'
123 | obj = JSON.parse(JSON.generate(obj))
124 | McAPI::Utils.mutate_obj_prop(path, 'changed', obj)
125 | obj = JSON.generate(obj)
126 | res = JSON.generate(
127 | "first": {
128 | "second": {
129 | "third": {
130 | "field": 'value'
131 | }
132 | }
133 | },
134 | "foo": {
135 | "bar": {
136 | "yet": {
137 | "another": {
138 | "foo": {
139 | "bar": 'changed'
140 | }
141 | }
142 | }
143 | }
144 | }
145 | )
146 | assert_equal obj, res
147 | end
148 |
149 | def test_mutate_obj_prop_first_part_correct_field_not_found_create_it
150 | obj = {
151 | first: {
152 | second: {
153 | third: {
154 | field: 'value'
155 | }
156 | }
157 | }
158 | }
159 | path = 'first.foo.third'
160 | obj = JSON.parse(JSON.generate(obj))
161 | McAPI::Utils.mutate_obj_prop(path, 'changed', obj)
162 | obj = JSON.generate(obj)
163 | res = JSON.generate(
164 | first: {
165 | second: {
166 | third: {
167 | field: 'value'
168 | }
169 | },
170 | foo: {
171 | third: 'changed'
172 | }
173 | }
174 | )
175 | assert_equal obj, res
176 | end
177 |
178 | def test_parse_header_empty
179 | res = McAPI::Utils.parse_header('')
180 | assert_equal res, {}
181 | end
182 |
183 | def test_parse_header_wrong_format
184 | assert_exp_equals(Exception, 'bad header \'efgc\'.') do
185 | McAPI::Utils.parse_header("abcd\nefgc")
186 | end
187 | end
188 |
189 | def test_parse_header_one_line
190 | res = McAPI::Utils.parse_header('First Line')
191 | assert_equal res, {}
192 | end
193 |
194 | def test_parse_header_more_lines
195 | res = McAPI::Utils.parse_header("First Line\nx-one: one")
196 | assert_equal res, 'x-one' => ['one']
197 | end
198 |
199 | def test_parse_header_more_lines_2
200 | res = McAPI::Utils.parse_header("First Line\nx-one: one\nx-two: two")
201 | assert_equal res, 'x-one' => ['one'], 'x-two' => ['two']
202 | end
203 |
204 | def test_parse_header_with_spaces
205 | res = McAPI::Utils.parse_header("First Line\nx-one: one\n \n")
206 | assert_equal res, 'x-one' => ['one']
207 | end
208 |
209 | def test_parse_header_bad_header
210 | assert_exp_equals(Exception, "bad header ' \n'.") do
211 | McAPI::Utils.parse_header("First Line\n \n")
212 | end
213 | end
214 |
215 | def test_delete_node_nulls
216 | McAPI::Utils.delete_node(nil, nil, nil)
217 | McAPI::Utils.delete_node('path.to.foo', nil, nil)
218 | body = JSON.parse(JSON.generate({}))
219 | McAPI::Utils.delete_node('path.to.foo', body)
220 | assert_equal body, JSON.parse(JSON.generate({}))
221 | end
222 |
223 | def test_delete_not_found_path_shouldn_t_remove
224 | body = JSON.parse(JSON.generate(path: { to: { foo: { field: 'value' } } }))
225 | body_dup = body.dup
226 | McAPI::Utils.delete_node('path.to.notfound', body)
227 | assert_equal body, body_dup
228 | end
229 |
230 | def test_delete_found_path_should_remove
231 | body = JSON.parse(JSON.generate(path: { to: { foo: { field: 'value' } } }))
232 | McAPI::Utils.delete_node('path.to.foo', body)
233 | assert_equal body, JSON.parse(JSON.generate(path: { to: {} }))
234 | end
235 |
236 | def test_delete_root_path_without_properties_shouldn_t_remove
237 | body_hash = { path: { to: { foo: { field: 'value' } } } }
238 | body = JSON.parse(JSON.generate(body_hash))
239 | McAPI::Utils.delete_node('', body)
240 | assert_equal body, JSON.parse(JSON.generate(body_hash))
241 | end
242 |
243 | def test_delete_root_path_with_properties_should_remove_the_properties
244 | body_hash = { path: { to: { foo: { field: 'value' } } }, prop: 'prop', prop2: 'prop2' }
245 | body = JSON.parse(JSON.generate(body_hash))
246 | McAPI::Utils.delete_node('', body, %w[prop prop2])
247 | assert_equal body, JSON.parse(JSON.generate("path": { "to": { "foo": { "field": 'value' } } }))
248 | end
249 |
250 | def test_elem_from_path_not_valid_path
251 | res = McAPI::Utils.elem_from_path('elem1.elem2', JSON.parse(JSON.generate(elem2: 'test')))
252 | assert_nil res
253 | end
254 |
255 | def test_elem_from_path_valid_path
256 | res = McAPI::Utils.elem_from_path('elem1.elem2', JSON.parse(JSON.generate(elem1: { elem2: 'test' })))
257 | assert_equal res[:node], 'test'
258 | assert_equal res[:parent], JSON.parse(JSON.generate(elem2: 'test'))
259 | end
260 | end
261 |
--------------------------------------------------------------------------------
/test/test_field_level_encryption.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/autorun'
4 | require 'minitest/mock'
5 | require_relative '../lib/mcapi/encryption/field_level_encryption'
6 | require_relative '../lib/mcapi/encryption/utils/utils'
7 | require 'json'
8 |
9 | class TestFieldLevelEncryption < Minitest::Test
10 | def setup
11 | @test_config = JSON.parse(File.read('./test/mock/config.json'))
12 | @test_config_with_header = JSON.parse(File.read('./test/mock/config-header.json'))
13 | @config_readme = JSON.parse(File.read('./test/mock/config-readme.json'))
14 | end
15 |
16 | def test_encrypt
17 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
18 | request = JSON.generate(
19 | elem1: {
20 | encryptedData: {
21 | accountNumber: '5123456789012345'
22 | },
23 | shouldBeThere: "here I'am"
24 | }
25 | )
26 |
27 | res = fle.encrypt('/resource?q=foobar', nil, request)
28 | assert res[:header].nil?
29 | assert res[:body]['elem1']['encryptedData']
30 | assert res[:body]['elem1']['shouldBeThere']
31 | assert res[:body]['elem1']['encryptedKey']
32 | assert res[:body]['elem1']['iv']
33 | assert res[:body]['elem1']['oaepHashingAlgorithm']
34 | assert res[:body]['elem1']['publicKeyFingerprint']
35 | assert !res[:body]['elem1']['encryptedData']['accountNumber']
36 | end
37 |
38 | def test_encrypt_with_header
39 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config_with_header)
40 | request = JSON.generate(
41 | encrypted_payload: {
42 | data: {
43 | accountNumber: '5123456789012345'
44 | }
45 | }
46 | )
47 | header = {}
48 | res = fle.encrypt('/resource', header, request)
49 | assert_equal res[:header], header
50 | assert header[@test_config_with_header['encryptedKeyHeaderName']]
51 | assert header[@test_config_with_header['ivHeaderName']]
52 | assert header[@test_config_with_header['oaepHashingAlgorithmHeaderName']]
53 | assert header[@test_config_with_header['publicKeyFingerprintHeaderName']]
54 | assert_equal res[:body]['encrypted_payload']['data'].length, 160
55 | end
56 |
57 | def test_encrypt_root_array
58 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
59 | request = JSON.generate([{}, []])
60 |
61 | res = fle.encrypt('/array-resp', nil, request)
62 | assert res[:header].nil?
63 | assert res[:body]['encryptedData']
64 | assert res[:body]['encryptedKey']
65 | assert res[:body]['iv']
66 | assert res[:body]['oaepHashingAlgorithm']
67 | assert res[:body]['publicKeyFingerprint']
68 | assert !res[:body]['elem1']
69 |
70 | resp = JSON.generate(request: { url: '/array-resp' }, body: res[:body])
71 | decrypted_resp = JSON.parse(fle.decrypt(resp))
72 |
73 | assert_equal JSON.generate(decrypted_resp['body']), request
74 | end
75 |
76 | def test_encrypt_config_not_found
77 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
78 | request = JSON.generate(
79 | elem1: {
80 | encryptedData: {
81 | accountNumber: '5123456789012345'
82 | }
83 | }
84 | )
85 | res = fle.encrypt('/not-exists', nil, request)
86 | assert_nil res[:header]
87 | assert_equal request, JSON.generate(res[:body])
88 | end
89 |
90 | def test_decrypt
91 | resp = File.read('./test/mock/response.json')
92 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
93 | decrypted = JSON.parse(fle.decrypt(resp))
94 | assert_equal decrypted['body']['foo']['accountNumber'], '5123456789012345'
95 | assert !decrypted['body']['foo']['elem1']
96 | assert !decrypted['body']['foo']['encryptedData']
97 | end
98 |
99 | def test_decrypt_response_replacing_whole_body
100 | resp = File.read('./test/mock/response-root.json')
101 | config = @test_config.dup
102 | config['paths'][0]['toDecrypt'][0]['obj'] = 'encryptedData'
103 | config['paths'][0]['toDecrypt'][0]['element'] = ''
104 | fle = McAPI::Encryption::FieldLevelEncryption.new(config)
105 | decrypted = JSON.parse(fle.decrypt(resp))
106 | assert_equal decrypted['body']['encryptedData']['accountNumber'], '5123456789012345'
107 | assert decrypted['body']['notDelete']
108 | end
109 |
110 | def test_decrypt_with_header
111 | resp = File.read('./test/mock/response-header.json')
112 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config_with_header)
113 | decrypted = JSON.parse(fle.decrypt(resp))
114 | assert_equal decrypted['body']['encrypted_payload']['data']['accountNumber'], '5123456789012345'
115 | end
116 |
117 | def test_decrypt_node_not_found_in_body
118 | resp = File.read('./test/mock/response-header.json')
119 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config_with_header)
120 | resp_hash = JSON.parse(resp)
121 | resp_hash['body'].delete('encrypted_payload')
122 | resp_hash['body'] = JSON.parse(JSON.generate(test: 'foo'))
123 | resp_hash = JSON.generate(resp_hash)
124 | decrypted = JSON.parse(fle.decrypt(resp_hash))
125 | assert_equal decrypted['body']['test'], 'foo'
126 | end
127 |
128 | def test_decrypt_without_config
129 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
130 | response = JSON.generate(request: { url: '/foobar' }, body: 'abc')
131 | assert_equal JSON.parse(fle.decrypt(response))['body'], 'abc'
132 | end
133 |
134 | def test_encrypt_body_payload_with_readme_config
135 | fle = McAPI::Encryption::FieldLevelEncryption.new(@config_readme)
136 | request = JSON.generate(
137 | path: {
138 | to: {
139 | encryptedData: {
140 | sensitive: 'this is a secret',
141 | sensitive2: 'this is a super secret!'
142 | }
143 | }
144 | }
145 | )
146 | res = fle.encrypt('/resource', nil, request)
147 | assert res[:header].nil?
148 | assert res[:body]['path']
149 | assert res[:body]['path']['to']
150 | assert res[:body]['path']['to']['encryptedData']
151 | assert !res[:body]['path']['to']['encryptedData']['sensitive']
152 | assert !res[:body]['path']['to']['encryptedData']['sensitive2']
153 | assert res[:body]['path']['to']['iv']
154 | assert res[:body]['path']['to']['encryptedKey']
155 | assert res[:body]['path']['to']['oaepHashingAlgorithm']
156 | assert res[:body]['path']['to']['publicKeyFingerprint']
157 | end
158 |
159 | def test_decrypt_response_with_readme_config
160 | fle = McAPI::Encryption::FieldLevelEncryption.new(@config_readme)
161 | resp = File.read('./test/mock/response-readme.json')
162 | decrypted = JSON.parse(fle.decrypt(resp))
163 | assert decrypted['body']['path']
164 | assert decrypted['body']['path']['to']['foo']
165 | assert decrypted['body']['path']['to']['foo']['sensitive']
166 | assert decrypted['body']['path']['to']['foo']['sensitive2']
167 | assert_equal decrypted['body']['path']['to']['foo']['sensitive'], 'this is a secret'
168 | assert_equal decrypted['body']['path']['to']['foo']['sensitive2'], 'this is a super secret!'
169 | end
170 |
171 | def test_decrypt_root_arrays
172 | resp = JSON.generate(
173 | request: {
174 | url: '/array-resp'
175 | },
176 | body: {
177 | encryptedData: '3496b0c505bcea6a849f8e30b553e6d4',
178 | iv: 'ed82c0496e9d5ac769d77bdb2eb27958',
179 | encryptedKey: '29ea447b70bdf85dd509b5d4a23dc0ffb29fd1acf50ed0800ec189fbcf1fb813fa075952c3de2915d63ab42f16be2e'\
180 | 'd46dc27ba289d692778a1d585b589039ba0b25bad326d699c45f6d3cffd77b5ec37fe12e2c5456d49980b2ccf16402e83a8e9765b9b9'\
181 | '3ca37d4d5181ec3e5327fd58387bc539238f1c20a8bc9f4174f5d032982a59726b3e0b9cf6011d4d7bfc3afaf617e768dea6762750bc'\
182 | 'e07339e3e55fdbd1a1cd12ee6bbfbc3c7a2d7f4e1313410eb0dad13e594a50a842ee1b2d0ff59d641987c417deaa151d679bc892e5c0'\
183 | '51b48781dbdefe74a12eb2b604b981e0be32ab81d01797117a24fbf6544850eed9b4aefad0eea7b3f5747b20f65d3f',
184 | oaepHashingAlgorithm: 'SHA256'
185 | }
186 | )
187 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
188 | decrypted = JSON.parse(fle.decrypt(resp))
189 |
190 | assert_equal JSON.generate(decrypted['body']), '[{},{}]'
191 | end
192 |
193 | def test_decrypt_root_array_to_path
194 | resp = JSON.generate(
195 | request: {
196 | url: '/array-resp2'
197 | },
198 | body: {
199 | encryptedData: '3496b0c505bcea6a849f8e30b553e6d4',
200 | iv: 'ed82c0496e9d5ac769d77bdb2eb27958',
201 | encryptedKey: '29ea447b70bdf85dd509b5d4a23dc0ffb29fd1acf50ed0800ec189fbcf1fb813fa075952c3de2915d63ab42f16be2e'\
202 | 'd46dc27ba289d692778a1d585b589039ba0b25bad326d699c45f6d3cffd77b5ec37fe12e2c5456d49980b2ccf16402e83a8e9765b9b9'\
203 | '3ca37d4d5181ec3e5327fd58387bc539238f1c20a8bc9f4174f5d032982a59726b3e0b9cf6011d4d7bfc3afaf617e768dea6762750bc'\
204 | 'e07339e3e55fdbd1a1cd12ee6bbfbc3c7a2d7f4e1313410eb0dad13e594a50a842ee1b2d0ff59d641987c417deaa151d679bc892e5c0'\
205 | '51b48781dbdefe74a12eb2b604b981e0be32ab81d01797117a24fbf6544850eed9b4aefad0eea7b3f5747b20f65d3f',
206 | oaepHashingAlgorithm: 'SHA256'
207 | }
208 | )
209 | fle = McAPI::Encryption::FieldLevelEncryption.new(@test_config)
210 | decrypted = JSON.parse(fle.decrypt(resp))
211 |
212 | assert_equal JSON.generate(decrypted['body']), '{"path":{"to":{"foo":[{},{}]}}}'
213 | end
214 | end
215 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # client-encryption-ruby
2 | [](https://developer.mastercard.com/)
3 |
4 | [](https://github.com/Mastercard/.github/blob/main/CLIENT_LIBRARY_DEPRECATION_POLICY.md)
5 | [](https://github.com/Mastercard/client-encryption-ruby/actions?query=workflow%3A%22Build+%26+Test%22)
6 | [](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-ruby)
7 | [](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-ruby)
8 | [](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-ruby)
9 | [](https://github.com/Mastercard/client-encryption-ruby/actions?query=workflow%3A%22broken+links%3F%22)
10 | [](https://rubygems.org/gems/mastercard-client-encryption)
11 | [](https://github.com/Mastercard/client-encryption-ruby/blob/master/LICENSE)
12 |
13 | ## Table of Contents
14 |
15 | - [Overview](#overview)
16 | - [Compatibility](#compatibility)
17 | - [References](#references)
18 | - [Versioning and Deprecation Policy](#versioning)
19 | - [Usage](#usage)
20 | - [Prerequisites](#prerequisites)
21 | - [Loading the Decryption Key](#loading-the-decryption-key)
22 | - [Adding the Library to Your Project](#adding-the-libraries-to-your-project)
23 | - [Performing Field Level Encryption and Decryption](#performing-payload-encryption-and-decryption)
24 | - [Integrating with OpenAPI Generator API Client Libraries](#integrating-with-openapi-generator-api-client-libraries)
25 |
26 | ## Overview
27 |
28 | Ruby library for Mastercard API compliant payload encryption/decryption.
29 |
30 | ### Compatibility
31 |
32 | - Ruby 2.4.4+
33 | - Truffle Ruby 1.0.0+
34 |
35 | ### References
36 | * [JSON Web Encryption (JWE)](https://datatracker.ietf.org/doc/html/rfc7516)
37 | * [Securing Sensitive Data Using Payload Encryption](https://developer.mastercard.com/platform/documentation/security-and-authentication/securing-sensitive-data-using-payload-encryption/)
38 |
39 | ### Versioning and Deprecation Policy
40 | * [Mastercard Versioning and Deprecation Policy](https://github.com/Mastercard/.github/blob/main/CLIENT_LIBRARY_DEPRECATION_POLICY.md)
41 |
42 | ## Usage
43 |
44 | ### Prerequisites
45 |
46 | Before using this library, you will need to set up a project in the [Mastercard Developers Portal](https://developer.mastercard.com).
47 |
48 | As part of this set up, you'll receive:
49 |
50 | - A public request encryption certificate (aka _Client Encryption Keys_)
51 | - A private response decryption key (aka _Mastercard Encryption Keys_)
52 |
53 | #### Loading the Decryption Key
54 |
55 | By default, the decryption key will be given in as a PKCS#12 password-protected file.
56 | The key can be loaded using either of the 2 methods below.
57 |
58 | 1. The following code shows how to load the decryption key using `OpenSSL`:
59 | ```ruby
60 | require 'openssl'
61 |
62 | is = File.binread("");
63 | signing_key = OpenSSL::PKCS12.new(is, "").key;
64 | ```
65 |
66 | 2. Follow our guide on [Exporting Your Signing Key](https://developer.mastercard.com/platform/documentation/security-and-authentication/using-oauth-1a-to-access-mastercard-apis/#exporting-your-signing-key)
67 |
68 | ### Installation
69 |
70 | If you want to use **mastercard-client-encryption** with [Ruby](https://www.ruby-lang.org/en/), it is available as Gem:
71 |
72 | - [https://rubygems.org/gems/mastercard-client-encryption](https://rubygems.org/gems/mastercard-client-encryption)
73 |
74 | **Adding the library to your project**
75 |
76 | Add this line to your application's Gemfile:
77 |
78 | ```ruby
79 | gem 'mastercard-client-encryption'
80 | ```
81 |
82 | And then execute:
83 |
84 | ```bash
85 | $ bundle
86 | ```
87 |
88 | Or install it yourself as:
89 |
90 | ```bash
91 | $ gem install mastercard-client-encryption
92 | ```
93 |
94 | Import the library:
95 |
96 | ```ruby
97 | require 'mcapi/encryption/openapi_interceptor' # to add the interceptor
98 | # or
99 | require 'mcapi/encryption/field_level_encryption' # to perform ad-hoc Mastercard encryption/decryption
100 | require 'mcapi/encryption/jwe_encryption' # to perform ad-hoc JWE encryption/decryption
101 | ```
102 |
103 |
104 |
105 | ### Performing Payload Encryption and Decryption
106 |
107 | - [Introduction](#introduction)
108 | - [JWE Encryption and Decryption](#jwe-encryption-and-decryption)
109 | - [Mastercard Encryption and Decryption](#mastercard-encryption-and-decryption)
110 |
111 | #### Introduction
112 |
113 | This library supports two types of encryption/decryption, both of which support field level and entire payload encryption: JWE encryption and what the library refers to as Field Level Encryption (Mastercard encryption), a scheme used by many services hosted on Mastercard Developers before the library added support for JWE.
114 |
115 | #### JWE Encryption and Decryption
116 |
117 | + [Introduction](#jwe-introduction)
118 | + [Configuring the JWE Encryption](#configuring-the-jwe-encryption)
119 | + [Performing JWE Encryption](#performing-jwe-encryption)
120 | + [Performing JWE Decryption](#performing-jwe-decryption)
121 |
122 | ##### • Introduction
123 |
124 | This library uses [JWE compact serialization](https://datatracker.ietf.org/doc/html/rfc7516#section-7.1) for the encryption of sensitive data.
125 | The core methods responsible for payload encryption and decryption are `encrypt` and `decrypt` in the `JweEncryption` class.
126 |
127 | - `encrypt()` usage:
128 |
129 | ```ruby
130 | jwe = McAPI::Encryption::JweEncryption.new(@config)
131 | encrypted_request_payload = jwe.encrypt(endpoint, body)
132 | ```
133 |
134 | - `decrypt()` usage:
135 |
136 | ```ruby
137 | jwe = McAPI::Encryption::JweEncryption.new(@config)
138 | decrypted_response_payload = jwe.decrypt(encrypted_response_payload)
139 | ```
140 |
141 | #### Configuring the JWE Encryption
142 |
143 | `JweEncryption` needs a config object to instruct how to decrypt/decrypt the payloads. Example:
144 |
145 | ```json
146 | {
147 | "paths": [
148 | {
149 | "path": "/resource",
150 | "toEncrypt": [
151 | {
152 | "element": "path.to.foo",
153 | "obj": "path.to.encryptedFoo"
154 | }
155 | ],
156 | "toDecrypt": [
157 | {
158 | "element": "path.to.encryptedFoo",
159 | "obj": "path.to.foo"
160 | }
161 | ]
162 | }
163 | ],
164 | "encryptedValueFieldName": "encryptedData",
165 | "encryptionCertificate": "./path/to/public.cert",
166 | "privateKey": "./path/to/your/private.key",
167 | }
168 | ```
169 |
170 | For all config options, please see:
171 |
172 | - [Configuration object](https://github.com/Mastercard/client-encryption-ruby/wiki/Configuration-Object) for all config options
173 |
174 | We have a predefined set of configurations to use with Mastercard services:
175 |
176 | - [Service configurations](https://github.com/Mastercard/client-encryption-ruby/wiki/Service-Configurations-for-Client-Encryption-Ruby)
177 |
178 |
179 | #### Performing JWE Encryption
180 |
181 | Call `JweEncryption.encrypt()` with a JSON request payload.
182 |
183 | Example using the configuration [above](#configuring-the-jwe-encryption):
184 |
185 | ```ruby
186 | payload = JSON.generate({
187 | path: {
188 | to: {
189 | foo: {
190 | sensitiveField1: 'sensitiveValue1',
191 | sensitiveField2: 'sensitiveValue2'
192 | }
193 | }
194 | }
195 | })
196 | jwe = McAPI::Encryption::JweEncryption.new(@config)
197 | request_payload = jwe.encrypt("/resource", header, payload)
198 | ```
199 |
200 | Output:
201 |
202 | ```json
203 | {
204 | "path": {
205 | "to": {
206 | "encryptedFoo": {
207 | "encryptedValue": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM….Y+oPYKZEMTKyYcSIVEgtQw"
208 | }
209 | }
210 | }
211 | }
212 | ```
213 |
214 | #### Performing JWE Decryption
215 |
216 | Call `JweEncryption.decrypt()` with an (encrypted) `response` object with the following fields:
217 |
218 | - `body`: json payload
219 | - `request.url`: requesting url
220 |
221 | Example using the configuration [above](#configuring-the-jwe-encryption):
222 |
223 | ```ruby
224 | response = {}
225 | response[:request] = { url: '/resource1' }
226 | response[:body] =
227 | {
228 | "path": {
229 | "to": {
230 | "encryptedFoo": {
231 | "encryptedValue": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM….Y+oPYKZEMTKyYcSIVEgtQw"
232 | }
233 | }
234 | }
235 | }
236 | jwe = McAPI::Encryption::JweEncryption.new(@config)
237 | response_payload = jwe.decrypt(response)
238 | ```
239 |
240 | Output:
241 |
242 | ```json
243 | {
244 | "path": {
245 | "to": {
246 | "foo": {
247 | "sensitiveField1": "sensitiveValue1",
248 | "sensitiveField2": "sensitiveValue2"
249 | }
250 | }
251 | }
252 | }
253 | ```
254 |
255 | #### Mastercard Encryption and Decryption
256 |
257 | - [Introduction](#mastercard-introduction)
258 | - [Configuring the Mastercard Encryption](#configuring-the-mastercard-encryption)
259 | - [Performing Mastercard Encryption](#performing-mastercard-encryption)
260 | - [Performing Mastercard Decryption](#performing-mastercard-decryption)
261 |
262 | #### Introduction
263 |
264 | The core methods responsible for payload encryption and decryption are `encrypt` and `decrypt` in the `FieldLevelEncryption` class.
265 |
266 | - `encrypt()` usage:
267 |
268 | ```ruby
269 | fle = McAPI::Encryption::FieldLevelEncryption.new(@config)
270 | encrypted_request_payload = fle.encrypt(endpoint, header, body)
271 | ```
272 |
273 | - `decrypt()` usage:
274 |
275 | ```ruby
276 | fle = McAPI::Encryption::FieldLevelEncryption.new(@config)
277 | decrypted_response_payload = fle.decrypt(encrypted_response_payload)
278 | ```
279 |
280 | #### Configuring the Mastercard Encryption
281 |
282 | `FieldLevelEncryption` needs a config object to instruct how to decrypt/decrypt the payloads. Example:
283 |
284 | ```json
285 | {
286 | "paths": [
287 | {
288 | "path": "/resource",
289 | "toEncrypt": [
290 | {
291 | "element": "path.to.foo",
292 | "obj": "path.to.encryptedFoo"
293 | }
294 | ],
295 | "toDecrypt": [
296 | {
297 | "element": "path.to.encryptedFoo",
298 | "obj": "path.to.foo"
299 | }
300 | ]
301 | }
302 | ],
303 | "ivFieldName": "iv",
304 | "encryptedKeyFieldName": "encryptedKey",
305 | "encryptedValueFieldName": "encryptedData",
306 | "dataEncoding": "hex",
307 | "encryptionCertificate": "./path/to/public.cert",
308 | "privateKey": "./path/to/your/private.key",
309 | "oaepPaddingDigestAlgorithm": "SHA-256"
310 | }
311 | ```
312 |
313 | For all config options, please see:
314 |
315 | - [Configuration object](https://github.com/Mastercard/client-encryption-ruby/wiki/Configuration-Object) for all config options
316 |
317 | We have a predefined set of configurations to use with Mastercard services:
318 |
319 | - [Service configurations](https://github.com/Mastercard/client-encryption-ruby/wiki/Service-Configurations-for-Client-Encryption-Ruby)
320 |
321 |
322 |
323 | #### Performing Mastercard Encryption
324 |
325 | Call `FieldLevelEncryption.encrypt()` with a JSON request payload, and optional `header` object.
326 |
327 | Example using the configuration [above](#configuring-the-mastercard-encryption):
328 |
329 | ```ruby
330 | payload = JSON.generate({
331 | path: {
332 | to: {
333 | foo: {
334 | sensitiveField1: 'sensitiveValue1',
335 | sensitiveField2: 'sensitiveValue2'
336 | }
337 | }
338 | }
339 | })
340 | fle = McAPI::Encryption::FieldLevelEncryption.new(@config)
341 | request_payload = fle.encrypt("/resource", header, payload)
342 | ```
343 |
344 | Output:
345 |
346 | ```json
347 | {
348 | "path": {
349 | "to": {
350 | "encryptedFoo": {
351 | "iv": "7f1105fb0c684864a189fb3709ce3d28",
352 | "encryptedKey": "67f467d1b653d98411a0c6d3c…ffd4c09dd42f713a51bff2b48f937c8",
353 | "encryptedData": "b73aabd267517fc09ed72455c2…dffb5fa04bf6e6ce9ade1ff514ed6141",
354 | "publicKeyFingerprint": "80810fc13a8319fcf0e2e…82cc3ce671176343cfe8160c2279",
355 | "oaepHashingAlgorithm": "SHA256"
356 | }
357 | }
358 | }
359 | }
360 | ```
361 |
362 | #### Performing Mastercard Decryption
363 |
364 | Call `FieldLevelEncryption.decrypt()` with an (encrypted) `response` object with the following fields:
365 |
366 | - `body`: json payload
367 | - `request.url`: requesting url
368 | - `header`: *optional*, header object
369 |
370 | Example using the configuration [above](#configuring-the-mastercard-encryption):
371 |
372 | ```ruby
373 | response = {}
374 | response[:request] = { url: '/resource1' }
375 | response[:body] =
376 | {
377 | path: {
378 | to: {
379 | encryptedFoo: {
380 | iv: 'e5d313c056c411170bf07ac82ede78c9',
381 | encryptedKey: 'e3a56746c0f9109d18b3a2652b76…f16d8afeff36b2479652f5c24ae7bd',
382 | encryptedData: '809a09d78257af5379df0c454dcdf…353ed59fe72fd4a7735c69da4080e74f',
383 | oaepHashingAlgorithm: 'SHA256',
384 | publicKeyFingerprint: '80810fc13a8319fcf0e2e…3ce671176343cfe8160c2279'
385 | }
386 | }
387 | }
388 | }
389 | fle = McAPI::Encryption::FieldLevelEncryption.new(@config)
390 | response_payload = fle.decrypt(response)
391 | ```
392 |
393 | Output:
394 |
395 | ```json
396 | {
397 | "path": {
398 | "to": {
399 | "foo": {
400 | "sensitiveField1": "sensitiveValue1",
401 | "sensitiveField2": "sensitiveValue2"
402 | }
403 | }
404 | }
405 | }
406 | ```
407 |
408 | ### Integrating with OpenAPI Generator API Client Libraries
409 |
410 | [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) generates API client libraries from [OpenAPI Specs](https://github.com/OAI/OpenAPI-Specification).
411 | It provides generators and library templates for supporting multiple languages and frameworks.
412 |
413 | The **client-encryption-ruby** library provides a method you can use to integrate the OpenAPI generated client with this library:
414 | ```ruby
415 | # JWE Encryption
416 | McAPI::Encryption::OpenAPIInterceptor.install_jwe_encryption(open_api_client, config)
417 |
418 | # Mastercard Encryption
419 | McAPI::Encryption::OpenAPIInterceptor.install_field_level_encryption(open_api_client, config)
420 | ```
421 | The above methods will handle the encryption in the generated OpenApi client, taking 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.
422 |
423 | ##### OpenAPI Generator
424 |
425 | OpenAPI client can be generated, starting from your OpenAPI Spec / Swagger using the following command:
426 |
427 | ```shell
428 | openapi-generator-cli generate -i openapi-spec.yaml -l ruby -o out
429 | ```
430 |
431 | Client library will be generated in the `out` folder.
432 |
433 | See also:
434 |
435 | - [OpenAPI Generator CLI Installation](https://openapi-generator.tech/docs/installation/)
436 |
437 | ##### Usage of the `McAPI::Encryption::OpenAPIInterceptor.install_field_level_encryption`:
438 |
439 | To use it:
440 |
441 | 1. Generate the OpenAPI client, as [above](#openapi-generator)
442 |
443 | 2. Import the **mastercard-client-encryption** OpenAPI Interceptor and the generated OpenApi client
444 |
445 | ```ruby
446 | require 'mcapi/encryption/openapi_interceptor'
447 | require_relative './out/generated_open_apiclient' #import generated OpenAPI client
448 | ```
449 |
450 | 3. Install the Mastercard/JWE encryption in the generated client:
451 |
452 | ```ruby
453 | # Read the service configuration obj
454 | @config = File.read('./config.json')
455 | # Create a new instance of the generated client
456 | @api_client = client::ApiClient.new
457 |
458 | # Use 1 of the below 2 methods (depending on the encryption type) to enable encryption
459 | # Enable Mastercard encryption
460 | McAPI::Encryption::OpenAPIInterceptor.install_field_level_encryption(@api_client, @config)
461 |
462 | # Enable JWE encryption
463 | McAPI::Encryption::OpenAPIInterceptor.install_jwe_encryption(@api_client, @config)
464 | ```
465 |
466 | 4. Use the `api_client` object with the Field Level Encryption enabled:
467 |
468 | Example:
469 |
470 | ```ruby
471 | api_instance = OpenApiService::ServiceApi.new @api_client
472 | body = # … #
473 | response = api_instance.create_merchants(body)
474 | # requests and responses will be automatically encrypted and decrypted
475 | # accordingly with the configuration object used
476 |
477 | # … use the (decrypted) response object here …
478 | ```
479 |
480 |
--------------------------------------------------------------------------------