├── 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/_/_/src/global/assets/svg/mcdev-logo-dark.svg)](https://developer.mastercard.com/) 3 | 4 | [![maintenance-status](https://img.shields.io/badge/maintenance-deprecated-red.svg)](https://github.com/Mastercard/.github/blob/main/CLIENT_LIBRARY_DEPRECATION_POLICY.md) 5 | [![](https://github.com/Mastercard/client-encryption-ruby/workflows/Build%20&%20Test/badge.svg)](https://github.com/Mastercard/client-encryption-ruby/actions?query=workflow%3A%22Build+%26+Test%22) 6 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_client-encryption-ruby&metric=alert_status)](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-ruby) 7 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_client-encryption-ruby&metric=coverage)](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-ruby) 8 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_client-encryption-ruby&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-ruby) 9 | [![](https://github.com/Mastercard/client-encryption-ruby/workflows/broken%20links%3F/badge.svg)](https://github.com/Mastercard/client-encryption-ruby/actions?query=workflow%3A%22broken+links%3F%22) 10 | [![](https://img.shields.io/gem/v/mastercard-client-encryption.svg)](https://rubygems.org/gems/mastercard-client-encryption) 11 | [![](https://img.shields.io/badge/license-MIT-yellow.svg)](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 | --------------------------------------------------------------------------------