├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── broken-links.yml │ ├── sonar.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── client-encryption-python.pyproj ├── client-encryption-python.sln ├── client_encryption ├── __init__.py ├── api_encryption.py ├── encoding_utils.py ├── encryption_exception.py ├── encryption_utils.py ├── field_level_encryption.py ├── field_level_encryption_config.py ├── json_path_utils.py ├── jwe_encryption.py ├── jwe_encryption_config.py ├── session_key_params.py └── version.py ├── requirements.txt ├── setup.py ├── sonar-project.properties └── tests ├── __init__.py ├── resources ├── certificates │ ├── test_certificate-2048.der │ └── test_certificate-2048.pem ├── jwe_test_config.json ├── keys │ ├── test_invalid_key.der │ ├── test_key.p12 │ ├── test_key_pkcs1-1024.pem │ ├── test_key_pkcs1-2048.pem │ ├── test_key_pkcs1-4096.pem │ ├── test_key_pkcs1-512.pem │ ├── test_key_pkcs8-2048.der │ └── test_key_pkcs8-2048.pem └── mastercard_test_config.json ├── test_api_encryption.py ├── test_api_encryption_jwe.py ├── test_encoding_utils.py ├── test_encryption_utils.py ├── test_field_level_encryption.py ├── test_field_level_encryption_config.py ├── test_json_path_utils.py ├── test_jwe_encryption.py ├── test_jwe_encryption_config.py ├── test_session_key_params.py └── utils └── api_encryption_test_utils.py /.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. -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ### PR checklist 3 | 4 | - [ ] An issue/feature request has been created for this PR 5 | - [ ] Pull Request title clearly describes the work in the pull request and the Pull Request description provides details about how to validate the work. Missing information here may result in a delayed response. 6 | - [ ] File the PR against the `master` branch 7 | - [ ] The code in this PR is covered by unit tests 8 | 9 | #### Link to issue/feature request: *add the link here* 10 | 11 | #### Description 12 | A clear and concise description of what is this PR for and any additional info might be useful for reviewing it. 13 | -------------------------------------------------------------------------------- /.github/workflows/broken-links.yml: -------------------------------------------------------------------------------- 1 | name: broken links? 2 | 'on': 3 | push: 4 | branches: 5 | - "**" 6 | schedule: 7 | - cron: 0 16 * * * 8 | workflow_dispatch: 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 | -------------------------------------------------------------------------------- /.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 | name: Sonar 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | - name: Check for external PR 22 | if: ${{ !(contains(github.event.pull_request.labels.*.name, 'safe') || 23 | github.event.pull_request.head.repo.full_name == github.repository || 24 | github.event_name != 'pull_request_target') }} 25 | run: echo "Unsecure PR, must be labelled with the 'safe' label, then run the workflow again" && exit 1 26 | - name: Set up Python 3.8 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: 3.8 30 | - name: Install dependencies 31 | run: | 32 | pip3 install -r requirements.txt 33 | pip3 install . 34 | pip3 install coverage 35 | - name: Run Tests 36 | run: | 37 | coverage run setup.py test 38 | coverage xml 39 | - name: SonarCloud Scan 40 | uses: SonarSource/sonarcloud-github-action@master 41 | env: 42 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 43 | SONAR_TOKEN: '${{ secrets.SONAR_TOKEN }}' 44 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 'on': 3 | push: 4 | branches: 5 | - "**" 6 | pull_request: 7 | branches: 8 | - "**" 9 | schedule: 10 | - cron: 0 16 * * * 11 | workflow_dispatch: 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | python-version: 18 | - 3.8 19 | - 3.9 20 | include: 21 | - os: "ubuntu-latest" 22 | steps: 23 | - uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 0 26 | - name: 'Set up Python ${{ matrix.python-version }}' 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: '${{ matrix.python-version }}' 30 | - name: Install dependencies 31 | run: | 32 | pip3 install -r requirements.txt 33 | pip3 install . 34 | pip3 install coverage 35 | - name: Run Tests 36 | run: | 37 | python -m unittest discover 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | coverage-reports/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | tests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sonar 68 | .scannerwork/ 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | 110 | *.iml 111 | .idea 112 | 113 | # Visual Studio 114 | .vs 115 | .vscode 116 | .vscode/* -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # client-encryption-python 2 | [![](https://developer.mastercard.com/_/_/src/global/assets/svg/mcdev-logo-dark.svg)](https://developer.mastercard.com/) 3 | 4 | [![](https://github.com/Mastercard/client-encryption-python/workflows/Build%20&%20Test/badge.svg)](https://github.com/Mastercard/client-encryption-python/actions?query=workflow%3A%22Build+%26+Test%22) 5 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_client-encryption-python&metric=alert_status)](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-python) 6 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_client-encryption-python&metric=coverage)](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-python) 7 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_client-encryption-python&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-python) 8 | [![](https://github.com/Mastercard/client-encryption-python/workflows/broken%20links%3F/badge.svg)](https://github.com/Mastercard/client-encryption-python/actions?query=workflow%3A%22broken+links%3F%22) 9 | [![](https://img.shields.io/pypi/v/mastercard-client-encryption.svg?style=flat&color=blue)](https://pypi.org/project/mastercard-client-encryption) 10 | [![](https://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/Mastercard/client-encryption-python/blob/master/LICENSE) 11 | 12 | ## Table of Contents 13 | - [Overview](#overview) 14 | * [Compatibility](#compatibility) 15 | * [References](#references) 16 | * [Versioning and Deprecation Policy](#versioning) 17 | - [Usage](#usage) 18 | * [Prerequisites](#prerequisites) 19 | * [Adding the Library to Your Project](#adding-the-library-to-your-project) 20 | * [Performing Payload Encryption and Decryption](#performing-payload-encryption-and-decryption) 21 | * [JWE Encryption and Decryption](#jwe-encryption-and-decryption) 22 | * [Mastercard Encryption and Decryption](#mastercard-encryption-and-decryption) 23 | * [Integrating with OpenAPI Generator API Client Libraries](#integrating-with-openapi-generator-api-client-libraries) 24 | 25 | 26 | ## Overview 27 | This is the Python version of the Mastercard compliant payload encryption/decryption. 28 | 29 | ### Compatibility 30 | Python 3.8+ 31 | 32 | ### References 33 | * [JSON Web Encryption (JWE)](https://datatracker.ietf.org/doc/html/rfc7516) 34 | * [Securing Sensitive Data Using Payload Encryption](https://developer.mastercard.com/platform/documentation/security-and-authentication/securing-sensitive-data-using-payload-encryption/) 35 | 36 | ### Versioning and Deprecation Policy 37 | * [Mastercard Versioning and Deprecation Policy](https://github.com/Mastercard/.github/blob/main/CLIENT_LIBRARY_DEPRECATION_POLICY.md) 38 | 39 | ## Usage 40 | ### Prerequisites 41 | Before using this library, you will need to set up a project in the [Mastercard Developers Portal](https://developer.mastercard.com). 42 | 43 | As part of this set up, you'll receive: 44 | - A public request encryption certificate (aka _Client Encryption Keys_) 45 | - A private response decryption key (aka _Mastercard Encryption Keys_) 46 | 47 | ### Installation 48 | If you want to use **mastercard-client-encryption** with [Python](https://www.python.org/), it is available through `PyPI`: 49 | - [https://pypi.org/project/mastercard-client-encryption](https://pypi.org/project/mastercard-client-encryption) 50 | 51 | **Adding the library to your project** 52 | Install the library by pip: 53 | 54 | ```bash 55 | pip install mastercard-client-encryption 56 | ``` 57 | 58 | Or clone it from git: 59 | 60 | ```bash 61 | git clone https://github.com/Mastercard/client-encryption-python.git 62 | ``` 63 | 64 | and then execute from the repo folder: 65 | 66 | ```bash 67 | python3 setup.py install 68 | ``` 69 | 70 | You can then use it as a regular module: 71 | 72 | ```python 73 | # Mastercard Encryption/Decryption 74 | from client_encryption.field_level_encryption_config import FieldLevelEncryptionConfig 75 | from client_encryption.field_level_encryption import encrypt_payload, decrypt_payload 76 | ``` 77 | 78 | ```python 79 | # JWE Encryption/Decryption 80 | from client_encryption.jwe_encryption_config import JweEncryptionConfig 81 | from client_encryption.jwe_encryption import encrypt_payload, decrypt_payload 82 | ``` 83 | 84 | ### Performing Payload Encryption and Decryption 85 | 86 | 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. 87 | 88 | + [JWE Encryption and Decryption](#jwe-encryption-and-decryption) 89 | + [Mastercard Encryption and Decryption](#mastercard-encryption-and-decryption) 90 | 91 | #### JWE Encryption and Decryption 92 | 93 | + [Introduction](#jwe-introduction) 94 | + [Configuring the JWE Encryption](#configuring-the-jwe-encryption) 95 | + [Performing JWE Encryption](#performing-jwe-encryption) 96 | + [Performing JWE Decryption](#performing-jwe-decryption) 97 | 98 | ##### Introduction 99 | 100 | This library uses [JWE compact serialization](https://datatracker.ietf.org/doc/html/rfc7516#section-7.1) for the encryption of sensitive data. 101 | The core methods responsible for payload encryption and decryption are `encrypt_payload` and `decrypt_payload` in the `jwe_encryption` module. 102 | 103 | - `encrypt_payload()` usage: 104 | 105 | ```python 106 | config = JweEncryptionConfig(config_dictionary) 107 | encrypted_request_payload = encrypt_payload(body, config) 108 | ``` 109 | 110 | - `decrypt_payload()` usage: 111 | 112 | ```python 113 | config = JweEncryptionConfig(config_dictionary) 114 | decrypted_response_payload = decrypt_payload(body, config) 115 | ``` 116 | 117 | ##### Configuring the JWE Encryption 118 | 119 | `jwe_encryption` needs a config dictionary to instruct how to decrypt/decrypt the payloads. Example: 120 | 121 | ```json 122 | { 123 | "paths": { 124 | "$": { 125 | "toEncrypt": { 126 | "path.to.foo": "path.to.encryptedFoo" 127 | }, 128 | "toDecrypt": { 129 | "path.to.encryptedFoo": "path.to.foo" 130 | } 131 | } 132 | }, 133 | "encryptedValueFieldName": "encryptedData", 134 | "encryptionCertificate": "./path/to/public.cert", 135 | "decryptionKey": "./path/to/your/private.key", 136 | } 137 | ``` 138 | 139 | The above can be either stored to a file or passed to 'JweEncryptionConfig' as dictionary: 140 | ```python 141 | config_dictionary = { 142 | "paths": {…}, 143 | … 144 | "decryptionKey": "./path/to/your/private.key" 145 | } 146 | 147 | config = JweEncryptionConfig(config_dictionary) 148 | 149 | config_file_path = "./config.json" 150 | config = JweEncryptionConfig(config_file_path) 151 | ``` 152 | 153 | ##### Performing JWE Encryption 154 | 155 | Call `jwe_encryption.encrypt_payload()` with a JSON (dict) request payload, and optional `params` object. 156 | 157 | Example using the configuration [above](#configuring-the-jwe-encryption): 158 | 159 | ```python 160 | from client_encryption.session_key_params import SessionKeyParams 161 | 162 | payload = { 163 | "path": { 164 | "to": { 165 | "foo": { 166 | "sensitiveField1": "sensitiveValue1", 167 | "sensitiveField2": "sensitiveValue2" 168 | } 169 | } 170 | } 171 | } 172 | 173 | params = SessionKeyParams.generate(conf) # optional 174 | request_payload = encrypt_payload(payload, config, params) 175 | ``` 176 | 177 | Output: 178 | 179 | ```json 180 | { 181 | "path": { 182 | "to": { 183 | "encryptedFoo": { 184 | "encryptedValue": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM(...)==.Y+oPYKZEMTKyYcSIVEgtQw==" 185 | } 186 | } 187 | } 188 | } 189 | ``` 190 | 191 | ##### Performing JWE Decryption 192 | 193 | Call `jwe_encryption.decrypt_payload()` with a JSON (dict) encrypted response payload. 194 | 195 | Example using the configuration [above](#configuring-the-jwe-encryption): 196 | 197 | ```python 198 | response = { 199 | "path": { 200 | "to": { 201 | "encryptedFoo": { 202 | "encryptedValue": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM(...)==.Y+oPYKZEMTKyYcSIVEgtQw==" 203 | } 204 | } 205 | } 206 | } 207 | 208 | response_payload = decrypt_payload(response, config) 209 | 210 | ``` 211 | 212 | Output: 213 | 214 | ```json 215 | { 216 | "path": { 217 | "to": { 218 | "foo": { 219 | "sensitiveField1": "sensitiveValue1", 220 | "sensitiveField2": "sensitiveValue2" 221 | } 222 | } 223 | } 224 | } 225 | ``` 226 | 227 | #### Mastercard Encryption and Decryption 228 | 229 | + [Introduction](#mastercard-introduction) 230 | + [Configuring the Mastercard Encryption](#configuring-the-mastercard-encryption) 231 | + [Performing Mastercard Encryption](#performing-mastercard-encryption) 232 | + [Performing Mastercard Decryption](#performing-mastercard-decryption) 233 | 234 | ##### Introduction 235 | 236 | The core methods responsible for payload encryption and decryption are `encrypt_payload` and `decrypt_payload` in the `field_level_encryption` module. 237 | 238 | - `encrypt_payload()` usage: 239 | 240 | ```python 241 | config = FieldLevelEncryptionConfig(config_dictionary) 242 | encrypted_request_payload = encrypt_payload(body, config) 243 | ``` 244 | 245 | - `decrypt_payload()` usage: 246 | 247 | ```python 248 | config = FieldLevelEncryptionConfig(config_dictionary) 249 | decrypted_response_payload = decrypt_payload(body, config) 250 | ``` 251 | 252 | ##### Configuring the Mastercard Encryption 253 | 254 | `field_level_encryption` needs a config dictionary to instruct how to decrypt/decrypt the payloads. Example: 255 | 256 | ```json 257 | { 258 | "paths": { 259 | "$": { 260 | "toEncrypt": { 261 | "path.to.foo": "path.to.encryptedFoo" 262 | }, 263 | "toDecrypt": { 264 | "path.to.encryptedFoo": "path.to.foo" 265 | } 266 | } 267 | }, 268 | "ivFieldName": "iv", 269 | "encryptedKeyFieldName": "encryptedKey", 270 | "encryptedValueFieldName": "encryptedData", 271 | "dataEncoding": "hex", 272 | "encryptionCertificate": "./path/to/public.cert", 273 | "decryptionKey": "./path/to/your/private.key", 274 | "oaepPaddingDigestAlgorithm": "SHA256" 275 | } 276 | ``` 277 | 278 | The above can be either stored to a file or passed to 'FieldLevelEncryptionConfig' as dictionary: 279 | ```python 280 | config_dictionary = { 281 | "paths": {…}, 282 | … 283 | "decryptionKey": "./path/to/your/private.key", 284 | "oaepPaddingDigestAlgorithm": "SHA256" 285 | } 286 | 287 | config = FieldLevelEncryptionConfig(config_dictionary) 288 | 289 | config_file_path = "./config.json" 290 | config = FieldLevelEncryptionConfig(config_file_path) 291 | ``` 292 | 293 | ##### Performing Mastercard Encryption 294 | 295 | Call `field_level_encryption.encrypt_payload()` with a JSON (dict) request payload, and optional `params` object. 296 | 297 | Example using the configuration [above](#configuring-the-field-level-encryption): 298 | 299 | ```python 300 | from client_encryption.session_key_params import SessionKeyParams 301 | 302 | payload = { 303 | "path": { 304 | "to": { 305 | "foo": { 306 | "sensitiveField1": "sensitiveValue1", 307 | "sensitiveField2": "sensitiveValue2" 308 | } 309 | } 310 | } 311 | } 312 | 313 | params = SessionKeyParams.generate(conf) # optional 314 | request_payload = encrypt_payload(payload, config, params) 315 | ``` 316 | 317 | Output: 318 | 319 | ```json 320 | { 321 | "path": { 322 | "to": { 323 | "encryptedFoo": { 324 | "iv": "7f1105fb0c684864a189fb3709ce3d28", 325 | "encryptedKey": "67f467d1b653d98411a0c6d3c…ffd4c09dd42f713a51bff2b48f937c8", 326 | "encryptedData": "b73aabd267517fc09ed72455c2…dffb5fa04bf6e6ce9ade1ff514ed6141", 327 | "publicKeyFingerprint": "80810fc13a8319fcf0e2e…82cc3ce671176343cfe8160c2279", 328 | "oaepHashingAlgorithm": "SHA256" 329 | } 330 | } 331 | } 332 | } 333 | ``` 334 | 335 | ##### Performing Mastercard Decryption 336 | 337 | Call `field_level_encryption.decrypt_payload()` with a JSON (dict) encrypted response payload. 338 | 339 | Example using the configuration [above](#configuring-the-field-level-encryption): 340 | 341 | ```python 342 | response = { 343 | "path": { 344 | "to": { 345 | "encryptedFoo": { 346 | "iv": "e5d313c056c411170bf07ac82ede78c9", 347 | "encryptedKey": "e3a56746c0f9109d18b3a2652b76…f16d8afeff36b2479652f5c24ae7bd", 348 | "encryptedData": "809a09d78257af5379df0c454dcdf…353ed59fe72fd4a7735c69da4080e74f", 349 | "oaepHashingAlgorithm": "SHA256", 350 | "publicKeyFingerprint": "80810fc13a8319fcf0e2e…3ce671176343cfe8160c2279" 351 | } 352 | } 353 | } 354 | } 355 | 356 | response_payload = decrypt_payload(response, config) 357 | 358 | ``` 359 | 360 | Output: 361 | 362 | ```json 363 | { 364 | "path": { 365 | "to": { 366 | "foo": { 367 | "sensitiveField1": "sensitiveValue1", 368 | "sensitiveField2": "sensitiveValue2" 369 | } 370 | } 371 | } 372 | } 373 | ``` 374 | 375 | ### Integrating with OpenAPI Generator API Client Libraries 376 | 377 | [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) generates API client libraries from [OpenAPI Specs](https://github.com/OAI/OpenAPI-Specification). 378 | It provides generators and library templates for supporting multiple languages and frameworks. 379 | 380 | The **client-encryption-python** library provides a method you can use to integrate the OpenAPI generated client with this library: 381 | ```python 382 | from client_encryption.api_encryption import add_encryption_layer 383 | 384 | config = { 385 | "paths": { 386 | "$": { 387 | … 388 | } 389 | }, 390 | "encryptionCertificate": "path/to/cert.pem", 391 | … 392 | "decryptionKey": "path/to/to/key.pem" 393 | } 394 | 395 | add_encryption_layer(api_client, config) 396 | ``` 397 | 398 | Alternatively you can pass the configuration by a json file: 399 | ```python 400 | from client_encryption.api_encryption import add_encryption_layer 401 | 402 | add_encryption_layer(api_client, "path/to/my/config.json") 403 | ``` 404 | 405 | This method will add the Mastercard/JWE 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_payload()`/`decrypt_payload()` functions for each API request or response. 406 | 407 | ##### OpenAPI Generator 408 | 409 | OpenAPI client can be generated, starting from your OpenAPI Spec using the following command: 410 | 411 | ```shell 412 | openapi-generator-cli generate -i openapi-spec.yaml -l python -o out 413 | ``` 414 | 415 | The client library will be generated in the `out` folder. 416 | 417 | See also: 418 | 419 | - [OpenAPI Generator CLI Installation](https://openapi-generator.tech/docs/installation/) 420 | 421 | ##### Usage of the `api_encryption.add_encryption_layer`: 422 | 423 | To use it: 424 | 425 | 1. Generate the [OpenAPI client](#openapi-generator) 426 | 427 | 2. Import the **mastercard-client-encryption** module and the generated OpenAPI client 428 | 429 | ```python 430 | from client_encryption.api_encryption import add_encryption_layer 431 | from openapi_client.api_client import ApiClient # import generated OpenAPI client 432 | ``` 433 | 434 | 3. Add the encryption layer to the generated client: 435 | 436 | ```python 437 | # Create a new instance of the generated client 438 | api_client = ApiClient() 439 | # Enable encryption 440 | add_encryption_layer(api_client, "path/to/my/config.json") 441 | ``` 442 | 443 | 4. Use the `ApiClient` instance with Encryption enabled: 444 | 445 | Example: 446 | 447 | ```python 448 | request_body = {…} 449 | response = MyServiceApi(api_client).do_some_action_post(body=request_body) 450 | # requests and responses will be automatically encrypted and decrypted 451 | # accordingly with the configuration object used 452 | 453 | # … use the (decrypted) response object here … 454 | decrypted = response.json() 455 | 456 | ``` 457 | 458 | ##### Integrating with `mastercard-client-encryption` module: 459 | 460 | In order to use both signing and encryption layers, a defined order is required as signing library should calculate the hash of the encrypted payload. 461 | According to the above the signing layer must be applied first in order to work as inner layer. The outer layer - encryption - will be executed first, providing the signing layer the encrypted payload to sign. 462 | 463 | 1. Generate the [OpenAPI client](#openapi-generator) 464 | 465 | 2. Import both **mastercard-client-encryption** and **mastercard-client-encryption** modules and the generated OpenAPI client 466 | 467 | ```python 468 | from oauth1.signer_interceptor import add_signing_layer 469 | from client_encryption.api_encryption import add_encryption_layer 470 | from openapi_client.api_client import ApiClient # import generated OpenAPI client 471 | ``` 472 | 473 | 3. Add the authentication layer to the generated client: 474 | ```python 475 | # Create a new instance of the generated client 476 | api_client = ApiClient() 477 | 478 | # Enable authentication 479 | add_signing_layer(api_client, key_file, key_password, consumer_key) 480 | ``` 481 | 482 | 4. Then add the encryption layer: 483 | ```python 484 | add_encryption_layer(api_client, "path/to/my/config.json") 485 | ``` 486 | 487 | 5. Use the `ApiClient` instance with Authentication and Encryption both enabled: 488 | ```python 489 | response = MyServiceApi(api_client).do_some_action_post(body=request_body) 490 | decrypted = response.json() 491 | ``` 492 | -------------------------------------------------------------------------------- /client-encryption-python.pyproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | 2.0 6 | {43298c1a-86ab-4d9d-87fa-f969332c5ef8} 7 | 8 | setup.py 9 | 10 | . 11 | . 12 | {888888a0-9f3d-457c-b088-3a5042f75d52} 13 | Standard Python launcher 14 | Global|PythonCore|3.7-32 15 | 16 | 17 | 18 | 19 | 10.0 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /client-encryption-python.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29102.190 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "client-encryption-python", "client-encryption-python.pyproj", "{43298C1A-86AB-4D9D-87FA-F969332C5EF8}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {43298C1A-86AB-4D9D-87FA-F969332C5EF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {43298C1A-86AB-4D9D-87FA-F969332C5EF8}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | GlobalSection(ExtensibilityGlobals) = postSolution 21 | SolutionGuid = {EFFA9930-7863-48D0-A151-C279697FE663} 22 | EndGlobalSection 23 | EndGlobal 24 | -------------------------------------------------------------------------------- /client_encryption/__init__.py: -------------------------------------------------------------------------------- 1 | from client_encryption.version import __version__ 2 | -------------------------------------------------------------------------------- /client_encryption/api_encryption.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import json 3 | from enum import Enum 4 | from functools import wraps 5 | from warnings import warn 6 | 7 | from client_encryption.field_level_encryption import encrypt_payload as encrypt_field_level, \ 8 | decrypt_payload as decrypt_field_level 9 | from client_encryption.field_level_encryption_config import FieldLevelEncryptionConfig 10 | from client_encryption.jwe_encryption import encrypt_payload as encrypt_jwe, decrypt_payload as decrypt_jwe 11 | from client_encryption.jwe_encryption_config import JweEncryptionConfig 12 | from client_encryption.session_key_params import SessionKeyParams 13 | 14 | 15 | class ApiEncryption(object): 16 | 17 | def __init__(self, encryption_conf_file, encryption_type='Mastercard'): 18 | """Load and initialize FieldLevelEncryptionConfig object.""" 19 | 20 | if type(encryption_conf_file) is dict: 21 | if encryption_type == EncryptionType.MASTERCARD.value: 22 | self._encryption_conf = FieldLevelEncryptionConfig(encryption_conf_file) 23 | else: 24 | self._encryption_conf = JweEncryptionConfig(encryption_conf_file) 25 | else: 26 | if encryption_type == EncryptionType.MASTERCARD.value: 27 | with open(encryption_conf_file, encoding='utf-8') as json_file: 28 | self._encryption_conf = FieldLevelEncryptionConfig(json_file.read()) 29 | else: 30 | with open(encryption_conf_file, encoding='utf-8') as json_file: 31 | self._encryption_conf = JweEncryptionConfig(json_file.read()) 32 | 33 | def field_encryption_call_api(self, func): 34 | """Decorator for API call_api. func is APIClient.call_api""" 35 | 36 | @wraps(func) 37 | def call_api_function(*args, **kwargs): 38 | original_parameters = inspect.signature(func.__self__.call_api).parameters 39 | check_type_is_none = original_parameters.get("_check_type") is None 40 | preload_content_is_not_none = original_parameters.get("_preload_content") is not None 41 | if check_type_is_none and preload_content_is_not_none: 42 | kwargs["_preload_content"] = False # version 4.3.1 43 | return func(*args, **kwargs) 44 | 45 | call_api_function.__fle__ = True 46 | 47 | return call_api_function 48 | 49 | def field_encryption(self, func): 50 | """Decorator for API call_api. func is APIClient.call_api""" 51 | 52 | @wraps(func) 53 | def call_api_function(*args, **kwargs): 54 | """Wrap call_api and add field encryption layer to it.""" 55 | in_body = kwargs.get("body", None) 56 | 57 | in_headers = kwargs.get("headers", None) 58 | 59 | kwargs["body"] = self._encrypt_payload(in_headers, in_body) if in_body else in_body 60 | 61 | response = func(*args, **kwargs) 62 | 63 | response.data = self._decrypt_payload(response.getheaders(), response.response.data) 64 | 65 | return response 66 | 67 | call_api_function.__fle__ = True 68 | return call_api_function 69 | 70 | def _encrypt_payload(self, headers, body): 71 | """Encryption enforcement based on configuration - encrypt and add session key params to header or body""" 72 | 73 | conf = self._encryption_conf 74 | 75 | encrypted_payload = self.encrypt_field_level_payload(headers, conf, body) if type( 76 | conf) is FieldLevelEncryptionConfig else self.encrypt_jwe_payload(conf, body) 77 | 78 | # convert the encrypted_payload to the same data type as the input body 79 | if isinstance(body, str): 80 | return json.dumps(encrypted_payload) 81 | 82 | if isinstance(body, bytes): 83 | return json.dumps(encrypted_payload).encode("utf-8") 84 | 85 | return encrypted_payload 86 | 87 | def _decrypt_payload(self, headers, body): 88 | """Encryption enforcement based on configuration - decrypt using session key params from header or body""" 89 | 90 | conf = self._encryption_conf 91 | 92 | if type(conf) is FieldLevelEncryptionConfig: 93 | return self.decrypt_field_level_payload(headers, conf, body) 94 | else: 95 | return self.decrypt_jwe_payload(conf, body) 96 | 97 | @staticmethod 98 | def encrypt_jwe_payload(conf, body): 99 | return encrypt_jwe(body, conf) 100 | 101 | @staticmethod 102 | def decrypt_jwe_payload(conf, body): 103 | decrypted_body = decrypt_jwe(body, conf) 104 | try: 105 | payload = json.dumps(decrypted_body).encode('utf-8') 106 | except: 107 | payload = decrypted_body 108 | 109 | return payload 110 | 111 | @staticmethod 112 | def decrypt_field_level_payload(headers, conf, body): 113 | """Encryption enforcement based on configuration - decrypt using session key params from header or body""" 114 | params = None 115 | 116 | if conf.use_http_headers: 117 | if conf.iv_field_name in headers and conf.encrypted_key_field_name in headers: 118 | iv = headers.pop(conf.iv_field_name) 119 | encrypted_key = headers.pop(conf.encrypted_key_field_name) 120 | oaep_digest_algo = headers.pop(conf.oaep_padding_digest_algorithm_field_name) \ 121 | if _contains_param(conf.oaep_padding_digest_algorithm_field_name, headers) else None 122 | if _contains_param(conf.encryption_certificate_fingerprint_field_name, headers): 123 | del headers[conf.encryption_certificate_fingerprint_field_name] 124 | if _contains_param(conf.encryption_key_fingerprint_field_name, headers): 125 | del headers[conf.encryption_key_fingerprint_field_name] 126 | 127 | params = SessionKeyParams(conf, encrypted_key, iv, oaep_digest_algo) 128 | else: 129 | # skip decryption and return original body if not iv nor key is in headers 130 | return body 131 | 132 | decrypted_body = decrypt_field_level(body, conf, params) 133 | try: 134 | payload = json.dumps(decrypted_body).encode('utf-8') 135 | except: 136 | payload = decrypted_body 137 | 138 | return payload 139 | 140 | @staticmethod 141 | def encrypt_field_level_payload(headers, conf, body): 142 | if conf.use_http_headers: 143 | params = SessionKeyParams.generate(conf) 144 | 145 | encryption_params = { 146 | conf.iv_field_name: params.iv_value, 147 | conf.encrypted_key_field_name: params.encrypted_key_value 148 | } 149 | if conf.encryption_certificate_fingerprint_field_name: 150 | encryption_params[conf.encryption_certificate_fingerprint_field_name] = \ 151 | conf.encryption_certificate_fingerprint 152 | if conf.encryption_key_fingerprint_field_name: 153 | encryption_params[conf.encryption_key_fingerprint_field_name] = conf.encryption_key_fingerprint 154 | if conf.oaep_padding_digest_algorithm_field_name: 155 | encryption_params[conf.oaep_padding_digest_algorithm_field_name] = conf.oaep_padding_digest_algorithm 156 | 157 | encrypted_payload = encrypt_field_level(body, conf, params) 158 | headers.update(encryption_params) 159 | else: 160 | encrypted_payload = encrypt_field_level(body, conf) 161 | 162 | return encrypted_payload 163 | 164 | 165 | def _contains_param(param_name, headers): return param_name and param_name in headers 166 | 167 | 168 | def add_encryption_layer(api_client, encryption_conf_file, encryption_type='Mastercard'): 169 | """Decorate APIClient.call_api with encryption""" 170 | __check_oauth(api_client) # warn the user if authentication layer is missing/not set 171 | api_encryption = ApiEncryption(encryption_conf_file, encryption_type) 172 | api_client.rest_client.request = api_encryption.field_encryption(api_client.rest_client.request) 173 | api_client.call_api = api_encryption.field_encryption_call_api(api_client.call_api) 174 | 175 | 176 | def __check_oauth(api_client): 177 | try: 178 | api_client.rest_client.request.__wrapped__ 179 | except AttributeError: 180 | __oauth_warn() 181 | 182 | 183 | def __oauth_warn(): 184 | warn("No signing layer detected. Request will be only encrypted without being signed. " 185 | "Please refer to " 186 | "https://github.com/Mastercard/client-encryption-python#integrating-with-mastercard-oauth1-signer-module") 187 | 188 | 189 | class EncryptionType(Enum): 190 | MASTERCARD = 'Mastercard' 191 | JWE = 'JWE' 192 | -------------------------------------------------------------------------------- /client_encryption/encoding_utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from enum import Enum 3 | 4 | from client_encryption.encryption_exception import EncodingError 5 | 6 | 7 | def encode_bytes(_bytes, encoding): 8 | """Encode byte sequence to Hex or Base64.""" 9 | 10 | if type(_bytes) is bytes: 11 | if encoding == ClientEncoding.HEX: 12 | encoded = _bytes.hex() 13 | elif encoding == ClientEncoding.BASE64: 14 | encoded = base64.b64encode(_bytes).decode('utf-8') 15 | else: 16 | raise EncodingError("Encode: Invalid encoding.") 17 | 18 | return encoded 19 | else: 20 | raise ValueError("Encode: Invalid or missing input bytes.") 21 | 22 | 23 | def url_encode_bytes(_bytes): 24 | encoded = base64.urlsafe_b64encode(_bytes).decode().replace("=", "") 25 | return encoded 26 | 27 | 28 | def decode_jwe(value): 29 | return base64.urlsafe_b64decode(value + "==") 30 | 31 | 32 | def decode_value(value, encoding): 33 | """Decode Hex or Base64 string to byte sequence.""" 34 | 35 | if type(value) is str: 36 | if encoding == ClientEncoding.HEX: 37 | decoded = bytes.fromhex(value) 38 | elif encoding == ClientEncoding.BASE64: 39 | decoded = base64.b64decode(value) 40 | else: 41 | raise EncodingError("Decode: Invalid encoding.") 42 | 43 | return decoded 44 | else: 45 | raise ValueError("Decode: Invalid or missing input string.") 46 | 47 | 48 | class ClientEncoding(Enum): 49 | BASE64 = 'BASE64' 50 | HEX = 'HEX' 51 | -------------------------------------------------------------------------------- /client_encryption/encryption_exception.py: -------------------------------------------------------------------------------- 1 | class EncryptionError(Exception): 2 | """Encryption related exception for client-encryption module.""" 3 | pass 4 | 5 | 6 | class EncodingError(Exception): 7 | """Encoding not supported or invalid.""" 8 | pass 9 | 10 | 11 | class CertificateError(Exception): 12 | """Certificate exception for client-encryption module.""" 13 | pass 14 | 15 | 16 | class PrivateKeyError(Exception): 17 | """Private key exception for client-encryption module.""" 18 | pass 19 | 20 | 21 | class HashAlgorithmError(Exception): 22 | """Hash algorithm exception for client-encryption module.""" 23 | pass 24 | 25 | 26 | class KeyWrappingError(Exception): 27 | """Encryption exception on wrapping/unwrapping session key for client-encryption module.""" 28 | pass 29 | -------------------------------------------------------------------------------- /client_encryption/encryption_utils.py: -------------------------------------------------------------------------------- 1 | from Crypto.Hash import SHA1, SHA224, SHA256, SHA384, SHA512 2 | from Crypto.PublicKey import RSA 3 | from cryptography import x509 4 | from cryptography.hazmat.primitives import serialization 5 | from cryptography.hazmat.primitives.serialization import Encoding 6 | from cryptography.hazmat.primitives.serialization import pkcs12 7 | from enum import IntEnum 8 | 9 | from client_encryption.encryption_exception import CertificateError, PrivateKeyError, HashAlgorithmError 10 | 11 | _SUPPORTED_HASH = {"SHA1": SHA1, "SHA224": SHA224, "SHA256": SHA256, "SHA384": SHA384, "SHA512": SHA512} 12 | 13 | 14 | class FileType(IntEnum): 15 | FILETYPE_PEM = 0 16 | FILETYPE_ASN1 = 1 17 | FILETYPE_INVALID = -1 18 | 19 | 20 | def load_encryption_certificate(certificate_path): 21 | """Load X509 encryption certificate data at the given file path.""" 22 | 23 | try: 24 | with open(certificate_path, "rb") as cert_content: 25 | certificate = cert_content.read() 26 | except IOError: 27 | raise CertificateError("Unable to load certificate.") 28 | 29 | try: 30 | cert_type = __get_crypto_file_type(certificate) 31 | 32 | if cert_type == FileType.FILETYPE_PEM: 33 | cert = x509.load_pem_x509_certificate(certificate) 34 | return cert, Encoding.PEM 35 | if cert_type == FileType.FILETYPE_ASN1: 36 | cert = x509.load_der_x509_certificate(certificate) 37 | return cert, Encoding.DER 38 | if cert_type == FileType.FILETYPE_INVALID: 39 | raise CertificateError("Wrong certificate format.") 40 | except ValueError: 41 | raise CertificateError("Invalid certificate format.") 42 | 43 | 44 | def write_encryption_certificate(certificate_path, certificate, cert_type): 45 | with open(certificate_path, "wb") as f: 46 | f.write(certificate.public_bytes(cert_type)) 47 | 48 | 49 | def load_decryption_key(key_file_path, decryption_key_password=None): 50 | """Load a RSA decryption key.""" 51 | 52 | try: 53 | with open(key_file_path, "rb") as key_content: 54 | private_key = key_content.read() 55 | # if key format is p12 (decryption_key_password is populated) then we have to retrieve the private key 56 | if decryption_key_password is not None: 57 | private_key = __load_pkcs12_private_key(private_key, decryption_key_password) 58 | return RSA.importKey(private_key) 59 | except ValueError: 60 | raise PrivateKeyError("Wrong decryption key format.") 61 | except (Exception): 62 | raise PrivateKeyError("Unable to load key file.") 63 | 64 | 65 | def __load_pkcs12_private_key(pkcs_file, password): 66 | private_key, certs, addcerts = pkcs12.load_key_and_certificates(pkcs_file, password.encode("utf-8")) 67 | return private_key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, 68 | serialization.NoEncryption()) 69 | 70 | 71 | def __get_crypto_file_type(file_content): 72 | if file_content.startswith(b"-----BEGIN "): 73 | return FileType.FILETYPE_PEM 74 | else: 75 | return FileType.FILETYPE_ASN1 76 | 77 | 78 | def validate_hash_algorithm(algo_str): 79 | """Validate a hash algorithm against a list of supported ones.""" 80 | 81 | if algo_str: 82 | algo_key = algo_str.replace("-", "").upper() 83 | 84 | if algo_key in _SUPPORTED_HASH: 85 | return algo_key 86 | else: 87 | raise HashAlgorithmError("Hash algorithm invalid or not supported.") 88 | else: 89 | raise HashAlgorithmError("No hash algorithm provided.") 90 | 91 | 92 | def load_hash_algorithm(algo_str): 93 | """Load a hash algorithm object of Crypto.Hash from a list of supported ones.""" 94 | 95 | return _SUPPORTED_HASH[validate_hash_algorithm(algo_str)] 96 | -------------------------------------------------------------------------------- /client_encryption/field_level_encryption.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | from Crypto.Cipher import AES 4 | from Crypto.Util.Padding import pad, unpad 5 | 6 | from client_encryption.encoding_utils import encode_bytes, decode_value 7 | from client_encryption.encryption_exception import EncryptionError 8 | from client_encryption.json_path_utils import get_node, pop_node, update_node, cleanup_node 9 | from client_encryption.session_key_params import SessionKeyParams 10 | 11 | 12 | def encrypt_payload(payload, config, _params=None): 13 | """Encrypt some fields of a JSON payload using the given configuration.""" 14 | 15 | try: 16 | json_payload = copy.deepcopy(payload) if type(payload) is dict or type(payload) is list else json.loads(payload) 17 | 18 | for elem, target in config.paths["$"].to_encrypt.items(): 19 | if not _params: 20 | params = SessionKeyParams.generate(config) 21 | else: 22 | params = _params 23 | 24 | try: 25 | value = pop_node(json_payload, elem) 26 | 27 | try: 28 | encrypted_value = _encrypt_value(params.key, params.iv_spec, value) 29 | crypto_node = get_node(json_payload, target, create=True) 30 | crypto_node[config.encrypted_value_field_name] = encode_bytes(encrypted_value, config.data_encoding) 31 | 32 | if not _params: 33 | _populate_node_with_key_params(crypto_node, config, params) 34 | 35 | except KeyError: 36 | raise EncryptionError("Field " + target + " not found!") 37 | 38 | except KeyError: 39 | pass # data-to-encrypt node not found, nothing to encrypt 40 | 41 | return json_payload 42 | 43 | except (IOError, ValueError, TypeError) as e: 44 | raise EncryptionError("Payload encryption failed!", e) 45 | 46 | 47 | def decrypt_payload(payload, config, _params=None): 48 | """Decrypt some fields of a JSON payload using the given configuration.""" 49 | 50 | try: 51 | json_payload = payload if type(payload) is dict or type(payload) is list else json.loads(payload) 52 | 53 | for elem, target in config.paths["$"].to_decrypt.items(): 54 | try: 55 | node = get_node(json_payload, elem) 56 | 57 | cipher_text = decode_value(node.pop(config.encrypted_value_field_name), config.data_encoding) 58 | 59 | if not _params: 60 | try: 61 | encrypted_key = node.pop(config.encrypted_key_field_name) 62 | iv = node.pop(config.iv_field_name) 63 | except KeyError: 64 | raise EncryptionError("Encryption field(s) missing in payload.") 65 | 66 | oaep_digest_algo = node.pop(config.oaep_padding_digest_algorithm_field_name, 67 | config.oaep_padding_digest_algorithm) 68 | 69 | _remove_fingerprint_from_node(node, config) 70 | 71 | params = SessionKeyParams(config, encrypted_key, iv, oaep_digest_algo) 72 | else: 73 | params = _params 74 | 75 | cleanup_node(json_payload, elem, target) 76 | 77 | try: 78 | update_node(json_payload, target, _decrypt_bytes(params.key, params.iv_spec, cipher_text)) 79 | except KeyError: 80 | raise EncryptionError("Field '" + target + "' not found!") 81 | 82 | except KeyError: 83 | pass # encrypted data node not found, nothing to decrypt 84 | 85 | return json_payload 86 | 87 | except json.JSONDecodeError: # not a json response - return it as is 88 | return payload 89 | except (IOError, ValueError, TypeError) as e: 90 | raise EncryptionError("Payload decryption failed!", e) 91 | 92 | 93 | def _encrypt_value(_key, iv, node_str): 94 | padded_node = pad(node_str.encode('utf-8'), AES.block_size) 95 | 96 | aes = AES.new(_key, AES.MODE_CBC, iv) 97 | return aes.encrypt(padded_node) 98 | 99 | 100 | def _decrypt_bytes(_key, iv, _bytes): 101 | aes = AES.new(_key, AES.MODE_CBC, iv) 102 | plain_bytes = aes.decrypt(_bytes) 103 | 104 | return unpad(plain_bytes, AES.block_size).decode('utf-8') 105 | 106 | 107 | def _populate_node_with_key_params(node, config, params): 108 | node[config.encrypted_key_field_name] = params.encrypted_key_value 109 | node[config.iv_field_name] = params.iv_value 110 | if config.oaep_padding_digest_algorithm_field_name: 111 | node[config.oaep_padding_digest_algorithm_field_name] = params.oaep_padding_digest_algorithm_value 112 | if config.encryption_certificate_fingerprint_field_name: 113 | node[config.encryption_certificate_fingerprint_field_name] = config.encryption_certificate_fingerprint 114 | if config.encryption_key_fingerprint_field_name: 115 | node[config.encryption_key_fingerprint_field_name] = config.encryption_key_fingerprint 116 | 117 | 118 | def _remove_fingerprint_from_node(node, config): 119 | if config.encryption_certificate_fingerprint_field_name in node: 120 | del node[config.encryption_certificate_fingerprint_field_name] 121 | if config.encryption_key_fingerprint_field_name in node: 122 | del node[config.encryption_key_fingerprint_field_name] 123 | -------------------------------------------------------------------------------- /client_encryption/field_level_encryption_config.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Crypto.Hash import SHA256 3 | from cryptography.hazmat.primitives.serialization import PublicFormat, Encoding 4 | 5 | from client_encryption import encoding_utils 6 | from client_encryption.encryption_utils import load_encryption_certificate, load_decryption_key, validate_hash_algorithm 7 | 8 | 9 | class FieldLevelEncryptionConfig(object): 10 | """Class implementing a full configuration for field level encryption.""" 11 | 12 | def __init__(self, conf): 13 | if type(conf) is str: 14 | json_config = json.loads(conf) 15 | elif type(conf) is dict: 16 | json_config = conf 17 | else: 18 | raise ValueError("Invalid configuration format. Must be valid json string or dict.") 19 | 20 | if not json_config["paths"]: 21 | raise KeyError("Invalid configuration. Must provide at least one service path.") 22 | 23 | self._paths = dict() 24 | for path, opt in json_config["paths"].items(): 25 | self._paths[path] = EncryptionPathConfig(opt) 26 | 27 | if "encryptionCertificate" in json_config: 28 | x509_cert, cert_type = load_encryption_certificate(json_config["encryptionCertificate"]) 29 | self._encryption_certificate = x509_cert 30 | # Fixed encoding is required, regardless of initial certificate encoding to ensure correct calculation of fingerprint value 31 | self._encryption_certificate_type = Encoding.DER 32 | self._encryption_key_fingerprint = \ 33 | json_config.get("encryptionKeyFingerprint", self.__compute_fingerprint( 34 | x509_cert.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo))) 35 | self._encryption_certificate_fingerprint = \ 36 | json_config.get("encryptionCertificateFingerprint", 37 | self.__compute_fingerprint(x509_cert.public_bytes(Encoding.DER))) 38 | 39 | else: 40 | self._encryption_certificate = None 41 | self._encryption_key_fingerprint = None 42 | self._encryption_certificate_fingerprint = None 43 | self._encryption_certificate_type = None 44 | 45 | if "decryptionKey" in json_config: 46 | decryption_key_password = json_config.get("decryptionKeyPassword", None) 47 | self._decryption_key = load_decryption_key(json_config["decryptionKey"], decryption_key_password) 48 | else: 49 | self._decryption_key = None 50 | 51 | self._oaep_padding_digest_algorithm = validate_hash_algorithm(json_config["oaepPaddingDigestAlgorithm"]) 52 | 53 | data_enc = encoding_utils.ClientEncoding(json_config["dataEncoding"].upper()) 54 | self._data_encoding = data_enc 55 | self._iv_field_name = json_config["ivFieldName"] 56 | self._encrypted_key_field_name = json_config["encryptedKeyFieldName"] 57 | self._encrypted_value_field_name = json_config["encryptedValueFieldName"] 58 | 59 | self._encryption_certificate_fingerprint_field_name = \ 60 | json_config.get("encryptionCertificateFingerprintFieldName", None) 61 | self._encryption_key_fingerprint_field_name = \ 62 | json_config.get("encryptionKeyFingerprintFieldName", None) 63 | self._oaep_padding_digest_algorithm_field_name = \ 64 | json_config.get("oaepPaddingDigestAlgorithmFieldName", None) 65 | 66 | self._use_http_headers = json_config.get("useHttpHeaders", False) 67 | 68 | @property 69 | def paths(self): 70 | return self._paths 71 | 72 | @property 73 | def encryption_certificate(self): 74 | return self._encryption_certificate 75 | 76 | @property 77 | def encryption_certificate_type(self): 78 | return self._encryption_certificate_type 79 | 80 | @property 81 | def encryption_key_fingerprint(self): 82 | return self._encryption_key_fingerprint 83 | 84 | @property 85 | def encryption_certificate_fingerprint(self): 86 | return self._encryption_certificate_fingerprint 87 | 88 | @property 89 | def decryption_key(self): 90 | return self._decryption_key 91 | 92 | @property 93 | def oaep_padding_digest_algorithm(self): 94 | return self._oaep_padding_digest_algorithm 95 | 96 | @property 97 | def data_encoding(self): 98 | return self._data_encoding 99 | 100 | @property 101 | def iv_field_name(self): 102 | return self._iv_field_name 103 | 104 | @property 105 | def encrypted_key_field_name(self): 106 | return self._encrypted_key_field_name 107 | 108 | @property 109 | def encrypted_value_field_name(self): 110 | return self._encrypted_value_field_name 111 | 112 | @property 113 | def encryption_certificate_fingerprint_field_name(self): 114 | return self._encryption_certificate_fingerprint_field_name 115 | 116 | @property 117 | def encryption_key_fingerprint_field_name(self): 118 | return self._encryption_key_fingerprint_field_name 119 | 120 | @property 121 | def oaep_padding_digest_algorithm_field_name(self): 122 | return self._oaep_padding_digest_algorithm_field_name 123 | 124 | @property 125 | def use_http_headers(self): 126 | return self._use_http_headers 127 | 128 | @staticmethod 129 | def __compute_fingerprint(asn1): 130 | return SHA256.new(asn1).hexdigest() 131 | 132 | 133 | class EncryptionPathConfig(object): 134 | 135 | def __init__(self, conf): 136 | self._to_encrypt = conf["toEncrypt"] 137 | self._to_decrypt = conf["toDecrypt"] 138 | 139 | @property 140 | def to_encrypt(self): 141 | return self._to_encrypt 142 | 143 | @property 144 | def to_decrypt(self): 145 | return self._to_decrypt 146 | -------------------------------------------------------------------------------- /client_encryption/json_path_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | _SEPARATOR = "." 4 | _ROOT_SYMBOL = "$" 5 | 6 | 7 | def __not_root(path): return path != _ROOT_SYMBOL 8 | 9 | 10 | def __target_in_path(path, target): return target and target.startswith(path) 11 | 12 | 13 | def get_node(tree, path, create=False): 14 | """Retrieve json or value given a path""" 15 | 16 | if not path: 17 | raise ValueError("Cannot accept empty path") 18 | 19 | current = tree 20 | if __not_root(path): 21 | current = __get_node(tree, path.split(_SEPARATOR), create) 22 | 23 | return current # is a dict 24 | 25 | 26 | def update_node(tree, path, node_str): 27 | """Update node with json or value in string format given a path""" 28 | 29 | __check_path_not_empty(path) 30 | 31 | if __not_root(path): 32 | parent = path.split(_SEPARATOR) 33 | to_set = parent.pop() 34 | current_node = __get_node(tree, parent, False) if parent else tree 35 | 36 | try: 37 | node_json = json.loads(node_str) 38 | except json.JSONDecodeError: 39 | node_json = node_str 40 | 41 | if type(current_node) is list: 42 | update_node_list(to_set, current_node, node_json) 43 | elif to_set in current_node and type(current_node[to_set]) is dict and type(node_json) is dict: 44 | current_node[to_set].update(node_json) 45 | else: 46 | current_node[to_set] = node_json 47 | else: 48 | tree.clear() 49 | tree.update(json.loads(node_str)) 50 | 51 | return tree 52 | 53 | 54 | def update_node_list(to_set, current_node, node_json): 55 | if to_set in current_node[0] and type(current_node[0][to_set]) is dict and type(node_json) is dict: 56 | current_node[0][to_set].update(node_json) 57 | else: 58 | current_node[0][to_set] = node_json 59 | 60 | 61 | def pop_node(tree, path): 62 | """Retrieve and delete json or value given a path""" 63 | 64 | __check_path_not_empty(path) 65 | 66 | if __not_root(path): 67 | parent = path.split(_SEPARATOR) 68 | to_delete = parent.pop() 69 | if parent: 70 | node = __get_node(tree, parent, False) 71 | else: 72 | node = tree 73 | 74 | if type(node) is list: 75 | deleted_elem = node[0].pop(to_delete) 76 | else: 77 | deleted_elem = node.pop(to_delete) 78 | if isinstance(deleted_elem, str): 79 | return deleted_elem 80 | else: 81 | return json.dumps(deleted_elem) 82 | 83 | else: 84 | node = json.dumps(tree) 85 | tree.clear() 86 | return node 87 | 88 | 89 | def cleanup_node(tree, path, target): 90 | """Remove a node if not in target path and no child is found given a path""" 91 | 92 | __check_path_not_empty(path) 93 | 94 | if __not_root(path): 95 | if not __target_in_path(path, target): 96 | parent = path.split(_SEPARATOR) 97 | to_delete = parent.pop() 98 | if parent: 99 | node = __get_node(tree, parent, False) 100 | else: 101 | node = tree 102 | if type(node) is list and not node[0][to_delete]: 103 | del node[0][to_delete] 104 | elif not node[to_delete]: 105 | del node[to_delete] 106 | 107 | else: 108 | if not tree: 109 | tree.clear() 110 | 111 | return tree 112 | 113 | 114 | def __get_node(tree, node_list, create): 115 | current = tree 116 | last_node = node_list.pop() 117 | 118 | for node in node_list: 119 | if type(current) is list: 120 | current = current[0][node] 121 | else: 122 | current = current[node] 123 | 124 | if type(current) is not dict and type(current) is not list: 125 | raise ValueError("'" + current + "' is not of dict type") 126 | 127 | if type(current) is list: 128 | if not current and create: 129 | d = dict() 130 | d[last_node] = {} 131 | current.append(d) 132 | elif last_node not in current[0] and create: 133 | current[0][last_node] = {} 134 | return current[0][last_node] 135 | elif last_node not in current and create: 136 | current[last_node] = {} 137 | 138 | return current[last_node] 139 | 140 | 141 | def __check_path_not_empty(path): 142 | if not path: 143 | raise ValueError("Cannot accept empty path") 144 | -------------------------------------------------------------------------------- /client_encryption/jwe_encryption.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | from Crypto.Cipher import AES 4 | from Crypto.Cipher.AES import block_size 5 | from Crypto.Util.Padding import unpad 6 | 7 | from client_encryption.encoding_utils import url_encode_bytes, decode_jwe 8 | from client_encryption.encryption_exception import EncryptionError 9 | from client_encryption.json_path_utils import pop_node, update_node, get_node 10 | from client_encryption.session_key_params import SessionKeyParams 11 | 12 | 13 | def encrypt_payload(payload, config, _params=None): 14 | algorithm = "RSA-OAEP-256" 15 | cty = "application/json" 16 | enc = "A256GCM" 17 | 18 | try: 19 | json_payload = copy.deepcopy(payload) if type(payload) is dict or type(payload) is list else json.loads(payload) 20 | 21 | for elem, target in config.paths["$"].to_encrypt.items(): 22 | if not _params: 23 | params = SessionKeyParams.generate(config) 24 | else: 25 | params = _params 26 | 27 | try: 28 | value = pop_node(json_payload, elem) 29 | 30 | try: 31 | header = _build_header(algorithm, enc, cty, config.encryption_key_fingerprint) 32 | encoded_header = url_encode_bytes(header.encode()) 33 | aad = encoded_header.encode('ascii') 34 | 35 | encoded_payload = value.encode() 36 | iv = params.iv_spec 37 | cipher = AES.new(params.key, AES.MODE_GCM, iv) 38 | cipher.update(aad) 39 | 40 | encrypted_and_digest = cipher.encrypt_and_digest(encoded_payload) 41 | full_cipher_text = encrypted_and_digest[0] + encrypted_and_digest[1] 42 | 43 | cipher_text = full_cipher_text[: len(full_cipher_text) - 16] 44 | tag = full_cipher_text[-16:] 45 | 46 | jwe_payload = _jwe_compact_serialize(encoded_header, params.encrypted_key_value, iv, cipher_text, 47 | tag) 48 | 49 | if isinstance(json_payload, list): 50 | json_payload = {config.encrypted_value_field_name: jwe_payload} 51 | else: 52 | crypto_node = get_node(json_payload, target, create=True) 53 | crypto_node[config.encrypted_value_field_name] = jwe_payload 54 | 55 | except KeyError: 56 | raise EncryptionError("Field " + target + " not found!") 57 | 58 | except KeyError: 59 | pass # data-to-encrypt node not found, nothing to encrypt 60 | 61 | return json_payload 62 | 63 | except (IOError, ValueError, TypeError) as e: 64 | raise EncryptionError("Payload encryption failed!", e) 65 | 66 | 67 | def decrypt_payload(payload, config, _params=None): 68 | try: 69 | json_payload = payload if type(payload) is dict else json.loads(payload) 70 | 71 | for elem, target in config.paths["$"].to_decrypt.items(): 72 | try: 73 | node = get_node(json_payload, elem) 74 | 75 | # If entire payload isn't encrypted 76 | if isinstance(node, dict): 77 | node = get_node(node, config.encrypted_value_field_name) 78 | 79 | encrypted_value = node.split(".") 80 | 81 | encrypted_key = decode_jwe(encrypted_value[1]) 82 | iv = decode_jwe(encrypted_value[2]) 83 | params = SessionKeyParams(config, encrypted_key, iv, 'SHA256') 84 | key = params.key 85 | 86 | header = json.loads(decode_jwe(encrypted_value[0])) 87 | cipher_text = decode_jwe(encrypted_value[3]) 88 | decryption_method = header['enc'] 89 | 90 | if decryption_method == 'A128CBC-HS256': 91 | aes = AES.new(key[16:], AES.MODE_CBC, iv) # NOSONAR 92 | elif decryption_method == 'A128GCM' or decryption_method == 'A192GCM' or decryption_method == 'A256GCM': 93 | aad = json.dumps(header).encode("ascii") 94 | aes = AES.new(key, AES.MODE_GCM, iv) 95 | aes.update(aad) 96 | else: 97 | raise EncryptionError("Unsupported decryption method:", decryption_method) 98 | 99 | decrypted = aes.decrypt(cipher_text) 100 | try: 101 | decoded_payload = unpad(decrypted, block_size) 102 | except ValueError: 103 | decoded_payload = decrypted 104 | 105 | if isinstance(json.loads(decoded_payload), list): 106 | json_payload = json.loads(decoded_payload) 107 | else: 108 | update_node(json_payload, target, decoded_payload) 109 | del json_payload[elem] 110 | except KeyError: 111 | pass # encrypted data node not found, nothing to decrypt 112 | 113 | return json_payload 114 | 115 | except json.JSONDecodeError: # not a json response - return it as is 116 | return payload 117 | except (IOError, ValueError, TypeError) as e: 118 | raise EncryptionError("Payload decryption failed!", e) 119 | 120 | 121 | def _jwe_compact_serialize(encoded_header, encrypted_cek, iv, cipher_text, auth_tag): 122 | encoded_cipher_text = url_encode_bytes(cipher_text) 123 | encoded_auth_tag = url_encode_bytes(auth_tag) 124 | encoded_iv = url_encode_bytes(iv) 125 | return ( 126 | encoded_header 127 | + "." 128 | + encrypted_cek 129 | + "." 130 | + encoded_iv 131 | + "." 132 | + encoded_cipher_text 133 | + "." 134 | + encoded_auth_tag 135 | ) 136 | 137 | 138 | def _build_header(alg, enc, cty, kid): 139 | header = {"alg": alg, "enc": enc, "kid": kid, "cty": cty} 140 | json_header = json.dumps( 141 | header, 142 | separators=(",", ":"), 143 | sort_keys=False 144 | ) 145 | return json_header 146 | -------------------------------------------------------------------------------- /client_encryption/jwe_encryption_config.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Crypto.Hash import SHA256 3 | from cryptography.hazmat.primitives.serialization import PublicFormat, Encoding 4 | 5 | from client_encryption.encoding_utils import ClientEncoding 6 | from client_encryption.encryption_utils import load_encryption_certificate, load_decryption_key 7 | 8 | 9 | class JweEncryptionConfig(object): 10 | """Class implementing a full configuration for field level encryption.""" 11 | 12 | def __init__(self, conf): 13 | if type(conf) is str: 14 | json_config = json.loads(conf) 15 | elif type(conf) is dict: 16 | json_config = conf 17 | else: 18 | raise ValueError("Invalid configuration format. Must be valid json string or dict.") 19 | 20 | if not json_config["paths"]: 21 | raise KeyError("Invalid configuration. Must provide at least one service path.") 22 | 23 | self._paths = dict() 24 | for path, opt in json_config["paths"].items(): 25 | self._paths[path] = EncryptionPathConfig(opt) 26 | 27 | if "encryptionCertificate" in json_config: 28 | x509_cert, cert_type = load_encryption_certificate(json_config["encryptionCertificate"]) 29 | self._encryption_certificate = x509_cert 30 | # Fixed encoding is required, regardless of initial certificate encoding to ensure correct calculation of fingerprint value 31 | self._encryption_certificate_type = Encoding.DER 32 | self._encryption_key_fingerprint = \ 33 | json_config.get("encryptionKeyFingerprint", self.__compute_fingerprint( 34 | x509_cert.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo))) 35 | else: 36 | self._encryption_certificate = None 37 | self._encryption_key_fingerprint = None 38 | self._encryption_certificate_type = None 39 | 40 | if "decryptionKey" in json_config: 41 | decryption_key_password = json_config.get("decryptionKeyPassword", None) 42 | self._decryption_key = load_decryption_key(json_config["decryptionKey"], decryption_key_password) 43 | else: 44 | self._decryption_key = None 45 | 46 | self._encrypted_value_field_name = json_config["encryptedValueFieldName"] 47 | 48 | # Fixed properties 49 | self._data_encoding = ClientEncoding.BASE64 50 | self._oaep_padding_digest_algorithm = "SHA256" 51 | 52 | @property 53 | def paths(self): 54 | return self._paths 55 | 56 | @property 57 | def data_encoding(self): 58 | return self._data_encoding 59 | 60 | @property 61 | def oaep_padding_digest_algorithm(self): 62 | return self._oaep_padding_digest_algorithm 63 | 64 | @property 65 | def encryption_certificate(self): 66 | return self._encryption_certificate 67 | 68 | @property 69 | def encryption_certificate_type(self): 70 | return self._encryption_certificate_type 71 | 72 | @property 73 | def encryption_key_fingerprint(self): 74 | return self._encryption_key_fingerprint 75 | 76 | @property 77 | def decryption_key(self): 78 | return self._decryption_key 79 | 80 | @property 81 | def encrypted_value_field_name(self): 82 | return self._encrypted_value_field_name 83 | 84 | @staticmethod 85 | def __compute_fingerprint(asn1): 86 | return SHA256.new(asn1).hexdigest() 87 | 88 | 89 | class EncryptionPathConfig(object): 90 | 91 | def __init__(self, conf): 92 | self._to_encrypt = conf["toEncrypt"] 93 | self._to_decrypt = conf["toDecrypt"] 94 | 95 | @property 96 | def to_encrypt(self): 97 | return self._to_encrypt 98 | 99 | @property 100 | def to_decrypt(self): 101 | return self._to_decrypt 102 | -------------------------------------------------------------------------------- /client_encryption/session_key_params.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import PKCS1_OAEP, AES 2 | from Crypto.PublicKey import RSA 3 | from Crypto.Random import get_random_bytes 4 | from binascii import Error 5 | from cryptography.hazmat.primitives.serialization import PublicFormat 6 | 7 | from client_encryption.encoding_utils import encode_bytes, decode_value, url_encode_bytes 8 | from client_encryption.encryption_exception import KeyWrappingError 9 | from client_encryption.encryption_utils import load_hash_algorithm 10 | from client_encryption.field_level_encryption_config import FieldLevelEncryptionConfig 11 | 12 | 13 | class SessionKeyParams(object): 14 | """Class implementing private session key and its params. Provide key and iv random generation functionality""" 15 | 16 | _JWE_KEY_SIZE = 256 // 8 17 | _MASTERCARD_KEY_SIZE = 128 // 8 18 | _BLOCK_SIZE = AES.block_size 19 | 20 | def __init__(self, config, encrypted_key, iv_value, padding_digest_algorithm=None): 21 | self._config = config 22 | self._encrypted_key_value = encrypted_key 23 | self._iv_value = iv_value 24 | self._oaep_padding_digest_algorithm_value = \ 25 | config.oaep_padding_digest_algorithm if padding_digest_algorithm is None else padding_digest_algorithm 26 | 27 | self._key = None 28 | self._iv = None 29 | 30 | @property 31 | def config(self): 32 | return self._config 33 | 34 | @property 35 | def key(self): 36 | if not self._key: 37 | self._key = SessionKeyParams.__unwrap_secret_key(self._encrypted_key_value, 38 | self._config, 39 | self._oaep_padding_digest_algorithm_value) 40 | 41 | return self._key 42 | 43 | @property 44 | def iv_spec(self): 45 | if not self._iv: 46 | self._iv = decode_value(self._iv_value, self._config.data_encoding) 47 | 48 | return self._iv 49 | 50 | @property 51 | def encrypted_key_value(self): 52 | return self._encrypted_key_value 53 | 54 | @property 55 | def iv_value(self): 56 | return self._iv_value 57 | 58 | @property 59 | def oaep_padding_digest_algorithm_value(self): 60 | return self._oaep_padding_digest_algorithm_value 61 | 62 | @staticmethod 63 | def generate(config): 64 | """Generate encryption parameters.""" 65 | # Generate an AES secret key 66 | if type(config) is FieldLevelEncryptionConfig: 67 | secret_key = get_random_bytes(SessionKeyParams._MASTERCARD_KEY_SIZE) 68 | else: 69 | secret_key = get_random_bytes(SessionKeyParams._JWE_KEY_SIZE) 70 | 71 | encoding = config.data_encoding 72 | 73 | # Generate a random IV 74 | iv = get_random_bytes(SessionKeyParams._BLOCK_SIZE) 75 | iv_encoded = encode_bytes(iv, encoding) 76 | 77 | # Encrypt the secret key 78 | secret_key_encrypted = SessionKeyParams.__wrap_secret_key(secret_key, config) 79 | 80 | key_params = SessionKeyParams(config, secret_key_encrypted, iv_encoded) 81 | key_params._key = secret_key 82 | key_params._iv = iv 83 | 84 | return key_params 85 | 86 | @staticmethod 87 | def __wrap_secret_key(plain_key, config): 88 | try: 89 | hash_algo = load_hash_algorithm(config.oaep_padding_digest_algorithm) 90 | _cipher = PKCS1_OAEP.new(key=RSA.import_key( 91 | config.encryption_certificate.public_key().public_bytes(config.encryption_certificate_type, 92 | PublicFormat.SubjectPublicKeyInfo)), 93 | hashAlgo=hash_algo) 94 | 95 | encrypted_secret_key = _cipher.encrypt(plain_key) 96 | if type(config) is FieldLevelEncryptionConfig: 97 | return encode_bytes(encrypted_secret_key, config.data_encoding) 98 | else: 99 | return url_encode_bytes(encrypted_secret_key) 100 | 101 | except (IOError, TypeError): 102 | raise KeyWrappingError("Unable to encrypt session secret key.") 103 | 104 | @staticmethod 105 | def __unwrap_secret_key(encrypted_key, config, _hash): 106 | try: 107 | hash_algo = load_hash_algorithm(_hash) 108 | 109 | if type(config) is FieldLevelEncryptionConfig: 110 | encrypted_key = decode_value(encrypted_key, config.data_encoding) 111 | 112 | _cipher = PKCS1_OAEP.new(key=config.decryption_key, 113 | hashAlgo=hash_algo) 114 | 115 | secret_key = _cipher.decrypt(encrypted_key) 116 | return secret_key 117 | 118 | except (IOError, TypeError, Error): 119 | raise KeyWrappingError("Unable to decrypt session secret key.") 120 | -------------------------------------------------------------------------------- /client_encryption/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | __version__ = "1.23.2" 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycryptodome==3.19.1 2 | setuptools>=69.1.0 3 | coverage>=4.5.3 4 | cryptography>=42.0.0 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | exec(open('client_encryption/version.py').read()) 4 | 5 | setup(name='mastercard-client-encryption', 6 | python_requires='>=3.8', 7 | version=__version__, 8 | description='Mastercard Client encryption.', 9 | long_description='Library for Mastercard API compliant payload encryption/decryption.', 10 | author='Mastercard', 11 | url='https://github.com/Mastercard/client-encryption-python', 12 | license='MIT', 13 | packages=['client_encryption'], 14 | classifiers=[ 15 | 'Development Status :: 5 - Production/Stable', 16 | 'Intended Audience :: Developers', 17 | 'Natural Language :: English', 18 | 'Operating System :: OS Independent', 19 | 'Programming Language :: Python :: 3.8', 20 | 'Programming Language :: Python :: 3.9', 21 | 'Topic :: Software Development :: Libraries :: Python Modules' 22 | ], 23 | tests_require=['coverage'], 24 | install_requires=['pycryptodome>=3.8.1', 'setuptools>=69.1.0', 'cryptography>=42.0.0' ] 25 | ) 26 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=Mastercard_client-encryption-python 2 | sonar.organization=mastercard 3 | sonar.projectName=client-encryption-python 4 | sonar.sources=./client_encryption 5 | sonar.tests=./tests 6 | sonar.python.coverage.reportPaths=coverage.xml 7 | sonar.host.url=https://sonarcloud.io -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | MASTERCARD_TEST_CONFIG = os.path.join(os.path.dirname(__file__), "resources/mastercard_test_config.json") 5 | JWE_TEST_CONFIG = os.path.join(os.path.dirname(__file__), "resources/jwe_test_config.json") 6 | TEST_RESOURCES_FOLDER = os.path.join(os.path.dirname(__file__), "resources/") 7 | 8 | 9 | def resource_path(file_name): return TEST_RESOURCES_FOLDER + file_name 10 | 11 | 12 | def get_mastercard_config_for_test(): 13 | with open(MASTERCARD_TEST_CONFIG, encoding='utf-8') as json_file: 14 | config = json.loads(json_file.read()) 15 | 16 | """ 17 | We need to update the certificate and key path in configuration in order to make it work with absolute path 18 | """ 19 | config["encryptionCertificate"] = resource_path(config["encryptionCertificate"]) 20 | config["decryptionKey"] = resource_path(config["decryptionKey"]) 21 | 22 | return json.dumps(config) 23 | 24 | def get_jwe_config_for_test(): 25 | with open(JWE_TEST_CONFIG, encoding='utf-8') as json_file: 26 | config = json.loads(json_file.read()) 27 | 28 | """ 29 | We need to update the certificate and key path in configuration in order to make it work with absolute path 30 | """ 31 | config["encryptionCertificate"] = resource_path(config["encryptionCertificate"]) 32 | config["decryptionKey"] = resource_path(config["decryptionKey"]) 33 | 34 | return json.dumps(config) 35 | -------------------------------------------------------------------------------- /tests/resources/certificates/test_certificate-2048.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/client-encryption-python/9b7fd6cc80bd9c563b8629830ef6f82872d70906/tests/resources/certificates/test_certificate-2048.der -------------------------------------------------------------------------------- /tests/resources/certificates/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 | -------------------------------------------------------------------------------- /tests/resources/jwe_test_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "paths": { 3 | "$": { 4 | "toEncrypt": { 5 | "node1.node2.colour": "node1.node2.enc" 6 | }, 7 | "toDecrypt": { 8 | "node1.node2.enc": "node1.node2.plainColour" 9 | } 10 | } 11 | }, 12 | "encryptedValueFieldName": "encryptedValue", 13 | "encryptionCertificate": "certificates/test_certificate-2048.der", 14 | "decryptionKey": "keys/test_key_pkcs8-2048.pem" 15 | } 16 | -------------------------------------------------------------------------------- /tests/resources/keys/test_invalid_key.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/client-encryption-python/9b7fd6cc80bd9c563b8629830ef6f82872d70906/tests/resources/keys/test_invalid_key.der -------------------------------------------------------------------------------- /tests/resources/keys/test_key.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/client-encryption-python/9b7fd6cc80bd9c563b8629830ef6f82872d70906/tests/resources/keys/test_key.p12 -------------------------------------------------------------------------------- /tests/resources/keys/test_key_pkcs1-1024.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQDHul8A7MM3ynx1XRxmLCRZ1Lr66QppWP5ZaEotsQPpFtyq3w/x 3 | dLWZd5XXQcIb/wUQWcbxBWYJPMk/GVQ3pvkIKEqC+h08VdXtfbBlM+RE8OYaQW5O 4 | Fg7YjHgL/WvCka6VJ990RuX9Zdj9TUu8GyqERll/XUDACs85atbSW6PBQQIDAQAB 5 | AoGBAL9acs0LCZn5OLalB6FoJ0edhasA/MWjysRUI8WU898s1SwsXDUEkTxAk2HR 6 | kayK7woUSYL/nhu5jkIS/Vn4clvH7XRkch22cjAAs4oGZXUUlVrcXFDiR0o2OqAI 7 | Xh24ESM/tpDWmn/N30afn31TBAKxgY5Ej2SrmK5gjSeMGI9xAkEA+gPM/P+jLiwy 8 | wuiS4QRYxeDFtluaafl7UHR/I6vL9VaqOptwV1e/JlytKNMCwoMqadd739/BAX9d 9 | qNgpniHC9QJBAMyCZBAc8PIjfM1h4seEDeb2mxl1Y2DmcDUVWIt+E4Zs3m1I59we 10 | VgY/BwX4UIElhgsVjNo6qXYPbWiql4/tzZ0CQQCLJ6RnyO2NXIJgY8yku6Ohd6r0 11 | BeZbR8XwEPdW5l8eTb9v4WZU5vz4oCqtB02I8DKiOJK1F7g4WijKOo5neoklAkBt 12 | G9/w7M/sD9zk4qWQVrboE4faRFPZ/fe9in7sJT6biHf/DFePi6vPt06y87FXxcJH 13 | JZ85SvTgZQi1P9aO1ovNAkAVxgJq94CKCZjOiks5xTro6V0pYdsx0tuuIlx/vGVG 14 | H3bcmpSm/S54nSovzbEDN7gKr4CSSsAQqC6oPwJSQN6m 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /tests/resources/keys/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 | -------------------------------------------------------------------------------- /tests/resources/keys/test_key_pkcs1-4096.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKAIBAAKCAgEA0054bLCGElQG5YldXGiu/nvEnJKevV/81xI+dAMSMzS2vd2O 3 | xxD4wZtqLPTB/x+33NsrymeboScrQXn0g0G9BdVXqXqLZIUiO0I9dHvb82GCZTaA 4 | v19AXY25T+In42aVARqMNO5uVlLWVak74XMmD8WKZP3eU9wdKMXq4ehNehiIa121 5 | HNNiPtTVVvZ3TAw1VxUeEaxtDLvpqD6WO9YBKfWc8GXdrtqFVWbbYUl7izlzj943 6 | z9VwkJgsj0b6qt58AcyiOAZ56VoFUx5N5p00EDcamTYRTvJFAFQH+/ejwcTsDm2s 7 | 4xCEjzZ/7DZ9FRwovPNZIH30+JtU+ld+BhZ/mWSddI5Zh1cor0uMeGNnpg4eLJI6 8 | fJ+/JjOdkhoZacY65DHozCsy5aOccmU9BrncQGG4+H1lN8L+gOpHZ7e/X6hnHxtg 9 | NbnIoSd7Uo5yIvUiCxxfPyCP/yjOqnfDtdR8F6GkMnjzCXGngyIvv25lz4p+gPdD 10 | j6PFSVHJkRMS5Lry+c7AOYFHzJ81K8BANPbiwT8D9x1znM6GVGfH687U/sbRiWLp 11 | maqGvEBlXhGgmQ5xqhwQCB9Ds9DWzdGrbXduCoLlDIkhM8tDXdGYMDSNrrOiD4JO 12 | ZP1Bhe/6+zMNPlusd3z44eCSEXpLCixdiPNvl1vNolytnJi35VZrIXPRessCAwEA 13 | AQKCAgBmQAWUCsOF4PVJY3QzAFEVwgx8+5Im72jpJeHkv4uyDaMUMz8g4vyMq0jw 14 | oiux6cZN8By7n/E2RT7wOzRvw4LVbMwzraIALVBIPqCAWmMv3ZJ8qagZct0xqB/x 15 | IO3OY1hdJVyNTIdF7GXdI7xfNxpG7X8vqY1JJS1TCprDYGcFWxPAaKL4ZO2Ym+L0 16 | ZuWJfirdjdF0GezXCaNij46hO8hqZnjf91sTfpign9outKE82Lsr9gsp3g3PWmPN 17 | nTo1Lt3w/PXOiIu7uJz1AKgPnSiRZCjR1NEBU8jCBOesLMQoQsM7pCTR569NocC7 18 | LA7RBURNUrBhQbImDvxK+8V26rIpRd8NbY4UuTY5wFVR0qgGtRZD9ocg1Fij5nEa 19 | xnv58hIHm8x8nOAQjtLjDj/PYRjgafaaxc+9q74bz30tqf5uHoNzJVVWJdzUN1he 20 | JOtdxjFOArtkVFSJs1mr3o0wwrbRDrVguAXA3z6YZGBHIfgB1hz13TFMcxfRWVVO 21 | cpZUmwx2uXTmOOX+iBUlcPulSnUxmV9wB219F2qPZqSQG6lD05L/GUGjeB56srVh 22 | NwYgC7HO6lJkBAb9JDvYfxR9kKQ5Ppen/vma4kfTcLlbciME3S99meRfOtmlGFGd 23 | yaSjiPEYyYlQlypSVPf64hDMUu1CuyfIM9CBJ6BOq7P73+qf+QKCAQEA7KkScU+F 24 | wK0QDzT3ohlxynMCUPI52/dl2G/rRERVsB3T+W+W1YrxurOu+5hjSH+0b0dnbs3t 25 | 5UKfBq6n0CRNGPXaha8BDq8r4K0rpvRUn3Xprk41c2qV0xo3INbuCZd1ldpFu3Jj 26 | GBmIJNC2r58Za7UJYaB10WNdJrPPLa7PUKreZeSBeAqFBqHmy1jA2nQiF5GStLY/ 27 | OIa8cxy7a0ZCa+bWVEXk6tPtgnJNFU/U2IQ46P2ldNswCbwJMouG4oWCQQ7ODpQA 28 | JqeC2o9mBDsmSyULJr7NyteVCACGBmz6QVp6mxoAa80jd9KyiSQZmZTDFAvuOpkS 29 | occ/K7BCzONEZQKCAQEA5JL+03qx1IUK9RYPChZjfrn8Tv8dKOIfNto842uRmH21 30 | boR1O5fHvyGHh8+N6Ao0PeFKb7CRQSnXE0W5SLEVw/kYyzEyo/zjpir7CVncsOG8 31 | 3RtzYOtnJ/jD9wIY3UsjXqX/x6EsolsWPgzJ19Tx0qAi+fnJAX+69z8vzXlmu8OP 32 | YuuG+UMpNftJO+ZxOncxsiNw3781v9k6GKffjuMe5W655om1AlQWaECUAWX66zPW 33 | LyVq2xOj41FU4k+xMqUMREBXyeX9vpig7ZVL7rTIMdG24CyromtcusBDTxueccWD 34 | nRW7S0UPFHYApig+5QSr1Y9zN9iFWJzCIS7fnx7XbwKCAQEA6ChLYUCjcwnSsThC 35 | nI/dYr5DzWhxfelJzXKtFoD6lhQMt6rSCpWM4JwX0dQBwUMVm/wt6TK2ZqpeGk4H 36 | bVXPE+dKAM5WeTM6FeOK6PLSeMNRA57RLHGonDghUGPHiz07Kk+/DE0ADMovFf5w 37 | 2AN5CoHDvDOOoGObI7ZMTQIpeXbFSKtKnpmjOYhlQaHFPgei0gAKLKCDkE4MW9gZ 38 | uvhnfDYslushz4MqgUbjez6fC+9ZbKY2Q1Yp38LIOv9IyLoztuJxHTfulfzJjuIR 39 | L6FexWSHdfDDLHMjTYBF+dO6A5Zgo/pz40yPuKHGZmY1fsXCQM4bWvyCnJU60P7N 40 | 6PQhSQKCAQANeDQYFkTgdy6cHr6oI4WddCxQI2x+ekTIoLex1ybvS4kjiB64cktN 41 | EhbAhBSitec6NkqCpm8I3gRUmGlAxV64+7bgUnfffgmUQzgj5u3AZq0QgoucDIM5 42 | sckqhy8b60+cRj/6bZ8JukBnS62hUGUnulQVUwjrU7Ga3FhezWambfHHLIX5rmGB 43 | UtuP8hZ+EYQWMUx3gvcR5SUtSsc7zlqFvq6pzTejeX0Qi62tH2tX7OgUQyo22sNv 44 | o91SsMuKZnuAkiIaPblkP+5L0d51pKWfefJC5579pUIDp0zQHpqJrdABs8QjvWAU 45 | HpgPMpPyPwI5RYjOo63H+QTfm7mF0PV1AoIBAA4RW+P999ANeuI9Uj8GDfoTv3p0 46 | 7xy97W2vE+sJHUnK2I1klgvyumN7WX9kzIMXk+oxxhoI1OZJuTCQ87Ax9l4uhOla 47 | U3EAx0CnDGU8HyuHc/RS0ZJpX4jw7I5ay367HnNpoKexCBVvKUTt6TFcDBtZMrWf 48 | uwpxKb4ufCwShQYqI4cWTnLK4drbB+u4ZZ0l+y30w1DLYZ8wpnmjfd2lsuRHy5Zr 49 | oGnMj7kunF9JWufwd4+CLhJLS+tqUki0q+HISDPijnPYuJlfmYUaCjDNJisj/lNg 50 | 9fYp8F0HlfLCjzVi8QOzboMF8SjLZI9gfRoY5xMg9uP/Z47M+HGeFftAuiU= 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /tests/resources/keys/test_key_pkcs1-512.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIBOQIBAAJBAOiY+DyTnjyIs41Z9SfqIEV+0BUuDqC4iceBIY98yTTGtlIkb27M 3 | DJjBAW2fhrxyG8Cb74SspmcD4u/mxrm6460CAwEAAQJAbbwViUa/paF8zFg/gAhG 4 | F2Nfuk5TWmIVpoj2k2J07q9W9JeY5sJfLvFwwsMc/vY6FPHGSO0P4sQ3MQHo1zyA 5 | RQIhAPir1/laHualWd5ssAJrvzoK7EtVSnjbuU1tVKBTLwnvAiEA73Paq1U0wO12 6 | DyfRcE+UQ1NwvROLVn0XQA/MAGMj+CMCIDjwMA2aQwUQy1kQjeSgAzMpGR3Os7Sk 7 | qvM9m2jyYwzlAiBrBFJUhI5BM1+yQk9+bHKM7HvUZSm/C8UaYnUAL07iFQIgCLIh 8 | fJ5xPiUD6tO1jn0Kauf1DOlwhDQC10zpOitM8xo= 9 | -----END RSA PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /tests/resources/keys/test_key_pkcs8-2048.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/client-encryption-python/9b7fd6cc80bd9c563b8629830ef6f82872d70906/tests/resources/keys/test_key_pkcs8-2048.der -------------------------------------------------------------------------------- /tests/resources/keys/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 | -------------------------------------------------------------------------------- /tests/resources/mastercard_test_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "paths": { 3 | "$": { 4 | "toEncrypt": { 5 | "node1.node2.colour": "node1.node2.enc" 6 | }, 7 | "toDecrypt": { 8 | "node1.node2.enc": "node1.node2.plainColour" 9 | } 10 | } 11 | }, 12 | "ivFieldName": "iv", 13 | "encryptedKeyFieldName": "encryptedKey", 14 | "encryptedValueFieldName": "encryptedValue", 15 | "dataEncoding": "base64", 16 | "encryptionCertificate": "certificates/test_certificate-2048.der", 17 | "decryptionKey": "keys/test_key_pkcs8-2048.pem", 18 | "oaepPaddingDigestAlgorithm": "SHA256", 19 | "encryptionCertificateFingerprintFieldName": "certFingerprint", 20 | "encryptionKeyFingerprintFieldName": "keyFingerprint", 21 | "oaepPaddingDigestAlgorithmFieldName": "oaepHashingAlgo" 22 | } 23 | -------------------------------------------------------------------------------- /tests/test_api_encryption.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch, Mock 3 | import json 4 | from tests.utils.api_encryption_test_utils import MockApiClient, MockService, MockRestApiClient 5 | from tests import get_mastercard_config_for_test, MASTERCARD_TEST_CONFIG, get_jwe_config_for_test 6 | import client_encryption.api_encryption as to_test 7 | 8 | 9 | class ApiEncryptionTest(unittest.TestCase): 10 | 11 | def setUp(self): 12 | self._json_config = json.loads(get_mastercard_config_for_test()) 13 | self._jwe_json_config = json.loads(get_jwe_config_for_test()) 14 | self._json_config["paths"]["$"]["toEncrypt"] = {"data": "encryptedData"} 15 | self._json_config["paths"]["$"]["toDecrypt"] = {"encryptedData": "data"} 16 | 17 | def _set_header_params_config(self): 18 | self._json_config.update({ 19 | "useHttpHeaders": True, 20 | "ivFieldName": "x-iv", 21 | "encryptedKeyFieldName": "x-key", 22 | "encryptionCertificateFingerprintFieldName": "x-cert-fingerprint", 23 | "encryptionKeyFingerprintFieldName": "x-key-fingerprint", 24 | "oaepPaddingDigestAlgorithmFieldName": "x-oaep-digest" 25 | }) 26 | 27 | @patch('client_encryption.api_encryption.FieldLevelEncryptionConfig') 28 | def test_ApiEncryption_with_config_as_file_name(self, FieldLevelEncryptionConfig): 29 | to_test.ApiEncryption(MASTERCARD_TEST_CONFIG) 30 | 31 | assert FieldLevelEncryptionConfig.called 32 | 33 | @patch('client_encryption.api_encryption.FieldLevelEncryptionConfig') 34 | def test_ApiEncryption_with_config_as_dict(self, FieldLevelEncryptionConfig): 35 | to_test.ApiEncryption(self._json_config) 36 | 37 | assert FieldLevelEncryptionConfig.called 38 | 39 | def test_ApiEncryption_fail_with_config_as_string(self): 40 | self.assertRaises(FileNotFoundError, to_test.ApiEncryption, "this is not accepted") 41 | 42 | def test_encrypt_payload_returns_same_data_type_as_input(self): 43 | api_encryption = to_test.ApiEncryption(self._json_config) 44 | 45 | test_headers = {"Content-Type": "application/json"} 46 | 47 | body = { 48 | "data": { 49 | "secret1": "test", 50 | "secret2": "secret" 51 | }, 52 | "encryptedData": {} 53 | } 54 | 55 | encrypted = api_encryption._encrypt_payload(body=body, headers=test_headers) 56 | self.assertIsInstance(encrypted, dict) 57 | 58 | encrypted = api_encryption._encrypt_payload(body=json.dumps(body), headers=test_headers) 59 | self.assertIsInstance(encrypted, str) 60 | 61 | encrypted = api_encryption._encrypt_payload(body=json.dumps(body).encode("utf-8"), headers=test_headers) 62 | self.assertIsInstance(encrypted, bytes) 63 | 64 | def test_encrypt_payload_with_params_in_body(self): 65 | api_encryption = to_test.ApiEncryption(self._json_config) 66 | 67 | test_headers = {"Content-Type": "application/json"} 68 | 69 | encrypted = api_encryption._encrypt_payload(body={ 70 | "data": { 71 | "secret1": "test", 72 | "secret2": "secret" 73 | }, 74 | "encryptedData": {} 75 | }, headers=test_headers) 76 | 77 | self.assertNotIn("data", encrypted) 78 | self.assertIn("encryptedData", encrypted) 79 | self.assertIn("encryptedValue", encrypted["encryptedData"]) 80 | self.assertEqual(6, len(encrypted["encryptedData"].keys())) 81 | self.assertDictEqual({"Content-Type": "application/json"}, test_headers) 82 | 83 | def test_decrypt_payload_with_params_in_body(self): 84 | api_encryption = to_test.ApiEncryption(self._json_config) 85 | 86 | test_headers = {"Content-Type": "application/json"} 87 | 88 | decrypted = json.loads(api_encryption._decrypt_payload(body={ 89 | "encryptedData": { 90 | "iv": "uldLBySPY3VrznePihFYGQ==", 91 | "encryptedKey": "Jmh/bQPScUVFHSC9qinMGZ4lM7uetzUXcuMdEpC5g4C0Pb9HuaM3zC7K/509n7RTBZUPEzgsWtgi7m33nhpXsUo8WMcQkBIZlKn3ce+WRyZpZxcYtVoPqNn3benhcv7cq7yH1ktamUiZ5Dq7Ga+oQCaQEsOXtbGNS6vA5Bwa1pjbmMiRIbvlstInz8XTw8h/T0yLBLUJ0yYZmzmt+9i8qL8KFQ/PPDe5cXOCr1Aq2NTSixe5F2K/EI00q6D7QMpBDC7K6zDWgAOvINzifZ0DTkxVe4EE6F+FneDrcJsj+ZeIabrlRcfxtiFziH6unnXktta0sB1xcszIxXdMDbUcJA==", 92 | "encryptedValue": "KGfmdUWy89BwhQChzqZJ4w==", 93 | "oaepHashingAlgo": "SHA256" 94 | } 95 | }, headers=test_headers)) 96 | 97 | self.assertNotIn("encryptedData", decrypted) 98 | self.assertDictEqual({"data": {}}, decrypted) 99 | self.assertDictEqual({"Content-Type": "application/json"}, test_headers) 100 | 101 | def test_encrypt_payload_with_params_in_headers(self): 102 | self._set_header_params_config() 103 | 104 | test_headers = {"Content-Type": "application/json"} 105 | 106 | api_encryption = to_test.ApiEncryption(self._json_config) 107 | encrypted = api_encryption._encrypt_payload(body={ 108 | "data": { 109 | "secret1": "test", 110 | "secret2": "secret" 111 | }, 112 | "encryptedData": {} 113 | }, headers=test_headers) 114 | 115 | self.assertNotIn("data", encrypted) 116 | self.assertIn("encryptedData", encrypted) 117 | self.assertIn("encryptedValue", encrypted["encryptedData"]) 118 | self.assertEqual(1, len(encrypted["encryptedData"].keys())) 119 | self.assertIn("x-iv", test_headers) 120 | self.assertIn("x-key", test_headers) 121 | self.assertIn("x-cert-fingerprint", test_headers) 122 | self.assertIn("x-key-fingerprint", test_headers) 123 | self.assertIn("x-oaep-digest", test_headers) 124 | self.assertEqual(6, len(test_headers.keys())) 125 | 126 | def test_decrypt_payload_with_params_in_headers(self): 127 | self._set_header_params_config() 128 | 129 | test_headers = {"Content-Type": "application/json", 130 | "x-iv": "uldLBySPY3VrznePihFYGQ==", 131 | "x-key": "Jmh/bQPScUVFHSC9qinMGZ4lM7uetzUXcuMdEpC5g4C0Pb9HuaM3zC7K/509n7RTBZUPEzgsWtgi7m33nhpXsUo8WMcQkBIZlKn3ce+WRyZpZxcYtVoPqNn3benhcv7cq7yH1ktamUiZ5Dq7Ga+oQCaQEsOXtbGNS6vA5Bwa1pjbmMiRIbvlstInz8XTw8h/T0yLBLUJ0yYZmzmt+9i8qL8KFQ/PPDe5cXOCr1Aq2NTSixe5F2K/EI00q6D7QMpBDC7K6zDWgAOvINzifZ0DTkxVe4EE6F+FneDrcJsj+ZeIabrlRcfxtiFziH6unnXktta0sB1xcszIxXdMDbUcJA==", 132 | "x-cert-fingerprint": "gIEPwTqDGfzw4uwyLIKkwwS3gsw85nEXY0PP6BYMInk=", 133 | "x-key-fingerprint": "dhsAPB6t46VJDlAA03iHuqXm7A4ibAdwblmUUfwDKnk=", 134 | "x-oaep-digest": "SHA256" 135 | } 136 | 137 | api_encryption = to_test.ApiEncryption(self._json_config) 138 | decrypted = json.loads(api_encryption._decrypt_payload(body={ 139 | "encryptedData": { 140 | "encryptedValue": "KGfmdUWy89BwhQChzqZJ4w==" 141 | } 142 | }, headers=test_headers)) 143 | 144 | self.assertNotIn("encryptedData", decrypted) 145 | self.assertDictEqual({"data": {}}, decrypted) 146 | self.assertDictEqual({"Content-Type": "application/json"}, test_headers) 147 | 148 | def test_decrypt_payload_with_params_in_headers_skip_decrypt(self): 149 | self._set_header_params_config() 150 | 151 | test_headers = {"Content-Type": "application/json", 152 | "x-key": "Jmh/bQPScUVFHSC9qinMGZ4lM7uetzUXcuMdEpC5g4C0Pb9HuaM3zC7K/509n7RTBZUPEzgsWtgi7m33nhpXsUo8WMcQkBIZlKn3ce+WRyZpZxcYtVoPqNn3benhcv7cq7yH1ktamUiZ5Dq7Ga+oQCaQEsOXtbGNS6vA5Bwa1pjbmMiRIbvlstInz8XTw8h/T0yLBLUJ0yYZmzmt+9i8qL8KFQ/PPDe5cXOCr1Aq2NTSixe5F2K/EI00q6D7QMpBDC7K6zDWgAOvINzifZ0DTkxVe4EE6F+FneDrcJsj+ZeIabrlRcfxtiFziH6unnXktta0sB1xcszIxXdMDbUcJA==", 153 | "x-cert-fingerprint": "gIEPwTqDGfzw4uwyLIKkwwS3gsw85nEXY0PP6BYMInk=", 154 | "x-key-fingerprint": "dhsAPB6t46VJDlAA03iHuqXm7A4ibAdwblmUUfwDKnk=", 155 | "x-oaep-digest": "SHA256" 156 | } 157 | 158 | api_encryption = to_test.ApiEncryption(self._json_config) 159 | decrypted = api_encryption._decrypt_payload(body={ 160 | "data": { 161 | "key1": "notSecret", 162 | "key2": "anotherValue" 163 | }, 164 | }, headers=test_headers) 165 | 166 | self.assertDictEqual({"data": {"key1": "notSecret", "key2": "anotherValue"}}, decrypted) 167 | self.assertEqual(5, len(test_headers.keys())) 168 | 169 | @patch('client_encryption.api_encryption.FieldLevelEncryptionConfig') 170 | def test_add_header_encryption_layer_with_config_as_file_name(self, FieldLevelEncryptionConfig): 171 | to_test.add_encryption_layer(MockApiClient(), MASTERCARD_TEST_CONFIG) 172 | 173 | assert FieldLevelEncryptionConfig.called 174 | 175 | @patch('client_encryption.api_encryption.FieldLevelEncryptionConfig') 176 | def test_add_header_encryption_layer_with_config_as_dict(self, FieldLevelEncryptionConfig): 177 | to_test.add_encryption_layer(MockApiClient(), self._json_config) 178 | 179 | assert FieldLevelEncryptionConfig.called 180 | 181 | def test_add_header_encryption_layer_fail_with_config_as_string(self): 182 | self.assertRaises(FileNotFoundError, to_test.add_encryption_layer, MockApiClient(), "this is not accepted") 183 | 184 | def test_add_encryption_layer_post(self): 185 | secret1 = 435 186 | secret2 = 746 187 | test_client = MockApiClient() 188 | to_test.add_encryption_layer(test_client, self._json_config) 189 | response = MockService(test_client).do_something_post(body={ 190 | "data": { 191 | "secret1": secret1, 192 | "secret2": secret2 193 | } 194 | }, headers={"Content-Type": "application/json"}) 195 | 196 | self.assertIn("data", json.loads(response.data)) 197 | self.assertIn("secret", json.loads(response.data)["data"]) 198 | self.assertEqual(secret2-secret1, json.loads(response.data)["data"]["secret"]) 199 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 200 | 201 | def test_add_encryption_layer_delete(self): 202 | secret1 = 394 203 | secret2 = 394 204 | test_client = MockApiClient() 205 | to_test.add_encryption_layer(test_client, self._json_config) 206 | response = MockService(test_client).do_something_delete(body={ 207 | "data": { 208 | "secret1": secret1, 209 | "secret2": secret2 210 | } 211 | }, headers={"Content-Type": "application/json"}) 212 | 213 | self.assertEqual("OK", json.loads(response.data)) 214 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 215 | 216 | def test_add_encryption_layer_get(self): 217 | test_client = MockApiClient() 218 | to_test.add_encryption_layer(test_client, self._json_config) 219 | response = MockService(test_client).do_something_get(headers={"Content-Type": "application/json"}) 220 | json_res = json.loads(response.data) 221 | 222 | self.assertIn("data", json_res) 223 | self.assertIn("secret", json_res['data']) 224 | self.assertEqual([53, 84, 75], json_res["data"]["secret"]) 225 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 226 | 227 | def test_add_header_encryption_layer_post_no_oaep_algo(self): 228 | self._set_header_params_config() 229 | del self._json_config["oaepPaddingDigestAlgorithmFieldName"] 230 | 231 | secret1 = 435 232 | secret2 = 746 233 | test_client = MockApiClient() 234 | to_test.add_encryption_layer(test_client, self._json_config) 235 | response = MockService(test_client).do_something_post_use_headers(body={ 236 | "data": { 237 | "secret1": secret1, 238 | "secret2": secret2 239 | }, 240 | "encryptedData": {} 241 | }, headers={"Content-Type": "application/json"}) 242 | 243 | self.assertIn("data", json.loads(response.data)) 244 | self.assertIn("secret", json.loads(response.data)["data"]) 245 | self.assertEqual(secret2-secret1, json.loads(response.data)["data"]["secret"]) 246 | self.assertDictEqual({"Content-Type": "application/json", "x-oaep-digest": "SHA256"}, response.getheaders()) 247 | 248 | def test_add_header_encryption_layer_post_no_cert_fingerprint(self): 249 | self._set_header_params_config() 250 | del self._json_config["encryptionCertificateFingerprintFieldName"] 251 | 252 | secret1 = 164 253 | secret2 = 573 254 | test_client = MockApiClient() 255 | to_test.add_encryption_layer(test_client, self._json_config) 256 | response = MockService(test_client).do_something_post_use_headers(body={ 257 | "data": { 258 | "secret1": secret1, 259 | "secret2": secret2 260 | }, 261 | "encryptedData": {} 262 | }, headers={"Content-Type": "application/json"}) 263 | 264 | self.assertIn("data", json.loads(response.data)) 265 | self.assertIn("secret", json.loads(response.data)["data"]) 266 | self.assertEqual(secret2-secret1, json.loads(response.data)["data"]["secret"]) 267 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 268 | 269 | def test_add_header_encryption_layer_post_no_pubkey_fingerprint(self): 270 | self._set_header_params_config() 271 | del self._json_config["encryptionKeyFingerprintFieldName"] 272 | 273 | secret1 = 245 274 | secret2 = 854 275 | test_client = MockApiClient() 276 | to_test.add_encryption_layer(test_client, self._json_config) 277 | response = MockService(test_client).do_something_post_use_headers(body={ 278 | "data": { 279 | "secret1": secret1, 280 | "secret2": secret2 281 | }, 282 | "encryptedData": {} 283 | }, headers={"Content-Type": "application/json"}) 284 | 285 | self.assertIn("data", json.loads(response.data)) 286 | self.assertIn("secret", json.loads(response.data)["data"]) 287 | self.assertEqual(secret2-secret1, json.loads(response.data)["data"]["secret"]) 288 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 289 | 290 | def test_add_header_encryption_layer_no_iv(self): 291 | self._set_header_params_config() 292 | del self._json_config["ivFieldName"] 293 | 294 | test_client = MockApiClient() 295 | 296 | self.assertRaises(KeyError, to_test.add_encryption_layer, test_client, self._json_config) 297 | 298 | def test_add_header_encryption_layer_no_secret_key(self): 299 | self._set_header_params_config() 300 | del self._json_config["encryptedKeyFieldName"] 301 | 302 | test_client = MockApiClient() 303 | 304 | self.assertRaises(KeyError, to_test.add_encryption_layer, test_client, self._json_config) 305 | 306 | def test_add_header_encryption_layer_post(self): 307 | self._set_header_params_config() 308 | 309 | secret1 = 445 310 | secret2 = 497 311 | test_client = MockApiClient() 312 | to_test.add_encryption_layer(test_client, self._json_config) 313 | response = MockService(test_client).do_something_post_use_headers(body={ 314 | "data": { 315 | "secret1": secret1, 316 | "secret2": secret2 317 | }, 318 | "encryptedData": {} 319 | }, headers={"Content-Type": "application/json"}) 320 | 321 | self.assertIn("data", json.loads(response.data)) 322 | self.assertIn("secret", json.loads(response.data)["data"]) 323 | self.assertEqual(secret2-secret1, json.loads(response.data)["data"]["secret"]) 324 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 325 | 326 | def test_add_header_encryption_layer_delete(self): 327 | self._set_header_params_config() 328 | 329 | secret1 = 783 330 | secret2 = 783 331 | test_client = MockApiClient() 332 | to_test.add_encryption_layer(test_client, self._json_config) 333 | response = MockService(test_client).do_something_delete_use_headers(body={ 334 | "data": { 335 | "secret1": secret1, 336 | "secret2": secret2 337 | }, 338 | "encryptedData": {} 339 | }, headers={"Content-Type": "application/json"}) 340 | 341 | self.assertEqual("OK", response.data) 342 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 343 | 344 | def test_add_header_encryption_layer_get(self): 345 | self._set_header_params_config() 346 | 347 | test_client = MockApiClient() 348 | to_test.add_encryption_layer(test_client, self._json_config) 349 | response = MockService(test_client).do_something_get_use_headers(headers={"Content-Type": "application/json"}) 350 | 351 | self.assertIn("data", json.loads(response.data)) 352 | self.assertIn("secret", json.loads(response.data)["data"]) 353 | self.assertEqual([53, 84, 75], json.loads(response.data)["data"]["secret"]) 354 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 355 | 356 | @patch('client_encryption.api_encryption.__oauth_warn') 357 | def test_add_encryption_layer_oauth_set(self, __oauth_warn): 358 | test_client = MockApiClient() 359 | test_rest_client = MockRestApiClient(test_client) 360 | to_test.add_encryption_layer(test_rest_client, self._json_config) 361 | 362 | assert not __oauth_warn.called 363 | 364 | def test_add_encryption_layer_missing_oauth_layer_warning(self): 365 | test_client = Mock() 366 | test_client.rest_client.request = None 367 | 368 | # no __oauth__ flag 369 | with self.assertWarns(UserWarning): 370 | to_test.add_encryption_layer(test_client, self._json_config) -------------------------------------------------------------------------------- /tests/test_api_encryption_jwe.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch, Mock 3 | import json 4 | from tests.utils.api_encryption_test_utils import MockApiClient, MockService, MockRestApiClient 5 | from tests import get_mastercard_config_for_test, JWE_TEST_CONFIG, get_jwe_config_for_test 6 | import client_encryption.api_encryption as to_test 7 | 8 | 9 | class ApiEncryptionJweTest(unittest.TestCase): 10 | 11 | def setUp(self): 12 | self._json_config = json.loads(get_mastercard_config_for_test()) 13 | self._jwe_json_config = json.loads(get_jwe_config_for_test()) 14 | self._json_config["paths"]["$"]["toEncrypt"] = {"data": "encryptedData"} 15 | self._json_config["paths"]["$"]["toDecrypt"] = {"encryptedData": "data"} 16 | 17 | def _set_header_params_config(self): 18 | self._json_config.update({ 19 | "useHttpHeaders": True, 20 | "ivFieldName": "x-iv", 21 | "encryptedKeyFieldName": "x-key", 22 | "encryptionCertificateFingerprintFieldName": "x-cert-fingerprint", 23 | "encryptionKeyFingerprintFieldName": "x-key-fingerprint", 24 | "oaepPaddingDigestAlgorithmFieldName": "x-oaep-digest" 25 | }) 26 | 27 | @patch('client_encryption.api_encryption.JweEncryptionConfig') 28 | def test_ApiEncryption_with_config_as_file_name(self, JweEncryptionConfig): 29 | to_test.ApiEncryption(JWE_TEST_CONFIG, "JWE") 30 | 31 | assert JweEncryptionConfig.called 32 | 33 | 34 | @patch('client_encryption.api_encryption.JweEncryptionConfig') 35 | def test_ApiEncryption_with_config_as_dict(self, JweEncryptionConfig): 36 | to_test.ApiEncryption(self._json_config, "JWE") 37 | 38 | assert JweEncryptionConfig.called 39 | 40 | def test_ApiEncryption_fail_with_config_as_string(self): 41 | self.assertRaises(FileNotFoundError, to_test.ApiEncryption, "this is not accepted") 42 | 43 | 44 | def test_encrypt_payload_returns_same_data_type_as_input(self): 45 | api_encryption = to_test.ApiEncryption(self._json_config, "JWE") 46 | 47 | test_headers = {"Content-Type": "application/json"} 48 | 49 | body = { 50 | "data": { 51 | "secret1": "test", 52 | "secret2": "secret" 53 | }, 54 | "encryptedData": {} 55 | } 56 | 57 | encrypted = api_encryption._encrypt_payload(body=body, headers=test_headers) 58 | self.assertIsInstance(encrypted, dict) 59 | 60 | encrypted = api_encryption._encrypt_payload(body=json.dumps(body), headers=test_headers) 61 | self.assertIsInstance(encrypted, str) 62 | 63 | encrypted = api_encryption._encrypt_payload(body=json.dumps(body).encode("utf-8"), headers=test_headers) 64 | self.assertIsInstance(encrypted, bytes) 65 | 66 | def test_encrypt_payload_with_params_in_body(self): 67 | api_encryption = to_test.ApiEncryption(self._json_config) 68 | 69 | test_headers = {"Content-Type": "application/json"} 70 | 71 | encrypted = api_encryption._encrypt_payload(body={ 72 | "data": { 73 | "secret1": "test", 74 | "secret2": "secret" 75 | }, 76 | "encryptedData": {} 77 | }, headers=test_headers) 78 | 79 | self.assertNotIn("data", encrypted) 80 | self.assertIn("encryptedData", encrypted) 81 | self.assertIn("encryptedValue", encrypted["encryptedData"]) 82 | self.assertEqual(6, len(encrypted["encryptedData"].keys())) 83 | self.assertDictEqual({"Content-Type": "application/json"}, test_headers) 84 | 85 | 86 | def test_decrypt_payload_with_params_in_body(self): 87 | api_encryption = to_test.ApiEncryption(self._json_config, "JWE") 88 | 89 | test_headers = {"Content-Type": "application/json"} 90 | 91 | decrypted = json.loads(api_encryption._decrypt_payload(body={ 92 | "encryptedData": { 93 | "iv": "uldLBySPY3VrznePihFYGQ==", 94 | "encryptedKey": "Jmh/bQPScUVFHSC9qinMGZ4lM7uetzUXcuMdEpC5g4C0Pb9HuaM3zC7K/509n7RTBZUPEzgsWtgi7m33nhpXsUo8WMcQkBIZlKn3ce+WRyZpZxcYtVoPqNn3benhcv7cq7yH1ktamUiZ5Dq7Ga+oQCaQEsOXtbGNS6vA5Bwa1pjbmMiRIbvlstInz8XTw8h/T0yLBLUJ0yYZmzmt+9i8qL8KFQ/PPDe5cXOCr1Aq2NTSixe5F2K/EI00q6D7QMpBDC7K6zDWgAOvINzifZ0DTkxVe4EE6F+FneDrcJsj+ZeIabrlRcfxtiFziH6unnXktta0sB1xcszIxXdMDbUcJA==", 95 | "encryptedValue": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiNzYxYjAwM2MxZWFkZTNhNTQ5MGU1MDAwZDM3ODg3YmFhNWU2ZWMwZTIyNmMwNzcwNmU1OTk0NTFmYzAzMmE3OSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24ifQ.Ita10bvCq9bKsJ_2Lvr6_4R1t-N5_B0FPxSOwwWbCMOSUELpAXTcT5mNKmvQuVi8poim73PyFFB_bASf_db2uQjhtz4xoGaD8u-Vsu5veBMJmT6cR0sW53fGLS-O5_4gvihN53TjgJnUFpaqf4O4-e9XnHJBgBxNkVZeYHIMyPL6XYWELaTs0J65TPynsI6iQBzN2UTnl5Zd1IWXQhh7FHYE93OXwesDWRv9L4bLIlNDCmXj_7UBaJn094iIpKLmwbRw56LezhGvxEHdHhVCwKgsSBHfMnA2QjpOv1L2A0H1ZSAtfdjOA0bJ3b-4GHw2LvFuwIoN7ylr-a7DdJdsXA.JivfdAdHKIa1eWZlsm4v5Q.WY58FSkcoCrnJH3PP-jx2ZXqt_e5Wwi8YJgh-vBVPQ1CES8yq88gPQ.hhdc0H3i-gsD8nOA5n1qvQ", 96 | "oaepHashingAlgo": "SHA256" 97 | } 98 | }, headers=test_headers)) 99 | 100 | self.assertNotIn("encryptedData", decrypted) 101 | self.assertDictEqual({"data": { 102 | "secret1": "test", 103 | "secret2": "secret" 104 | },}, decrypted) 105 | self.assertDictEqual({"Content-Type": "application/json"}, test_headers) 106 | 107 | def test_encrypt_payload_with_params_in_headers(self): 108 | self._set_header_params_config() 109 | 110 | test_headers = {"Content-Type": "application/json"} 111 | 112 | api_encryption = to_test.ApiEncryption(self._json_config, "JWE") 113 | encrypted = api_encryption._encrypt_payload(body={ 114 | "data": { 115 | "secret1": "test", 116 | "secret2": "secret" 117 | }, 118 | "encryptedData": {} 119 | }, headers=test_headers) 120 | 121 | self.assertNotIn("data", encrypted) 122 | self.assertIn("encryptedData", encrypted) 123 | self.assertIn("encryptedValue", encrypted["encryptedData"]) 124 | self.assertEqual(1, len(encrypted["encryptedData"].keys())) 125 | 126 | def test_decrypt_payload_with_params_in_headers(self): 127 | self._set_header_params_config() 128 | 129 | test_headers = {"Content-Type": "application/json", 130 | "x-iv": "uldLBySPY3VrznePihFYGQ==", 131 | "x-key": "Jmh/bQPScUVFHSC9qinMGZ4lM7uetzUXcuMdEpC5g4C0Pb9HuaM3zC7K/509n7RTBZUPEzgsWtgi7m33nhpXsUo8WMcQkBIZlKn3ce+WRyZpZxcYtVoPqNn3benhcv7cq7yH1ktamUiZ5Dq7Ga+oQCaQEsOXtbGNS6vA5Bwa1pjbmMiRIbvlstInz8XTw8h/T0yLBLUJ0yYZmzmt+9i8qL8KFQ/PPDe5cXOCr1Aq2NTSixe5F2K/EI00q6D7QMpBDC7K6zDWgAOvINzifZ0DTkxVe4EE6F+FneDrcJsj+ZeIabrlRcfxtiFziH6unnXktta0sB1xcszIxXdMDbUcJA==", 132 | "x-cert-fingerprint": "gIEPwTqDGfzw4uwyLIKkwwS3gsw85nEXY0PP6BYMInk=", 133 | "x-key-fingerprint": "dhsAPB6t46VJDlAA03iHuqXm7A4ibAdwblmUUfwDKnk=", 134 | "x-oaep-digest": "SHA256" 135 | } 136 | 137 | api_encryption = to_test.ApiEncryption(self._json_config, "JWE") 138 | decrypted = json.loads(api_encryption._decrypt_payload(body={ 139 | "encryptedData": { 140 | "encryptedValue": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiNzYxYjAwM2MxZWFkZTNhNTQ5MGU1MDAwZDM3ODg3YmFhNWU2ZWMwZTIyNmMwNzcwNmU1OTk0NTFmYzAzMmE3OSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24ifQ.Ita10bvCq9bKsJ_2Lvr6_4R1t-N5_B0FPxSOwwWbCMOSUELpAXTcT5mNKmvQuVi8poim73PyFFB_bASf_db2uQjhtz4xoGaD8u-Vsu5veBMJmT6cR0sW53fGLS-O5_4gvihN53TjgJnUFpaqf4O4-e9XnHJBgBxNkVZeYHIMyPL6XYWELaTs0J65TPynsI6iQBzN2UTnl5Zd1IWXQhh7FHYE93OXwesDWRv9L4bLIlNDCmXj_7UBaJn094iIpKLmwbRw56LezhGvxEHdHhVCwKgsSBHfMnA2QjpOv1L2A0H1ZSAtfdjOA0bJ3b-4GHw2LvFuwIoN7ylr-a7DdJdsXA.JivfdAdHKIa1eWZlsm4v5Q.WY58FSkcoCrnJH3PP-jx2ZXqt_e5Wwi8YJgh-vBVPQ1CES8yq88gPQ.hhdc0H3i-gsD8nOA5n1qvQ" 141 | } 142 | }, headers=test_headers)) 143 | 144 | self.assertNotIn("encryptedData", decrypted) 145 | self.assertDictEqual({"data": { 146 | "secret1": "test", 147 | "secret2": "secret" 148 | }}, decrypted) 149 | self.assertDictEqual({"Content-Type": "application/json", 150 | "x-iv": "uldLBySPY3VrznePihFYGQ==", 151 | "x-key": "Jmh/bQPScUVFHSC9qinMGZ4lM7uetzUXcuMdEpC5g4C0Pb9HuaM3zC7K/509n7RTBZUPEzgsWtgi7m33nhpXsUo8WMcQkBIZlKn3ce+WRyZpZxcYtVoPqNn3benhcv7cq7yH1ktamUiZ5Dq7Ga+oQCaQEsOXtbGNS6vA5Bwa1pjbmMiRIbvlstInz8XTw8h/T0yLBLUJ0yYZmzmt+9i8qL8KFQ/PPDe5cXOCr1Aq2NTSixe5F2K/EI00q6D7QMpBDC7K6zDWgAOvINzifZ0DTkxVe4EE6F+FneDrcJsj+ZeIabrlRcfxtiFziH6unnXktta0sB1xcszIxXdMDbUcJA==", 152 | "x-cert-fingerprint": "gIEPwTqDGfzw4uwyLIKkwwS3gsw85nEXY0PP6BYMInk=", 153 | "x-key-fingerprint": "dhsAPB6t46VJDlAA03iHuqXm7A4ibAdwblmUUfwDKnk=", 154 | "x-oaep-digest": "SHA256"}, test_headers) 155 | 156 | def test_decrypt_payload_with_params_in_headers_skip_decrypt(self): 157 | self._set_header_params_config() 158 | 159 | test_headers = {"Content-Type": "application/json", 160 | "x-key": "Jmh/bQPScUVFHSC9qinMGZ4lM7uetzUXcuMdEpC5g4C0Pb9HuaM3zC7K/509n7RTBZUPEzgsWtgi7m33nhpXsUo8WMcQkBIZlKn3ce+WRyZpZxcYtVoPqNn3benhcv7cq7yH1ktamUiZ5Dq7Ga+oQCaQEsOXtbGNS6vA5Bwa1pjbmMiRIbvlstInz8XTw8h/T0yLBLUJ0yYZmzmt+9i8qL8KFQ/PPDe5cXOCr1Aq2NTSixe5F2K/EI00q6D7QMpBDC7K6zDWgAOvINzifZ0DTkxVe4EE6F+FneDrcJsj+ZeIabrlRcfxtiFziH6unnXktta0sB1xcszIxXdMDbUcJA==", 161 | "x-cert-fingerprint": "gIEPwTqDGfzw4uwyLIKkwwS3gsw85nEXY0PP6BYMInk=", 162 | "x-key-fingerprint": "dhsAPB6t46VJDlAA03iHuqXm7A4ibAdwblmUUfwDKnk=", 163 | "x-oaep-digest": "SHA256" 164 | } 165 | 166 | api_encryption = to_test.ApiEncryption(self._json_config, "JWE") 167 | decrypted = api_encryption._decrypt_payload(body={ 168 | "data": { 169 | "key1": "notSecret", 170 | "key2": "anotherValue" 171 | }, 172 | }, headers=test_headers) 173 | 174 | self.assertDictEqual({"data": {"key1": "notSecret", "key2": "anotherValue"}}, json.loads(decrypted)) 175 | self.assertEqual(5, len(test_headers.keys())) 176 | 177 | @patch('client_encryption.api_encryption.JweEncryptionConfig') 178 | def test_add_header_encryption_layer_with_config_as_file_name(self, JweEncryptionConfig): 179 | to_test.add_encryption_layer(MockApiClient(), JWE_TEST_CONFIG, "JWE") 180 | 181 | assert JweEncryptionConfig.called 182 | 183 | @patch('client_encryption.api_encryption.JweEncryptionConfig') 184 | def test_add_header_encryption_layer_with_config_as_dict(self, JweEncryptionConfig): 185 | to_test.add_encryption_layer(MockApiClient(), self._json_config, "JWE") 186 | 187 | assert JweEncryptionConfig.called 188 | 189 | def test_add_header_encryption_layer_fail_with_config_as_string(self): 190 | self.assertRaises(FileNotFoundError, to_test.add_encryption_layer, MockApiClient(), "this is not accepted") 191 | 192 | def test_add_encryption_layer_post(self): 193 | secret1 = 435 194 | secret2 = 746 195 | test_client = MockApiClient() 196 | to_test.add_encryption_layer(test_client, self._json_config) 197 | response = MockService(test_client).do_something_post(body={ 198 | "data": { 199 | "secret1": secret1, 200 | "secret2": secret2 201 | } 202 | }, headers={"Content-Type": "application/json"}) 203 | 204 | self.assertIn("data", json.loads(response.data)) 205 | self.assertIn("secret", json.loads(response.data)["data"]) 206 | self.assertEqual(secret2-secret1, json.loads(response.data)["data"]["secret"]) 207 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 208 | 209 | def test_add_encryption_layer_delete(self): 210 | secret1 = 394 211 | secret2 = 394 212 | test_client = MockApiClient() 213 | to_test.add_encryption_layer(test_client, self._json_config) 214 | response = MockService(test_client).do_something_delete(body={ 215 | "data": { 216 | "secret1": secret1, 217 | "secret2": secret2 218 | } 219 | }, headers={"Content-Type": "application/json"}) 220 | 221 | self.assertEqual("OK", json.loads(response.data)) 222 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 223 | 224 | def test_add_encryption_layer_get(self): 225 | test_client = MockApiClient() 226 | to_test.add_encryption_layer(test_client, self._json_config) 227 | response = MockService(test_client).do_something_get(headers={"Content-Type": "application/json"}) 228 | json_res = json.loads(response.data) 229 | 230 | self.assertIn("data", json_res) 231 | self.assertIn("secret", json_res['data']) 232 | self.assertEqual([53, 84, 75], json_res["data"]["secret"]) 233 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 234 | 235 | def test_add_header_encryption_layer_post_no_oaep_algo(self): 236 | self._set_header_params_config() 237 | del self._json_config["oaepPaddingDigestAlgorithmFieldName"] 238 | 239 | secret1 = 435 240 | secret2 = 746 241 | test_client = MockApiClient() 242 | to_test.add_encryption_layer(test_client, self._json_config) 243 | response = MockService(test_client).do_something_post_use_headers(body={ 244 | "data": { 245 | "secret1": secret1, 246 | "secret2": secret2 247 | }, 248 | "encryptedData": {} 249 | }, headers={"Content-Type": "application/json"}) 250 | 251 | self.assertIn("data", json.loads(response.data)) 252 | self.assertIn("secret", json.loads(response.data)["data"]) 253 | self.assertEqual(secret2-secret1, json.loads(response.data)["data"]["secret"]) 254 | self.assertDictEqual({"Content-Type": "application/json", "x-oaep-digest": "SHA256"}, response.getheaders()) 255 | 256 | def test_add_header_encryption_layer_post_no_cert_fingerprint(self): 257 | self._set_header_params_config() 258 | del self._json_config["encryptionCertificateFingerprintFieldName"] 259 | 260 | secret1 = 164 261 | secret2 = 573 262 | test_client = MockApiClient() 263 | to_test.add_encryption_layer(test_client, self._json_config) 264 | response = MockService(test_client).do_something_post_use_headers(body={ 265 | "data": { 266 | "secret1": secret1, 267 | "secret2": secret2 268 | }, 269 | "encryptedData": {} 270 | }, headers={"Content-Type": "application/json"}) 271 | 272 | self.assertIn("data", json.loads(response.data)) 273 | self.assertIn("secret", json.loads(response.data)["data"]) 274 | self.assertEqual(secret2-secret1, json.loads(response.data)["data"]["secret"]) 275 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 276 | 277 | def test_add_header_encryption_layer_post_no_pubkey_fingerprint(self): 278 | self._set_header_params_config() 279 | del self._json_config["encryptionKeyFingerprintFieldName"] 280 | 281 | secret1 = 245 282 | secret2 = 854 283 | test_client = MockApiClient() 284 | to_test.add_encryption_layer(test_client, self._json_config) 285 | response = MockService(test_client).do_something_post_use_headers(body={ 286 | "data": { 287 | "secret1": secret1, 288 | "secret2": secret2 289 | }, 290 | "encryptedData": {} 291 | }, headers={"Content-Type": "application/json"}) 292 | 293 | self.assertIn("data", json.loads(response.data)) 294 | self.assertIn("secret", json.loads(response.data)["data"]) 295 | self.assertEqual(secret2-secret1, json.loads(response.data)["data"]["secret"]) 296 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 297 | 298 | def test_add_header_encryption_layer_no_secret_key(self): 299 | self._set_header_params_config() 300 | del self._json_config["encryptedKeyFieldName"] 301 | 302 | test_client = MockApiClient() 303 | 304 | self.assertRaises(KeyError, to_test.add_encryption_layer, test_client, self._json_config) 305 | 306 | def test_add_header_encryption_layer_post(self): 307 | self._set_header_params_config() 308 | 309 | secret1 = 445 310 | secret2 = 497 311 | test_client = MockApiClient() 312 | to_test.add_encryption_layer(test_client, self._json_config) 313 | response = MockService(test_client).do_something_post_use_headers(body={ 314 | "data": { 315 | "secret1": secret1, 316 | "secret2": secret2 317 | }, 318 | "encryptedData": {} 319 | }, headers={"Content-Type": "application/json"}) 320 | 321 | self.assertIn("data", json.loads(response.data)) 322 | self.assertIn("secret", json.loads(response.data)["data"]) 323 | self.assertEqual(secret2-secret1, json.loads(response.data)["data"]["secret"]) 324 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 325 | 326 | def test_add_header_encryption_layer_delete(self): 327 | self._set_header_params_config() 328 | 329 | secret1 = 783 330 | secret2 = 783 331 | test_client = MockApiClient() 332 | to_test.add_encryption_layer(test_client, self._json_config) 333 | response = MockService(test_client).do_something_delete_use_headers(body={ 334 | "data": { 335 | "secret1": secret1, 336 | "secret2": secret2 337 | }, 338 | "encryptedData": {} 339 | }, headers={"Content-Type": "application/json"}) 340 | 341 | self.assertEqual("OK", response.data) 342 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 343 | 344 | def test_add_header_encryption_layer_get(self): 345 | self._set_header_params_config() 346 | 347 | test_client = MockApiClient() 348 | to_test.add_encryption_layer(test_client, self._json_config) 349 | response = MockService(test_client).do_something_get_use_headers(headers={"Content-Type": "application/json"}) 350 | 351 | self.assertIn("data", json.loads(response.data)) 352 | self.assertIn("secret", json.loads(response.data)["data"]) 353 | self.assertEqual([53, 84, 75], json.loads(response.data)["data"]["secret"]) 354 | self.assertDictEqual({"Content-Type": "application/json"}, response.getheaders()) 355 | 356 | @patch('client_encryption.api_encryption.__oauth_warn') 357 | def test_add_encryption_layer_oauth_set(self, __oauth_warn): 358 | test_client = MockApiClient() 359 | test_rest_client = MockRestApiClient(test_client) 360 | to_test.add_encryption_layer(test_rest_client, self._json_config) 361 | 362 | assert not __oauth_warn.called 363 | 364 | def test_add_encryption_layer_missing_oauth_layer_warning(self): 365 | test_client = Mock() 366 | 367 | # no __oauth__ flag 368 | with self.assertWarns(UserWarning): 369 | to_test.add_encryption_layer(test_client, self._json_config) -------------------------------------------------------------------------------- /tests/test_encoding_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import client_encryption.encoding_utils as to_test 3 | from client_encryption.encryption_exception import EncodingError 4 | 5 | 6 | class EncodingUtilsTest(unittest.TestCase): 7 | 8 | def test_hex_encode(self): 9 | enc_one = to_test.encode_bytes(bytes(1), to_test.ClientEncoding.HEX) 10 | enc_string = to_test.encode_bytes(b"some data", to_test.ClientEncoding.HEX) 11 | enc_empty = to_test.encode_bytes(b"", to_test.ClientEncoding.HEX) 12 | 13 | self.assertEqual("00", enc_one, "Encoded bytes not matching") 14 | self.assertEqual("736f6d652064617461", enc_string, "Encoded bytes not matching") 15 | self.assertEqual("", enc_empty, "Encoded bytes not matching") 16 | 17 | def test_hex_decode(self): 18 | dec_one = to_test.decode_value("00", to_test.ClientEncoding.HEX) 19 | dec_string = to_test.decode_value("736f6d652064617461", to_test.ClientEncoding.HEX) 20 | dec_empty = to_test.decode_value("", to_test.ClientEncoding.HEX) 21 | 22 | self.assertEqual(bytes(1), dec_one, "Decoded value not matching") 23 | self.assertEqual(b"some data", dec_string, "Decoded value not matching") 24 | self.assertEqual(b"", dec_empty, "Decoded value not matching") 25 | 26 | def test_hex_decode_not_valid_hex(self): 27 | self.assertRaises(ValueError, to_test.decode_value, "736f6d65p064617461", to_test.ClientEncoding.HEX) 28 | 29 | def test_base64_encode(self): 30 | enc_one = to_test.encode_bytes(bytes(1), to_test.ClientEncoding.BASE64) 31 | enc_string = to_test.encode_bytes(b"some data", to_test.ClientEncoding.BASE64) 32 | enc_empty = to_test.encode_bytes(b"", to_test.ClientEncoding.BASE64) 33 | 34 | self.assertEqual("AA==", enc_one, "Encoded bytes not matching") 35 | self.assertEqual("c29tZSBkYXRh", enc_string, "Encoded bytes not matching") 36 | self.assertEqual("", enc_empty, "Encoded bytes not matching") 37 | 38 | def test_base64_decode(self): 39 | dec_one = to_test.decode_value("AA==", to_test.ClientEncoding.BASE64) 40 | dec_string = to_test.decode_value("c29tZSBkYXRh", to_test.ClientEncoding.BASE64) 41 | dec_empty = to_test.decode_value("", to_test.ClientEncoding.BASE64) 42 | 43 | self.assertEqual(bytes(1), dec_one, "Decoded value not matching") 44 | self.assertEqual(b"some data", dec_string, "Decoded value not matching") 45 | self.assertEqual(b"", dec_empty, "Decoded value not matching") 46 | 47 | def test_base64_decode_not_valid_base64(self): 48 | self.assertRaises(ValueError, to_test.decode_value, "c29tZS?kYXRh", to_test.ClientEncoding.BASE64) 49 | 50 | def test_encode_no_value(self): 51 | self.assertRaises(ValueError, to_test.encode_bytes, None, to_test.ClientEncoding.HEX) 52 | self.assertRaises(ValueError, to_test.encode_bytes, None, to_test.ClientEncoding.BASE64) 53 | 54 | def test_encode_not_a_byte_sequence(self): 55 | self.assertRaises(ValueError, to_test.encode_bytes, "not a byte sequence", to_test.ClientEncoding.HEX) 56 | self.assertRaises(ValueError, to_test.encode_bytes, "not a byte sequence", to_test.ClientEncoding.BASE64) 57 | 58 | def test_encode_invalid_encoding(self): 59 | self.assertRaises(EncodingError, to_test.encode_bytes, b"whatever", "ABC") 60 | 61 | def test_decode_no_value(self): 62 | self.assertRaises(ValueError, to_test.decode_value, None, to_test.ClientEncoding.HEX) 63 | self.assertRaises(ValueError, to_test.decode_value, None, to_test.ClientEncoding.BASE64) 64 | 65 | def test_decode_not_a_string(self): 66 | self.assertRaises(ValueError, to_test.decode_value, b"736f6d652064617461", to_test.ClientEncoding.HEX) 67 | self.assertRaises(ValueError, to_test.decode_value, b"736f6d652064617461", to_test.ClientEncoding.BASE64) 68 | 69 | def test_decode_invalid_encoding(self): 70 | self.assertRaises(EncodingError, to_test.decode_value, "whatever", "ABC") 71 | -------------------------------------------------------------------------------- /tests/test_encryption_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from tests import resource_path 3 | import client_encryption.encryption_utils as to_test 4 | from client_encryption.encryption_exception import CertificateError, PrivateKeyError, HashAlgorithmError 5 | from cryptography import x509 6 | from Crypto.PublicKey import RSA 7 | from Crypto.Hash import SHA224, SHA384, SHA512 8 | 9 | 10 | class EncryptionUtilsTest(unittest.TestCase): 11 | 12 | _pkcs1_512 = "MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA6Jj4PJOePIizjVn1J+ogRX7QFS4OoLiJx4Ehj3zJNMa2UiRvbswMmMEBbZ+GvHIbwJvvhKymZwPi7+bGubrjrQIDAQABAkBtvBWJRr+loXzMWD+ACEYXY1+6TlNaYhWmiPaTYnTur1b0l5jmwl8u8XDCwxz+9joU8cZI7Q/ixDcxAejXPIBFAiEA+KvX+Voe5qVZ3mywAmu/OgrsS1VKeNu5TW1UoFMvCe8CIQDvc9qrVTTA7XYPJ9FwT5RDU3C9E4tWfRdAD8wAYyP4IwIgOPAwDZpDBRDLWRCN5KADMykZHc6ztKSq8z2baPJjDOUCIGsEUlSEjkEzX7JCT35scozse9RlKb8LxRpidQAvTuIVAiAIsiF8nnE+JQPq07WOfQpq5/UM6XCENALXTOk6K0zzGg==" 13 | _pkcs1_2048 = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDMJPwAG210B9bznVVU0xKSmBxZnXIVODE275yg+kvxSDU5mBFg6CD59yP8DwxNiz/JeAgmEjWChdUX/01k9+vKsE/F4Ug4l74IZ48YyBe/cXuj25XTXTNzIiaAqe2/c2ssJXD22vMghoo2C/CCY8OF6AexalUyvsuTYMvlCY8Tbwnx6Qmdh8cnMZRKUlgmkKgJW07ts88MJxaCuU7OhqBO6O2nIFFYA9EmgDUnZvu6/Ouqa5QZ/xiqDwwh43tS7GziNKTeuwNcwITlwJUpr5t+hNXVdFAnY8AF026/Af/CqixGDdhOAV1YzjoBjgOAjPOpj/FJ4uJt3UfODbAdBMzrAgMBAAECggEBAMuaO2eT81cdFopEKb3/AfAJG4VZXVXChHspAYsf96v+e28ktnhzK6iCj3YuP/P65LR4LZBi6tFxzzUu4K7KAXZW4EkYReKDnPle574smlrKwSiMseJrewviTIpYcJFYeNQ/x2m8t22CIciDoe05uOENqNaOmciRuBIWIWUeYn9aoDsiMQ56EaKpOOt/Jekvyttwa35yElvbPSxF2UAGOXUxPaz1wIdkvDPrHV4NAMNjLDalFGYR10xnlVa0B3fsjxFaXY0esyyCBxonMgDkud12xGqYxXDv7WoUggrRkc5OSe++BDz5Ts/6vy/v7ea5+9fglqQ+KeCPnjeyVs4ZqkECgYEA7UXhER4p8KkYyjuRZmwABICQRv7kVy7iOsTNmR/aOTlYLjORaWZICprLVYsgQY/bksKjSjy9MR/HS0426QkyYQl7BWLnnU3HP5yVytuIlFxfo/xFMJ5wm1CNQ6rAcO8o02lwATzPPRg4ui0nGEIflJPdoTTuxzXn0r0QYzN52uECgYEA3EG9uCEzE3uVO5K/Ew7A1A5aAp9bNX59NctDtKAWEgKoRXrudgebSv+P2U3ZW3G6HouGpnavWSHMQ6HIfPtgEg0BhSqOOgUBVR+wdntq4zux1AFnHVXBLZdE+CWCmyj3ASFMTPvkLssfj/ae7UEhUB24TZxz3nAo8RR7Gmz8TUsCgYEAimnEVK8K+kg6nObI+D2yeO3ivHe/DpjcAjqCUXxCWjV4mmMcxaaUChOo4Dsr0vMvvNpsVUc/eqO2J9j1sVXbHL5iFI9Q2/Pect5Oh6svbpTAejIUzrrup7wC3GGEp5zsbP/KBf7KSjKSDRGAB+ey8oKbvInbbTymAsql/6iswiECgYEAuukzFZFe5banMpHaglKvwoSXT8hpv2Ci4samox6C/C+zGpsyx4i2+RMcwHy26kn9drRSxOrM7OeojvA40g8EPO06kAZIAeaDdfhZaIJdd44N32p9VcCTGZxYE/jI9+Dwk83tERtlTWxkUWgpAA+YNIO0BnCxR1+I7uTBfvBjvzcCgYBDrjptyo8peY0Jzaijg7hWBwLZp/M27GhR8uAV6FVESP+1uG06k3g3CECxCE3Pi6HVSaW6UpNMZnrtVaKQCJDyKnkdIExFVP8DhkJSHmid1TXJXEfpDT57JD4UX6NOCcB0ynSyYvDvJ6bodx6SSyB03CEMqJ8VMjXeYpZSHyAF7A==" 14 | _pkcs8_2048 = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD0ynqAQWn0T7/VJLletTJgoxsTt5TR3IkJ+Yk/Pxg6Q5hXuiGrBdC+OVo/9hrNnptuZh9rZYKto6lbSjYFiKMeBDvPZrYDPzusp0C0KllIoVbzYiOezD76XHsQAEje0UXbzZlXstPXef2bi2HkqV26ST167L5O4moK8+7jHMT80T6XgsUyvyt8PjsQ9CSu6fnD9NfCSYmt2cb16OXcEtA7To2zoGznXqB6JhntFjG0jxee7RkLR+moOqMI9kFM5GSIV4uhwQ9FtOCjUf7TFAU12wwfX/QXUEj6G93GVtzf6QdkVkWh4EyRHeMLyMNc5c0Iw1ZvXdOKfoeo9F47QpbzAgMBAAECggEAK3dMmzuCSdxjTsCPnc6E3H35z914Mm97ceb6RN26OpZIFcO6OLj2oOBkMxlLFxnDta2yhIpo0tZNuyUJRKBHfov35tLxHNB8kyK7rYIbincDjoHtm0PfJuuG+odiaRY11lrCkLzzOr6xlo4AWu7r8qkQnqQtAqrXc4xu7artG4rfMIunGnjjWQGzovtey1JgZctO97MU4Wvw18vgYBI6JM4eHJkZxgEhVQblBTKZs4OfiWk6MRHchgvqnWugwl213FgCzwy9cnyxTP13i9QKaFzL29TYmmN6bRWBH95z41M8IAa0CGahrSJjudZCFwsFh413YWv/pdqdkKHg1sqseQKBgQD641RYQkMn4G9vOiwB/is5M0OAhhUdWH1QtB8vvhY5ISTjFMqgIIVQvGmqDDk8QqFMOfFFqLtnArGn8HrKmBXMpRigS4ae/QgHEz34/RFjNDQ9zxIf/yoCRH5PmnPPU6x8j3bj/vJMRQA6/yngoca+9qvi3R32AtC5DUELnwyzNwKBgQD5x1iEV+albyCNNyLoT/f6LSH1NVcO+0IOvIaAVMtfy+hEEXz7izv3/AgcogVZzRARSK0qsQ+4WQN6Q2WG5cQYSyB92PR+VgwhnagVvA+QHNDL988xoMhB5r2D2IVSRuTB2EOg7LiWHUHIExaxVkbADODDj7YV2aQCJVv0gbDQJQKBgQCaABix5Fqci6NbPvXsczvM7K6uoZ8sWDjz5NyPzbqObs3ZpdWK3Ot4V270tnQbjTq9M4PqIlyGKp0qXO7ClQAskdq/6hxEU0UuMp2DzLNzlYPLvON/SH1czvZJnqEfzli+TMHJyaCpOGGf1Si7fhIk/f0cUGYnsCq2rHAU1hhRmQKBgE/BJTRs1MqyJxSwLEc9cZLCYntnYrr342nNLK1BZgbalvlVFDFFjgpqwTRTT54S6jR6nkBpdPmKAqBBcOOX7ftL0b4dTkQguZLqQkdeWyHK8aiPIetYyVixkoXM1xUkadqzcTSrIW1dPiniXnaVc9XSxtnqw1tKuSGuSCRUXN65AoGBAN/AmT1S4PAQpSWufC8NUJey8S0bURUNNjd52MQ7pWzGq2QC00+dBLkTPj3KOGYpXw9ScZPbxOthBFzHOxERWo16AFw3OeRtn4VB1QJ9XvoA/oz4lEhJKbwUfuFGGvSpYvg3vZcOHF2zlvcUu7C0ub/WhOjV9jZvU5B2Ev8x1neb" 15 | _pkcs1_1024 = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMe6XwDswzfKfHVdHGYsJFnUuvrpCmlY/lloSi2xA+kW3KrfD/F0tZl3lddBwhv/BRBZxvEFZgk8yT8ZVDem+QgoSoL6HTxV1e19sGUz5ETw5hpBbk4WDtiMeAv9a8KRrpUn33RG5f1l2P1NS7wbKoRGWX9dQMAKzzlq1tJbo8FBAgMBAAECgYEAv1pyzQsJmfk4tqUHoWgnR52FqwD8xaPKxFQjxZTz3yzVLCxcNQSRPECTYdGRrIrvChRJgv+eG7mOQhL9WfhyW8ftdGRyHbZyMACzigZldRSVWtxcUOJHSjY6oAheHbgRIz+2kNaaf83fRp+ffVMEArGBjkSPZKuYrmCNJ4wYj3ECQQD6A8z8/6MuLDLC6JLhBFjF4MW2W5pp+XtQdH8jq8v1Vqo6m3BXV78mXK0o0wLCgypp13vf38EBf12o2CmeIcL1AkEAzIJkEBzw8iN8zWHix4QN5vabGXVjYOZwNRVYi34ThmzebUjn3B5WBj8HBfhQgSWGCxWM2jqpdg9taKqXj+3NnQJBAIsnpGfI7Y1cgmBjzKS7o6F3qvQF5ltHxfAQ91bmXx5Nv2/hZlTm/PigKq0HTYjwMqI4krUXuDhaKMo6jmd6iSUCQG0b3/Dsz+wP3OTipZBWtugTh9pEU9n9972KfuwlPpuId/8MV4+Lq8+3TrLzsVfFwkclnzlK9OBlCLU/1o7Wi80CQBXGAmr3gIoJmM6KSznFOujpXSlh2zHS264iXH+8ZUYfdtyalKb9LnidKi/NsQM3uAqvgJJKwBCoLqg/AlJA3qY=" 16 | _pkcs1_4096 = "MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDTTnhssIYSVAbliV1caK7+e8Sckp69X/zXEj50AxIzNLa93Y7HEPjBm2os9MH/H7fc2yvKZ5uhJytBefSDQb0F1VepeotkhSI7Qj10e9vzYYJlNoC/X0BdjblP4ifjZpUBGow07m5WUtZVqTvhcyYPxYpk/d5T3B0oxerh6E16GIhrXbUc02I+1NVW9ndMDDVXFR4RrG0Mu+moPpY71gEp9ZzwZd2u2oVVZtthSXuLOXOP3jfP1XCQmCyPRvqq3nwBzKI4BnnpWgVTHk3mnTQQNxqZNhFO8kUAVAf796PBxOwObazjEISPNn/sNn0VHCi881kgffT4m1T6V34GFn+ZZJ10jlmHVyivS4x4Y2emDh4skjp8n78mM52SGhlpxjrkMejMKzLlo5xyZT0GudxAYbj4fWU3wv6A6kdnt79fqGcfG2A1ucihJ3tSjnIi9SILHF8/II//KM6qd8O11HwXoaQyePMJcaeDIi+/bmXPin6A90OPo8VJUcmRExLkuvL5zsA5gUfMnzUrwEA09uLBPwP3HXOczoZUZ8frztT+xtGJYumZqoa8QGVeEaCZDnGqHBAIH0Oz0NbN0attd24KguUMiSEzy0Nd0ZgwNI2us6IPgk5k/UGF7/r7Mw0+W6x3fPjh4JIReksKLF2I82+XW82iXK2cmLflVmshc9F6ywIDAQABAoICAGZABZQKw4Xg9UljdDMAURXCDHz7kibvaOkl4eS/i7INoxQzPyDi/IyrSPCiK7Hpxk3wHLuf8TZFPvA7NG/DgtVszDOtogAtUEg+oIBaYy/dknypqBly3TGoH/Eg7c5jWF0lXI1Mh0XsZd0jvF83Gkbtfy+pjUklLVMKmsNgZwVbE8Boovhk7Zib4vRm5Yl+Kt2N0XQZ7NcJo2KPjqE7yGpmeN/3WxN+mKCf2i60oTzYuyv2CyneDc9aY82dOjUu3fD89c6Ii7u4nPUAqA+dKJFkKNHU0QFTyMIE56wsxChCwzukJNHnr02hwLssDtEFRE1SsGFBsiYO/Er7xXbqsilF3w1tjhS5NjnAVVHSqAa1FkP2hyDUWKPmcRrGe/nyEgebzHyc4BCO0uMOP89hGOBp9prFz72rvhvPfS2p/m4eg3MlVVYl3NQ3WF4k613GMU4Cu2RUVImzWavejTDCttEOtWC4BcDfPphkYEch+AHWHPXdMUxzF9FZVU5yllSbDHa5dOY45f6IFSVw+6VKdTGZX3AHbX0Xao9mpJAbqUPTkv8ZQaN4HnqytWE3BiALsc7qUmQEBv0kO9h/FH2QpDk+l6f++ZriR9NwuVtyIwTdL32Z5F862aUYUZ3JpKOI8RjJiVCXKlJU9/riEMxS7UK7J8gz0IEnoE6rs/vf6p/5AoIBAQDsqRJxT4XArRAPNPeiGXHKcwJQ8jnb92XYb+tERFWwHdP5b5bVivG6s677mGNIf7RvR2duze3lQp8GrqfQJE0Y9dqFrwEOryvgrSum9FSfdemuTjVzapXTGjcg1u4Jl3WV2kW7cmMYGYgk0LavnxlrtQlhoHXRY10ms88trs9Qqt5l5IF4CoUGoebLWMDadCIXkZK0tj84hrxzHLtrRkJr5tZUReTq0+2Cck0VT9TYhDjo/aV02zAJvAkyi4bihYJBDs4OlAAmp4Laj2YEOyZLJQsmvs3K15UIAIYGbPpBWnqbGgBrzSN30rKJJBmZlMMUC+46mRKhxz8rsELM40RlAoIBAQDkkv7TerHUhQr1Fg8KFmN+ufxO/x0o4h822jzja5GYfbVuhHU7l8e/IYeHz43oCjQ94UpvsJFBKdcTRblIsRXD+RjLMTKj/OOmKvsJWdyw4bzdG3Ng62cn+MP3AhjdSyNepf/HoSyiWxY+DMnX1PHSoCL5+ckBf7r3Py/NeWa7w49i64b5Qyk1+0k75nE6dzGyI3DfvzW/2ToYp9+O4x7lbrnmibUCVBZoQJQBZfrrM9YvJWrbE6PjUVTiT7EypQxEQFfJ5f2+mKDtlUvutMgx0bbgLKuia1y6wENPG55xxYOdFbtLRQ8UdgCmKD7lBKvVj3M32IVYnMIhLt+fHtdvAoIBAQDoKEthQKNzCdKxOEKcj91ivkPNaHF96UnNcq0WgPqWFAy3qtIKlYzgnBfR1AHBQxWb/C3pMrZmql4aTgdtVc8T50oAzlZ5MzoV44ro8tJ4w1EDntEscaicOCFQY8eLPTsqT78MTQAMyi8V/nDYA3kKgcO8M46gY5sjtkxNAil5dsVIq0qemaM5iGVBocU+B6LSAAosoIOQTgxb2Bm6+Gd8NiyW6yHPgyqBRuN7Pp8L71lspjZDVinfwsg6/0jIujO24nEdN+6V/MmO4hEvoV7FZId18MMscyNNgEX507oDlmCj+nPjTI+4ocZmZjV+xcJAzhta/IKclTrQ/s3o9CFJAoIBAA14NBgWROB3LpwevqgjhZ10LFAjbH56RMigt7HXJu9LiSOIHrhyS00SFsCEFKK15zo2SoKmbwjeBFSYaUDFXrj7tuBSd99+CZRDOCPm7cBmrRCCi5wMgzmxySqHLxvrT5xGP/ptnwm6QGdLraFQZSe6VBVTCOtTsZrcWF7NZqZt8ccshfmuYYFS24/yFn4RhBYxTHeC9xHlJS1KxzvOWoW+rqnNN6N5fRCLra0fa1fs6BRDKjbaw2+j3VKwy4pme4CSIho9uWQ/7kvR3nWkpZ958kLnnv2lQgOnTNAemomt0AGzxCO9YBQemA8yk/I/AjlFiM6jrcf5BN+buYXQ9XUCggEADhFb4/330A164j1SPwYN+hO/enTvHL3tba8T6wkdScrYjWSWC/K6Y3tZf2TMgxeT6jHGGgjU5km5MJDzsDH2Xi6E6VpTcQDHQKcMZTwfK4dz9FLRkmlfiPDsjlrLfrsec2mgp7EIFW8pRO3pMVwMG1kytZ+7CnEpvi58LBKFBiojhxZOcsrh2tsH67hlnSX7LfTDUMthnzCmeaN93aWy5EfLlmugacyPuS6cX0la5/B3j4IuEktL62pSSLSr4chIM+KOc9i4mV+ZhRoKMM0mKyP+U2D19inwXQeV8sKPNWLxA7NugwXxKMtkj2B9GhjnEyD24/9njsz4cZ4V+0C6JQ==" 17 | _pkcs12 = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCYoc5Ue4MKxHIQeSESKQiIv341EFDtfAlAsXP74modJuwnSLOfSkFNgKH4y6vSKiUK7BxU2KFy7FkRJ9/vceJmP9MD6bWPgT2Wg4iSQxgPtAHEVps9MYvkhW0lt0hyhAcGLUR3kb4YjSkGfa8EzG/G2g+/VKdL0mnSgWhCnSBnR0xRwWccgdRTLm20/jzXkmHD92DBR7kDgiBUrPWTfLHDnsVoIUut6BAPI83TIjHjVG1Jn8K0prbGeQU9ALwaL36qvppYpmCqaAGHOM2fXsEPFNhEZxQpbyW2M4PtXHnjSqlNOKN2tmdF3jWwm9hKZ9xeaWJkBmBnLe3tNz0OdO0pAgMBAAECggEBAJHQGn5JFJJnw5SLM5XWz4lcb2SgNr/5/BjqriQXVEqPUZHh+X+Wf7ZbyeEWKgp4KrU5hYNlBS/2LMyf7GYixSfrl1qoncP/suektwcLw+PUks+P8XRPbhadhP1AEJ0eFlvHSR51hEaOLIA/98C80ZgF4H9njv93f5MT/5eL5lXipFX1dcxUB55q9QOtQ7uCg++NyG5F6u4FxbNtOtsjyNzWZSjYsjSyGHDip9ScDOPNsGQfznxo/oifdXvc25BgWvRflIIYEP08eeUSuGW2nUnx+Joc0oZTkC0wfU+aqKlaZp8zfOEIm0gUDgWtgnq5I5JHJMuW6BtA4K3E+nyP0lECgYEAzIbNx/lVxmFPbPp+AG9LD3JLycjdmTzwpHK44MsaUBOZ9PkLZs0NpR5z0/qcFb8YGGz3qN6E/TTydmfXCpZ3bxP3+x81gL9SVG/y2GP/ky/REA0jFycwVlONeVnd09xPNNLZLUgZhWyAQIA2pmVMh8W+pX6ojxGgOe+KIGutJCUCgYEAvwuNciTzkjBz9nFCjLONvP05WMdIAXo1uxd17iQ0lhRtmHbphojFAPcHYocm2oUXJo5nLvy+u8xnxbyXaZHmRqm98AzmBTtpphFtgfTtv/cSvOsBpdyyaJaN12IUs2XYACGBRa2DUkgxxvHtbmjFGFIU+5VgjOG8g0LfoPhLM7UCgYAmdRaOioihY7zOjg9RP5wKjIBJsfZREQ9irJus0SPieL0TPhzxuI7fRGmdK1tcD3GVbi/nVegFwIXy07WwrPhKL6QKWSTzT4ZIkEBGhg8RewVBkmbNvLWvFcjdT5ORebR/B0KE7DC4UN2Qw0sDYLrSMNGXRsilFjhdjHgZfoWw7QKBgAZrQvNk3nI5AoxzPcMwfUCuWXDsMTUrgAarQSEhQksQoKYQyMPmcIgZxLvAwsNw2VhITJs9jsMMmSgBsCyx5ETXizQ3mrruRhx4VW+aZSqgCJckZkfGZJAzDsz/1KY6c8l9VrSaoeDv4AxJMKsXBhhNGbtiR340T3sxkgX8kbpJAoGBAII2aFeQ4oE8DhSZZo2bpJxO072xy1P9PRlyasYBJ2sNiF0TTguXJB1Ncu0TM0+FLZXIFddalPgv1hY98vNX22dZWKvD3xJ7HRUx/Hyk+VEkH11lsLZ/8AhcwZAr76cE/HLz1XtkKKBCnnlOLPZN03j+WKU3p1fzeWqfW4nyCALQ" 18 | 19 | def test_load_encryption_certificate_pem(self): 20 | cert_path = resource_path("certificates/test_certificate-2048.pem") 21 | cert, type = to_test.load_encryption_certificate(cert_path) 22 | 23 | self.assertIsNotNone(cert) 24 | self.assertIsInstance(cert, x509.Certificate, "Must be X509 certificate") 25 | 26 | def test_load_encryption_certificate_der(self): 27 | cert_path = resource_path("certificates/test_certificate-2048.der") 28 | cert, type = to_test.load_encryption_certificate(cert_path) 29 | 30 | self.assertIsNotNone(cert) 31 | self.assertIsInstance(cert, x509.Certificate, "Must be X509 certificate") 32 | 33 | def test_load_encryption_certificate_invalid(self): 34 | cert_path = resource_path("keys/test_invalid_key.der") 35 | 36 | self.assertRaises(CertificateError, to_test.load_encryption_certificate, cert_path) 37 | 38 | def test_load_encryption_certificate_file_does_not_exist(self): 39 | cert_path = resource_path("certificates/non_existing_file.pem") 40 | 41 | self.assertRaises(CertificateError, to_test.load_encryption_certificate, cert_path) 42 | 43 | def test_load_decryption_key_pkcs8_pem(self): 44 | key_path = resource_path("keys/test_key_pkcs8-2048.pem") 45 | key = to_test.load_decryption_key(key_path) 46 | 47 | self.assertIsNotNone(key) 48 | self.assertIsInstance(key, RSA.RsaKey, "Must be RSA key") 49 | self.assertEqual(self._pkcs8_2048, self.__strip_key(key), "Decryption key does not match") 50 | 51 | def test_load_decryption_key_pkcs8_der(self): 52 | key_path = resource_path("keys/test_key_pkcs8-2048.der") 53 | key = to_test.load_decryption_key(key_path) 54 | 55 | self.assertIsNotNone(key) 56 | self.assertIsInstance(key, RSA.RsaKey, "Must be RSA key") 57 | self.assertEqual(self._pkcs8_2048, self.__strip_key(key), "Decryption key does not match") 58 | 59 | def test_load_decryption_key_pkcs1_pem(self): 60 | key_path = resource_path("keys/test_key_pkcs1-2048.pem") 61 | key = to_test.load_decryption_key(key_path) 62 | 63 | self.assertIsNotNone(key) 64 | self.assertIsInstance(key, RSA.RsaKey, "Must be RSA key") 65 | self.assertEqual(self._pkcs1_2048, self.__strip_key(key), "Decryption key does not match") 66 | 67 | def test_load_decryption_key_pkcs1_512bits_pem(self): 68 | key_path = resource_path("keys/test_key_pkcs1-512.pem") 69 | key = to_test.load_decryption_key(key_path) 70 | 71 | self.assertIsNotNone(key) 72 | self.assertIsInstance(key, RSA.RsaKey, "Must be RSA key") 73 | self.assertEqual(self._pkcs1_512, self.__strip_key(key), "Decryption key does not match") 74 | 75 | def test_load_decryption_key_pkcs1_1024bits_pem(self): 76 | key_path = resource_path("keys/test_key_pkcs1-1024.pem") 77 | key = to_test.load_decryption_key(key_path) 78 | 79 | self.assertIsNotNone(key) 80 | self.assertIsInstance(key, RSA.RsaKey, "Must be RSA key") 81 | self.assertEqual(self._pkcs1_1024, self.__strip_key(key), "Decryption key does not match") 82 | 83 | def test_load_decryption_key_pkcs1_4096bits_pem(self): 84 | key_path = resource_path("keys/test_key_pkcs1-4096.pem") 85 | key = to_test.load_decryption_key(key_path) 86 | 87 | self.assertIsNotNone(key) 88 | self.assertIsInstance(key, RSA.RsaKey, "Must be RSA key") 89 | self.assertEqual(self._pkcs1_4096, self.__strip_key(key), "Decryption key does not match") 90 | 91 | def test_load_decryption_key_pkcs12(self): 92 | key_path = resource_path("keys/test_key.p12") 93 | key_password = "Password1" 94 | p12_key = to_test.load_decryption_key(key_path, key_password) 95 | 96 | self.assertIsNotNone(p12_key) 97 | self.assertIsInstance(p12_key, RSA.RsaKey, "Must be RSA key") 98 | self.assertEqual(self._pkcs12, self.__strip_key(p12_key), "Decryption key does not match") 99 | 100 | def test_load_decryption_key_invalid_key(self): 101 | key_path = resource_path("keys/test_invalid_key.der") 102 | 103 | self.assertRaises(PrivateKeyError, to_test.load_decryption_key, key_path) 104 | 105 | def test_load_decryption_key_file_does_not_exist(self): 106 | key_path = resource_path("keys/non_existing_file.pem") 107 | 108 | self.assertRaises(PrivateKeyError, to_test.load_decryption_key, key_path) 109 | 110 | def test_load_hash_algorithm(self): 111 | hash_algo = to_test.load_hash_algorithm("SHA224") 112 | 113 | self.assertEqual(hash_algo, SHA224) 114 | 115 | def test_load_hash_algorithm_dash(self): 116 | hash_algo = to_test.load_hash_algorithm("SHA-512") 117 | 118 | self.assertEqual(hash_algo, SHA512) 119 | 120 | def test_load_hash_algorithm_lowercase(self): 121 | hash_algo = to_test.load_hash_algorithm("sha384") 122 | 123 | self.assertEqual(hash_algo, SHA384) 124 | 125 | def test_load_hash_algorithm_not_supported(self): 126 | self.assertRaises(HashAlgorithmError, to_test.load_hash_algorithm, "MD5") 127 | 128 | def test_load_hash_algorithm_underscore(self): 129 | self.assertRaises(HashAlgorithmError, to_test.load_hash_algorithm, "SHA_512") 130 | 131 | def test_load_hash_algorithm_none(self): 132 | self.assertRaises(HashAlgorithmError, to_test.load_hash_algorithm, None) 133 | 134 | def test_validate_hash_algorithm(self): 135 | hash_algo = to_test.validate_hash_algorithm("SHA224") 136 | 137 | self.assertEqual(hash_algo, "SHA224") 138 | 139 | def test_validate_hash_algorithm_dash(self): 140 | hash_algo = to_test.validate_hash_algorithm("SHA-512") 141 | 142 | self.assertEqual(hash_algo, "SHA512") 143 | 144 | def test_validate_hash_algorithm_lowercase(self): 145 | hash_algo = to_test.validate_hash_algorithm("sha384") 146 | 147 | self.assertEqual(hash_algo, "SHA384") 148 | 149 | def test_validate_hash_algorithm_not_supported(self): 150 | self.assertRaises(HashAlgorithmError, to_test.validate_hash_algorithm, "MD5") 151 | 152 | def test_validate_hash_algorithm_underscore(self): 153 | self.assertRaises(HashAlgorithmError, to_test.validate_hash_algorithm, "SHA_512") 154 | 155 | def test_validate_hash_algorithm_none(self): 156 | self.assertRaises(HashAlgorithmError, to_test.validate_hash_algorithm, None) 157 | 158 | @staticmethod 159 | def __strip_key(rsa_key): 160 | return rsa_key.export_key(pkcs=8).decode('utf-8').replace("\n", "")[27:-25] 161 | -------------------------------------------------------------------------------- /tests/test_field_level_encryption_config.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from tests import resource_path, get_mastercard_config_for_test 3 | import json 4 | import client_encryption.field_level_encryption_config as to_test 5 | from client_encryption.encryption_utils import load_encryption_certificate 6 | from client_encryption.encoding_utils import ClientEncoding 7 | from client_encryption.encryption_exception import HashAlgorithmError, PrivateKeyError, CertificateError 8 | from Crypto.PublicKey import RSA 9 | 10 | 11 | class FieldLevelEncryptionConfigTest(unittest.TestCase): 12 | 13 | def setUp(self): 14 | self._test_config_file = get_mastercard_config_for_test() 15 | self._expected_cert, cert_type = load_encryption_certificate(resource_path("certificates/test_certificate-2048.der")) 16 | self._expected_key = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD0ynqAQWn0T7/VJLletTJgoxsTt5TR3IkJ+Yk/Pxg6Q5hXuiGrBdC+OVo/9hrNnptuZh9rZYKto6lbSjYFiKMeBDvPZrYDPzusp0C0KllIoVbzYiOezD76XHsQAEje0UXbzZlXstPXef2bi2HkqV26ST167L5O4moK8+7jHMT80T6XgsUyvyt8PjsQ9CSu6fnD9NfCSYmt2cb16OXcEtA7To2zoGznXqB6JhntFjG0jxee7RkLR+moOqMI9kFM5GSIV4uhwQ9FtOCjUf7TFAU12wwfX/QXUEj6G93GVtzf6QdkVkWh4EyRHeMLyMNc5c0Iw1ZvXdOKfoeo9F47QpbzAgMBAAECggEAK3dMmzuCSdxjTsCPnc6E3H35z914Mm97ceb6RN26OpZIFcO6OLj2oOBkMxlLFxnDta2yhIpo0tZNuyUJRKBHfov35tLxHNB8kyK7rYIbincDjoHtm0PfJuuG+odiaRY11lrCkLzzOr6xlo4AWu7r8qkQnqQtAqrXc4xu7artG4rfMIunGnjjWQGzovtey1JgZctO97MU4Wvw18vgYBI6JM4eHJkZxgEhVQblBTKZs4OfiWk6MRHchgvqnWugwl213FgCzwy9cnyxTP13i9QKaFzL29TYmmN6bRWBH95z41M8IAa0CGahrSJjudZCFwsFh413YWv/pdqdkKHg1sqseQKBgQD641RYQkMn4G9vOiwB/is5M0OAhhUdWH1QtB8vvhY5ISTjFMqgIIVQvGmqDDk8QqFMOfFFqLtnArGn8HrKmBXMpRigS4ae/QgHEz34/RFjNDQ9zxIf/yoCRH5PmnPPU6x8j3bj/vJMRQA6/yngoca+9qvi3R32AtC5DUELnwyzNwKBgQD5x1iEV+albyCNNyLoT/f6LSH1NVcO+0IOvIaAVMtfy+hEEXz7izv3/AgcogVZzRARSK0qsQ+4WQN6Q2WG5cQYSyB92PR+VgwhnagVvA+QHNDL988xoMhB5r2D2IVSRuTB2EOg7LiWHUHIExaxVkbADODDj7YV2aQCJVv0gbDQJQKBgQCaABix5Fqci6NbPvXsczvM7K6uoZ8sWDjz5NyPzbqObs3ZpdWK3Ot4V270tnQbjTq9M4PqIlyGKp0qXO7ClQAskdq/6hxEU0UuMp2DzLNzlYPLvON/SH1czvZJnqEfzli+TMHJyaCpOGGf1Si7fhIk/f0cUGYnsCq2rHAU1hhRmQKBgE/BJTRs1MqyJxSwLEc9cZLCYntnYrr342nNLK1BZgbalvlVFDFFjgpqwTRTT54S6jR6nkBpdPmKAqBBcOOX7ftL0b4dTkQguZLqQkdeWyHK8aiPIetYyVixkoXM1xUkadqzcTSrIW1dPiniXnaVc9XSxtnqw1tKuSGuSCRUXN65AoGBAN/AmT1S4PAQpSWufC8NUJey8S0bURUNNjd52MQ7pWzGq2QC00+dBLkTPj3KOGYpXw9ScZPbxOthBFzHOxERWo16AFw3OeRtn4VB1QJ9XvoA/oz4lEhJKbwUfuFGGvSpYvg3vZcOHF2zlvcUu7C0ub/WhOjV9jZvU5B2Ev8x1neb" 17 | 18 | def test_load_config_as_string(self): 19 | conf = to_test.FieldLevelEncryptionConfig(self._test_config_file) 20 | self.__check_configuration(conf) 21 | 22 | def test_load_config_as_json(self): 23 | json_conf = json.loads(self._test_config_file) 24 | 25 | conf = to_test.FieldLevelEncryptionConfig(json_conf) 26 | self.__check_configuration(conf) 27 | 28 | def test_load_config_wrong_format(self): 29 | self.assertRaises(ValueError, to_test.FieldLevelEncryptionConfig, b"not a valid config format") 30 | 31 | def test_load_config_with_key_password(self): 32 | json_conf = json.loads(self._test_config_file) 33 | json_conf["decryptionKey"] = resource_path("keys/test_key.p12") 34 | json_conf["decryptionKeyPassword"] = "Password1" 35 | 36 | conf = to_test.FieldLevelEncryptionConfig(json_conf) 37 | self.assertIsNotNone(conf.decryption_key, "No key password set") 38 | 39 | def test_load_config_with_wrong_key_password(self): 40 | json_conf = json.loads(self._test_config_file) 41 | json_conf["decryptionKey"] = resource_path("keys/test_key.p12") 42 | json_conf["decryptionKeyPassword"] = "wrong_passwd" 43 | 44 | self.assertRaises(PrivateKeyError, to_test.FieldLevelEncryptionConfig, json_conf) 45 | 46 | def test_load_config_with_missing_required_key_password(self): 47 | json_conf = json.loads(self._test_config_file) 48 | json_conf["decryptionKey"] = resource_path("keys/test_key.p12") 49 | 50 | self.assertRaises(PrivateKeyError, to_test.FieldLevelEncryptionConfig, json_conf) 51 | 52 | def test_load_config_missing_paths(self): 53 | wrong_json = json.loads(self._test_config_file) 54 | del wrong_json["paths"]["$"] 55 | 56 | self.assertRaises(KeyError, to_test.FieldLevelEncryptionConfig, wrong_json) 57 | 58 | del wrong_json["paths"] 59 | 60 | self.assertRaises(KeyError, to_test.FieldLevelEncryptionConfig, wrong_json) 61 | 62 | def test_load_config_missing_path_to_encrypt(self): 63 | wrong_json = json.loads(self._test_config_file) 64 | del wrong_json["paths"]["$"]["toEncrypt"] 65 | 66 | self.assertRaises(KeyError, to_test.FieldLevelEncryptionConfig, wrong_json) 67 | 68 | def test_load_config_missing_path_to_decrypt(self): 69 | wrong_json = json.loads(self._test_config_file) 70 | del wrong_json["paths"]["$"]["toDecrypt"] 71 | 72 | self.assertRaises(KeyError, to_test.FieldLevelEncryptionConfig, wrong_json) 73 | 74 | def test_load_config_missing_iv_field_name(self): 75 | wrong_json = json.loads(self._test_config_file) 76 | del wrong_json["ivFieldName"] 77 | 78 | self.assertRaises(KeyError, to_test.FieldLevelEncryptionConfig, wrong_json) 79 | 80 | def test_load_config_missing_encrypted_key_field_name(self): 81 | wrong_json = json.loads(self._test_config_file) 82 | del wrong_json["encryptedKeyFieldName"] 83 | 84 | self.assertRaises(KeyError, to_test.FieldLevelEncryptionConfig, wrong_json) 85 | 86 | def test_load_config_missing_encrypted_value_field_name(self): 87 | wrong_json = json.loads(self._test_config_file) 88 | del wrong_json["encryptedValueFieldName"] 89 | 90 | self.assertRaises(KeyError, to_test.FieldLevelEncryptionConfig, wrong_json) 91 | 92 | def test_load_config_missing_data_encoding(self): 93 | wrong_json = json.loads(self._test_config_file) 94 | del wrong_json["dataEncoding"] 95 | 96 | self.assertRaises(KeyError, to_test.FieldLevelEncryptionConfig, wrong_json) 97 | 98 | def test_load_config_hex_data_encoding(self): 99 | json_conf = json.loads(self._test_config_file) 100 | json_conf["dataEncoding"] = "hex" 101 | 102 | conf = to_test.FieldLevelEncryptionConfig(json_conf) 103 | self.__check_configuration(conf, encoding=ClientEncoding.HEX) 104 | 105 | def test_load_config_wrong_data_encoding(self): 106 | wrong_json = json.loads(self._test_config_file) 107 | wrong_json["dataEncoding"] = "WRONG" 108 | 109 | self.assertRaises(ValueError, to_test.FieldLevelEncryptionConfig, wrong_json) 110 | 111 | def test_load_config_missing_encryption_certificate(self): 112 | json_conf = json.loads(self._test_config_file) 113 | del json_conf["encryptionCertificate"] 114 | 115 | conf = to_test.FieldLevelEncryptionConfig(json_conf) 116 | self.assertIsNone(conf.encryption_certificate) 117 | self.assertIsNone(conf.encryption_certificate_fingerprint) 118 | self.assertIsNone(conf.encryption_key_fingerprint) 119 | 120 | def test_load_config_encryption_certificate_file_not_found(self): 121 | wrong_json = json.loads(self._test_config_file) 122 | wrong_json["encryptionCertificate"] = resource_path("certificates/wrong_certificate_name.pem") 123 | 124 | self.assertRaises(CertificateError, to_test.FieldLevelEncryptionConfig, wrong_json) 125 | 126 | def test_load_config_missing_decryption_key(self): 127 | json_conf = json.loads(self._test_config_file) 128 | del json_conf["decryptionKey"] 129 | 130 | conf = to_test.FieldLevelEncryptionConfig(json_conf) 131 | self.assertIsNone(conf.decryption_key) 132 | 133 | def test_load_config_decryption_key_file_not_found(self): 134 | wrong_json = json.loads(self._test_config_file) 135 | wrong_json["decryptionKey"] = resource_path("keys/wrong_private_key_name.pem") 136 | 137 | self.assertRaises(PrivateKeyError, to_test.FieldLevelEncryptionConfig, wrong_json) 138 | 139 | def test_load_config_missing_oaep_padding_algorithm(self): 140 | wrong_json = json.loads(self._test_config_file) 141 | del wrong_json["oaepPaddingDigestAlgorithm"] 142 | 143 | self.assertRaises(KeyError, to_test.FieldLevelEncryptionConfig, wrong_json) 144 | 145 | def test_load_config_SHA512_oaep_padding_algorithm(self): 146 | oaep_algo_test = "sha-512" 147 | json_conf = json.loads(self._test_config_file) 148 | json_conf["oaepPaddingDigestAlgorithm"] = oaep_algo_test 149 | 150 | conf = to_test.FieldLevelEncryptionConfig(json_conf) 151 | self.__check_configuration(conf, oaep_algo="SHA512") 152 | 153 | def test_load_config_wrong_oaep_padding_algorithm(self): 154 | oaep_algo_test = "sha_512" 155 | wrong_json = json.loads(self._test_config_file) 156 | wrong_json["oaepPaddingDigestAlgorithm"] = oaep_algo_test 157 | 158 | self.assertRaises(HashAlgorithmError, to_test.FieldLevelEncryptionConfig, wrong_json) 159 | 160 | def test_load_config_unsupported_oaep_padding_algorithm(self): 161 | wrong_json = json.loads(self._test_config_file) 162 | wrong_json["oaepPaddingDigestAlgorithm"] = "MD5" 163 | 164 | self.assertRaises(HashAlgorithmError, to_test.FieldLevelEncryptionConfig, wrong_json) 165 | 166 | def test_load_config_missing_optional_param(self): 167 | json_conf = json.loads(self._test_config_file) 168 | del json_conf["encryptionCertificateFingerprintFieldName"] 169 | 170 | conf = to_test.FieldLevelEncryptionConfig(json_conf) 171 | 172 | self.assertIsNotNone(conf.encryption_certificate_fingerprint) # fingerprint is always calculated 173 | self.assertIsNone(conf.encryption_certificate_fingerprint_field_name) 174 | 175 | def __check_configuration(self, conf, encoding=ClientEncoding.BASE64, oaep_algo="SHA256"): 176 | self.assertIsNotNone(conf.paths["$"], "No resource to encrypt/decrypt fields of is set") 177 | resource = conf.paths["$"] 178 | self.assertIsInstance(resource, to_test.EncryptionPathConfig, "Must be EncryptionPathConfig") 179 | self.assertDictEqual({"node1.node2.colour": "node1.node2.enc"}, resource.to_encrypt, 180 | "Fields to be encrypted not set properly") 181 | self.assertDictEqual({"node1.node2.enc": "node1.node2.plainColour"}, resource.to_decrypt, 182 | "Fields to be decrypted not set properly") 183 | 184 | self.assertEqual("iv", conf.iv_field_name, "IV field name not set") 185 | self.assertEqual("encryptedKey", conf.encrypted_key_field_name, "Encrypted key field name not set") 186 | self.assertEqual("encryptedValue", conf.encrypted_value_field_name, "Encrypted value field name not set") 187 | self.assertEqual(encoding, conf.data_encoding, "Data encoding value not set") 188 | 189 | self.assertEqual(self._expected_cert, conf.encryption_certificate, "Wrong encryption certificate") 190 | self.assertIsInstance(conf.decryption_key, RSA.RsaKey, "Must be RSA key") 191 | self.assertEqual(self._expected_key, 192 | conf.decryption_key.export_key(pkcs=8).decode('utf-8').replace("\n", "")[27:-25], 193 | "Wrong decryption key") 194 | self.assertEqual("80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279", 195 | conf.encryption_certificate_fingerprint, "Wrong certificate fingerprint") 196 | self.assertEqual("761b003c1eade3a5490e5000d37887baa5e6ec0e226c07706e599451fc032a79", 197 | conf.encryption_key_fingerprint, "Wrong public key fingerprint") 198 | 199 | self.assertEqual(oaep_algo, conf.oaep_padding_digest_algorithm, "Oaep padding algorithm not set") 200 | self.assertEqual("certFingerprint", conf.encryption_certificate_fingerprint_field_name, 201 | "Certificate fingerprint field name not set") 202 | self.assertEqual("keyFingerprint", conf.encryption_key_fingerprint_field_name, 203 | "Public key fingerprint field name not set") 204 | self.assertEqual("oaepHashingAlgo", conf.oaep_padding_digest_algorithm_field_name, 205 | "Oaep padding algorithm field name not set") 206 | -------------------------------------------------------------------------------- /tests/test_json_path_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | 4 | import client_encryption.json_path_utils as to_test 5 | 6 | 7 | class JsonPathUtilsTest(unittest.TestCase): 8 | 9 | @staticmethod 10 | def __get_sample_json(): 11 | return { 12 | "node1": { 13 | "node2": { 14 | "colour": "red", 15 | "shape": "circle", 16 | "position": { 17 | "lat": 1, 18 | "long": 3 19 | } 20 | } 21 | } 22 | } 23 | 24 | @staticmethod 25 | def __get_array_sample_json(): 26 | return { 27 | "node1": [ 28 | { 29 | "node2": { 30 | "colour": "red", 31 | "shape": "circle", 32 | "position": { 33 | "lat": 1, 34 | "long": 3 35 | } 36 | } 37 | } 38 | ] 39 | } 40 | 41 | def test_get_node(self): 42 | sample_json = self.__get_sample_json() 43 | 44 | node = to_test.get_node(sample_json, "$") 45 | self.assertIsInstance(node, dict, "Not a dict") 46 | self.assertDictEqual(sample_json, node) 47 | 48 | node = to_test.get_node(sample_json, "node1") 49 | self.assertIsInstance(node, dict, "Not a dict") 50 | self.assertDictEqual(sample_json["node1"], node) 51 | 52 | node = to_test.get_node(sample_json, "node1.node2") 53 | self.assertIsInstance(node, dict, "Not a dict") 54 | self.assertDictEqual(sample_json["node1"]["node2"], node) 55 | 56 | node = to_test.get_node(sample_json, "node1.node2.shape") 57 | self.assertIsInstance(node, str, "Not a string") 58 | self.assertEqual("circle", node) 59 | 60 | node = to_test.get_node(sample_json, "node1.node2.position.lat") 61 | self.assertIsInstance(node, int, "Not an int") 62 | self.assertEqual(1, node) 63 | 64 | node = to_test.get_node(sample_json, "node1.node2.newnode", True) 65 | self.assertIsInstance(node, dict, "Not a dict") 66 | self.assertDictEqual({}, node) 67 | 68 | def test_get_node_empty_path(self): 69 | sample_json = self.__get_sample_json() 70 | self.assertRaises(ValueError, to_test.get_node, sample_json, None) 71 | 72 | sample_json = self.__get_sample_json() 73 | self.assertRaises(ValueError, to_test.get_node, sample_json, "") 74 | 75 | def test_get_node_not_a_dict(self): 76 | sample_json = self.__get_sample_json() 77 | 78 | self.assertRaises(ValueError, to_test.get_node, sample_json, "node1.node2.shape.newnode") 79 | self.assertRaises(ValueError, to_test.get_node, sample_json, "node1.node2.shape.newnode", True) 80 | 81 | def test_get_node_not_existing(self): 82 | sample_json = self.__get_sample_json() 83 | 84 | # create=False 85 | self.assertRaises(KeyError, to_test.get_node, sample_json, "node1.node2.newnode") 86 | # too many new nodes 87 | self.assertRaises(KeyError, to_test.get_node, sample_json, "node1.node2.newnode.newnode2", True) 88 | 89 | def test_update_node(self): 90 | sample_json = self.__get_sample_json() 91 | node = to_test.update_node(sample_json, "$", '{"node3": {"brightness": 6}}') 92 | 93 | self.assertIsInstance(node, dict, "Not a dict") 94 | self.assertDictEqual({"node3": { 95 | "brightness": 6 96 | } 97 | }, node) 98 | 99 | sample_json = self.__get_sample_json() 100 | node = to_test.update_node(sample_json, "node1", '{"node3": {"brightness": 6}}') 101 | 102 | self.assertIsInstance(node, dict, "Not a dict") 103 | self.assertDictEqual({"node1": { 104 | "node2": { 105 | "colour": "red", 106 | "shape": "circle", 107 | "position": { 108 | "lat": 1, 109 | "long": 3 110 | } 111 | }, 112 | "node3": { 113 | "brightness": 6 114 | } 115 | } 116 | }, node) 117 | 118 | sample_json = self.__get_sample_json() 119 | node = to_test.update_node(sample_json, "node1.node2", '{"node3": {"brightness": 6}}') 120 | 121 | self.assertIsInstance(node, dict, "Not a dict") 122 | self.assertDictEqual({"node1": { 123 | "node2": { 124 | "colour": "red", 125 | "shape": "circle", 126 | "position": { 127 | "lat": 1, 128 | "long": 3 129 | }, 130 | "node3": { 131 | "brightness": 6 132 | } 133 | } 134 | } 135 | }, node) 136 | 137 | sample_json = self.__get_sample_json() 138 | node = to_test.update_node(sample_json, "node1.node2.new", '{"node3": {"brightness": 6}}') 139 | 140 | self.assertIsInstance(node, dict, "Not a dict") 141 | self.assertDictEqual({"node1": { 142 | "node2": { 143 | "colour": "red", 144 | "shape": "circle", 145 | "position": { 146 | "lat": 1, 147 | "long": 3 148 | }, 149 | "new": { 150 | "node3": { 151 | "brightness": 6 152 | } 153 | } 154 | } 155 | } 156 | }, node) 157 | 158 | def test_update_node_empty_path(self): 159 | sample_json = self.__get_sample_json() 160 | self.assertRaises(ValueError, to_test.update_node, sample_json, None, '{"node3": {"brightness": 6}}') 161 | 162 | sample_json = self.__get_sample_json() 163 | self.assertRaises(ValueError, to_test.update_node, sample_json, "", '{"node3": {"brightness": 6}}') 164 | 165 | def test_update_node_not_json(self): 166 | sample_json = self.__get_sample_json() 167 | node = to_test.update_node(sample_json, "node1.node2", "not a json string") 168 | 169 | self.assertIsInstance(node["node1"]["node2"], str, "not a json string") 170 | 171 | def test_update_node_array_with_str(self): 172 | sample_json = self.__get_array_sample_json() 173 | node = to_test.update_node(sample_json, "node1.node2", "not a json string") 174 | 175 | self.assertIsInstance(node["node1"][0]["node2"], str, "not a json string") 176 | 177 | def test_update_node_array_with_json_str(self): 178 | sample_json = self.__get_array_sample_json() 179 | node = to_test.update_node(sample_json, "node1.node2", '{"position": {"brightness": 6}}') 180 | 181 | self.assertIsInstance(node["node1"][0]["node2"]["position"], dict) 182 | self.assertDictEqual({'node1': [ 183 | {'node2': { 184 | 'colour': 'red', 185 | 'shape': 'circle', 186 | 'position': { 187 | 'brightness': 6 188 | } 189 | } 190 | } 191 | ]}, node) 192 | 193 | 194 | def test_update_node_primitive_type(self): 195 | sample_json = self.__get_sample_json() 196 | 197 | node = to_test.update_node(sample_json, "node1.node2", '"I am a primitive data type"') 198 | 199 | self.assertIsInstance(node["node1"]["node2"], str, "Not a string") 200 | self.assertDictEqual({"node1": { 201 | "node2": "I am a primitive data type" 202 | } 203 | }, node) 204 | 205 | node = to_test.update_node(sample_json, "node1.node2", '4378462') 206 | 207 | self.assertIsInstance(node["node1"]["node2"], int, "Not an int") 208 | self.assertDictEqual({"node1": { 209 | "node2": 4378462 210 | } 211 | }, node) 212 | 213 | node = to_test.update_node(sample_json, "node1.node2", 'true') 214 | 215 | self.assertIsInstance(node["node1"]["node2"], bool, "Not a bool") 216 | self.assertDictEqual({"node1": { 217 | "node2": True 218 | } 219 | }, node) 220 | 221 | def test_pop_node(self): 222 | original_json = self.__get_sample_json() 223 | 224 | sample_json = self.__get_sample_json() 225 | node = to_test.pop_node(sample_json, "$") 226 | self.assertIsInstance(node, str, "Not a string") 227 | self.assertDictEqual(original_json, json.loads(node)) 228 | 229 | self.assertDictEqual({}, sample_json) 230 | 231 | sample_json = self.__get_sample_json() 232 | node = to_test.pop_node(sample_json, "node1") 233 | self.assertIsInstance(node, str, "Not a string") 234 | self.assertDictEqual(original_json["node1"], json.loads(node)) 235 | 236 | self.assertDictEqual({}, sample_json) 237 | 238 | sample_json = self.__get_sample_json() 239 | node = to_test.pop_node(sample_json, "node1.node2") 240 | self.assertIsInstance(node, str, "Not a string") 241 | self.assertDictEqual(original_json["node1"]["node2"], json.loads(node)) 242 | 243 | self.assertDictEqual({"node1": {}}, sample_json) 244 | 245 | sample_json = self.__get_sample_json() 246 | node = to_test.pop_node(sample_json, "node1.node2.colour") 247 | self.assertIsInstance(node, str, "Not a string") 248 | self.assertEqual("red", node) 249 | self.assertDictEqual({"node1": { 250 | "node2": { 251 | "shape": "circle", 252 | "position": { 253 | "lat": 1, 254 | "long": 3 255 | } 256 | } 257 | } 258 | }, sample_json) 259 | 260 | def test_pop_node_empty_path(self): 261 | sample_json = self.__get_sample_json() 262 | self.assertRaises(ValueError, to_test.pop_node, sample_json, None) 263 | 264 | sample_json = self.__get_sample_json() 265 | self.assertRaises(ValueError, to_test.pop_node, sample_json, "") 266 | 267 | def test_pop_node_not_existing(self): 268 | sample_json = self.__get_sample_json() 269 | 270 | self.assertRaises(KeyError, to_test.pop_node, sample_json, "node0") 271 | self.assertRaises(KeyError, to_test.pop_node, sample_json, "node1.node2.node3") 272 | 273 | def test_cleanup_node(self): 274 | original_json = self.__get_sample_json() 275 | 276 | sample_json = self.__get_sample_json() 277 | node = to_test.cleanup_node(sample_json, "node1.node2.colour", "target") 278 | self.assertIsInstance(node, dict, "Not a dictionary") 279 | self.assertDictEqual(original_json, node) 280 | self.assertDictEqual(original_json, sample_json) 281 | 282 | sample_json = self.__get_sample_json() 283 | del sample_json["node1"]["node2"]["colour"] 284 | del sample_json["node1"]["node2"]["shape"] 285 | del sample_json["node1"]["node2"]["position"] 286 | node = to_test.cleanup_node(sample_json, "node1.node2", "target") 287 | self.assertIsInstance(node, dict, "Not a dictionary") 288 | self.assertDictEqual({"node1": {}}, node) 289 | self.assertDictEqual({"node1": {}}, sample_json) 290 | 291 | def test_cleanup_node_in_target(self): 292 | sample_json = self.__get_sample_json() 293 | del sample_json["node1"]["node2"]["colour"] 294 | del sample_json["node1"]["node2"]["shape"] 295 | del sample_json["node1"]["node2"]["position"] 296 | node = to_test.cleanup_node(sample_json, "node1.node2", "node1.node2.target") 297 | self.assertIsInstance(node, dict, "Not a dictionary") 298 | self.assertDictEqual({"node1": {"node2": {}}}, node) 299 | self.assertDictEqual({"node1": {"node2": {}}}, sample_json) 300 | 301 | def test_cleanup_node_empty_path(self): 302 | sample_json = self.__get_sample_json() 303 | self.assertRaises(ValueError, to_test.cleanup_node, sample_json, None, "target") 304 | 305 | sample_json = self.__get_sample_json() 306 | self.assertRaises(ValueError, to_test.cleanup_node, sample_json, "", "target") 307 | 308 | def test_cleanup_node_empty_target(self): 309 | sample_json = self.__get_sample_json() 310 | del sample_json["node1"]["node2"]["colour"] 311 | del sample_json["node1"]["node2"]["shape"] 312 | del sample_json["node1"]["node2"]["position"] 313 | node = to_test.cleanup_node(sample_json, "node1.node2", None) 314 | self.assertIsInstance(node, dict, "Not a dictionary") 315 | self.assertDictEqual({"node1": {}}, node) 316 | self.assertDictEqual({"node1": {}}, sample_json) 317 | 318 | sample_json = self.__get_sample_json() 319 | del sample_json["node1"]["node2"]["colour"] 320 | del sample_json["node1"]["node2"]["shape"] 321 | del sample_json["node1"]["node2"]["position"] 322 | node = to_test.cleanup_node(sample_json, "node1.node2", "") 323 | self.assertIsInstance(node, dict, "Not a dictionary") 324 | self.assertDictEqual({"node1": {}}, node) 325 | self.assertDictEqual({"node1": {}}, sample_json) 326 | 327 | def test_cleanup_node_not_existing(self): 328 | sample_json = self.__get_sample_json() 329 | 330 | self.assertRaises(KeyError, to_test.cleanup_node, sample_json, "node0", "target") 331 | self.assertRaises(KeyError, to_test.cleanup_node, sample_json, "node1.node2.node3", "target") 332 | -------------------------------------------------------------------------------- /tests/test_jwe_encryption.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import client_encryption.jwe_encryption as to_test 3 | from client_encryption.jwe_encryption_config import JweEncryptionConfig 4 | from tests import get_mastercard_config_for_test 5 | 6 | 7 | class JweEncryptionTest(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self._config = JweEncryptionConfig(get_mastercard_config_for_test()) 11 | self._config._paths["$"]._to_encrypt = {"$": "$"} 12 | self._config._paths["$"]._to_decrypt = {"encryptedValue": "$"} 13 | 14 | def test_encrypt_payload_should_be_able_to_be_decrypted(self): 15 | payload = { 16 | "data": { 17 | "field1": "value1", 18 | "field2": "value2" 19 | } 20 | } 21 | 22 | encrypted_payload = to_test.encrypt_payload(payload, self._config) 23 | decrypted_payload = to_test.decrypt_payload(encrypted_payload, self._config) 24 | self.assertDictEqual(payload, decrypted_payload) 25 | 26 | def test_encrypt_payload_should_be_able_to_decrypt_empty_json(self): 27 | payload = {} 28 | 29 | encrypted_payload = to_test.encrypt_payload(payload, self._config) 30 | decrypted_payload = to_test.decrypt_payload(encrypted_payload, self._config) 31 | self.assertDictEqual(payload, decrypted_payload) 32 | 33 | def test_encrypt_payload_should_be_able_to_decrypt_root_arrays(self): 34 | payload = [ 35 | { 36 | 'field1': 'field2' 37 | } 38 | ] 39 | 40 | encrypted_payload = to_test.encrypt_payload(payload, self._config) 41 | decrypted_payload = to_test.decrypt_payload(encrypted_payload, self._config) 42 | self.assertListEqual(payload, decrypted_payload) 43 | 44 | def test_encrypt_payload_with_multiple_encryption_paths(self): 45 | self._config._paths["$"]._to_encrypt = {"data1": "encryptedData1", "data2": "encryptedData2"} 46 | self._config._paths["$"]._to_decrypt = {"encryptedData1": "data1", "encryptedData2": "data2"} 47 | 48 | payload = { 49 | "data1": { 50 | "field1": "value1", 51 | "field2": "value2" 52 | }, 53 | "data2": { 54 | "field3": "value3", 55 | "field4": "value4" 56 | } 57 | } 58 | 59 | encrypted_payload = to_test.encrypt_payload(payload, self._config) 60 | 61 | self.assertNotIn("data1", encrypted_payload) 62 | self.assertNotIn("data2", encrypted_payload) 63 | 64 | decrypted_payload = to_test.decrypt_payload(encrypted_payload, self._config) 65 | self.assertDictEqual(payload, decrypted_payload) 66 | 67 | def test_decrypt_payload_should_decrypt_aes128gcm_payload(self): 68 | encrypted_payload = { 69 | "encryptedValue": "eyJlbmMiOiJBMTI4R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.WtvYljbsjdEv-Ttxx1p6PgyIrOsLpj1FMF9NQNhJUAHlKchAo5QImgEgIdgJE7HC2KfpNcHiQVqKKZq_y201FVzpicDkNzlPJr5kIH4Lq-oC5iP0agWeou9yK5vIxFRP__F_B8HSuojBJ3gDYT_KdYffUIHkm_UysNj4PW2RIRlafJ6RKYanVzk74EoKZRG7MIr3pTU6LIkeQUW41qYG8hz6DbGBOh79Nkmq7Oceg0ZwCn1_MruerP-b15SGFkuvOshStT5JJp7OOq82gNAOkMl4fylEj2-vADjP7VSK8GlqrA7u9Tn-a4Q28oy0GOKr1Z-HJgn_CElknwkUTYsWbg.PKl6_kvZ4_4MjmjW.AH6pGFkn7J49hBQcwg.zdyD73TcuveImOy4CRnVpw" 70 | } 71 | 72 | decrypted_payload = {"foo": "bar"} 73 | 74 | payload = to_test.decrypt_payload(encrypted_payload, self._config) 75 | self.assertNotIn("encryptedValue", payload) 76 | self.assertDictEqual(decrypted_payload, payload) 77 | 78 | def test_decrypt_payload_should_decrypt_aes192gcm_payload(self): 79 | encrypted_payload = { 80 | "encryptedValue": "eyJlbmMiOiJBMTkyR0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.FWC8PVaZoR2TRKwKO4syhSJReezVIvtkxU_yKh4qODNvlVr8t8ttvySJ-AjM8xdI6vNyIg9jBMWASG4cE49jT9FYuQ72fP4R-Td4vX8wpB8GonQj40yLqZyfRLDrMgPR20RcQDW2ThzLXsgI55B5l5fpwQ9Nhmx8irGifrFWOcJ_k1dUSBdlsHsYxkjRKMENu5x4H6h12gGZ21aZSPtwAj9msMYnKLdiUbdGmGG_P8a6gPzc9ih20McxZk8fHzXKujjukr_1p5OO4o1N4d3qa-YI8Sns2fPtf7xPHnwi1wipmCC6ThFLU80r3173RXcpyZkF8Y3UacOS9y1f8eUfVQ.JRE7kZLN4Im1Rtdb.eW_lJ-U330n0QHqZnQ._r5xYVvMCrvICwLz4chjdw" 81 | } 82 | 83 | decrypted_payload = {"foo": "bar"} 84 | 85 | payload = to_test.decrypt_payload(encrypted_payload, self._config) 86 | self.assertNotIn("encryptedValue", payload) 87 | self.assertDictEqual(decrypted_payload, payload) 88 | 89 | def test_decrypt_payload_should_decrypt_aes256gcm_payload(self): 90 | encrypted_payload = { 91 | "encryptedValue": "eyJraWQiOiI3NjFiMDAzYzFlYWRlM2E1NDkwZTUwMDBkMzc4ODdiYWE1ZTZlYzBlMjI2YzA3NzA2ZTU5OTQ1MWZjMDMyYTc5IiwiY3R5IjoiYXBwbGljYXRpb25cL2pzb24iLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.8c6vxeZOUBS8A9SXYUSrRnfl1ht9xxciB7TAEv84etZhQQ2civQKso-htpa2DWFBSUm-UYlxb6XtXNXZxuWu-A0WXjwi1K5ZAACc8KUoYnqPldEtC9Q2bhbQgc_qZF_GxeKrOZfuXc9oi45xfVysF_db4RZ6VkLvY2YpPeDGEMX_nLEjzqKaDz_2m0Ae_nknr0p_Nu0m5UJgMzZGR4Sk1DJWa9x-WJLEyo4w_nRDThOjHJshOHaOU6qR5rdEAZr_dwqnTHrjX9Qm9N9gflPGMaJNVa4mvpsjz6LJzjaW3nJ2yCoirbaeJyCrful6cCiwMWMaDMuiBDPKa2ovVTy0Sw.w0Nkjxl0T9HHNu4R.suRZaYu6Ui05Z3-vsw.akknMr3Dl4L0VVTGPUszcA" 92 | } 93 | 94 | decrypted_payload = {"foo": "bar"} 95 | 96 | payload = to_test.decrypt_payload(encrypted_payload, self._config) 97 | self.assertNotIn("encryptedValue", payload) 98 | self.assertDictEqual(decrypted_payload, payload) 99 | 100 | def test_decrypt_payload_should_decrypt_cbc_payload(self): 101 | encrypted_payload = { 102 | "encryptedValue": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.2GzZlB3scifhqlzIV2Rxk1TwiWL35e0AtcI9MFusG9jv9zGrJ8BapJx73PlFu69S0IAR7hXpqwzD7-UzmHUdrxB7izbMm9TNDpznHIuTaJWSRngD5Zui_rUXETL0GJG8dERx7IngqTltfzZanhDnjDNfKaowD6pFSEVN-Ff-pTeJqLMPs5504DtnYGD_uhQjvFmREIBgQTGEINzT88PXwLTAVBbWbAad_I-4Q12YwW_Y4yqmARCMTRWP-ixMrlSWCJlh6hz-biEotWNwGvp2pdhdiEP2VSvvUKHd7IngMWcMozOcoZQ1n18kWiFvt90fzNXSmzTjyGYSWUsa_mVouA.aX5mOSiXtilwYPFeTUFN_A.ZyAY79BAjG-QMQIhesj9bQ.TPZ2VYWdTLopCNkvMqUyuQ" 103 | } 104 | 105 | decrypted_payload = {"foo": "bar"} 106 | 107 | payload = to_test.decrypt_payload(encrypted_payload, self._config) 108 | self.assertNotIn("encryptedValue", payload) 109 | self.assertDictEqual(decrypted_payload, payload) 110 | -------------------------------------------------------------------------------- /tests/test_jwe_encryption_config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | from Crypto.PublicKey import RSA 5 | 6 | import client_encryption.jwe_encryption_config as to_test 7 | from client_encryption.encoding_utils import ClientEncoding 8 | from client_encryption.encryption_exception import HashAlgorithmError, PrivateKeyError, CertificateError 9 | from client_encryption.encryption_utils import load_encryption_certificate 10 | from tests import resource_path, get_jwe_config_for_test 11 | 12 | 13 | class JweEncryptionConfigTest(unittest.TestCase): 14 | 15 | def setUp(self): 16 | self._test_config_file = get_jwe_config_for_test() 17 | self._expected_cert, cert_type = load_encryption_certificate(resource_path("certificates/test_certificate-2048.der")) 18 | self._expected_key = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD0ynqAQWn0T7/VJLletTJgoxsTt5TR3IkJ+Yk/Pxg6Q5hXuiGrBdC+OVo/9hrNnptuZh9rZYKto6lbSjYFiKMeBDvPZrYDPzusp0C0KllIoVbzYiOezD76XHsQAEje0UXbzZlXstPXef2bi2HkqV26ST167L5O4moK8+7jHMT80T6XgsUyvyt8PjsQ9CSu6fnD9NfCSYmt2cb16OXcEtA7To2zoGznXqB6JhntFjG0jxee7RkLR+moOqMI9kFM5GSIV4uhwQ9FtOCjUf7TFAU12wwfX/QXUEj6G93GVtzf6QdkVkWh4EyRHeMLyMNc5c0Iw1ZvXdOKfoeo9F47QpbzAgMBAAECggEAK3dMmzuCSdxjTsCPnc6E3H35z914Mm97ceb6RN26OpZIFcO6OLj2oOBkMxlLFxnDta2yhIpo0tZNuyUJRKBHfov35tLxHNB8kyK7rYIbincDjoHtm0PfJuuG+odiaRY11lrCkLzzOr6xlo4AWu7r8qkQnqQtAqrXc4xu7artG4rfMIunGnjjWQGzovtey1JgZctO97MU4Wvw18vgYBI6JM4eHJkZxgEhVQblBTKZs4OfiWk6MRHchgvqnWugwl213FgCzwy9cnyxTP13i9QKaFzL29TYmmN6bRWBH95z41M8IAa0CGahrSJjudZCFwsFh413YWv/pdqdkKHg1sqseQKBgQD641RYQkMn4G9vOiwB/is5M0OAhhUdWH1QtB8vvhY5ISTjFMqgIIVQvGmqDDk8QqFMOfFFqLtnArGn8HrKmBXMpRigS4ae/QgHEz34/RFjNDQ9zxIf/yoCRH5PmnPPU6x8j3bj/vJMRQA6/yngoca+9qvi3R32AtC5DUELnwyzNwKBgQD5x1iEV+albyCNNyLoT/f6LSH1NVcO+0IOvIaAVMtfy+hEEXz7izv3/AgcogVZzRARSK0qsQ+4WQN6Q2WG5cQYSyB92PR+VgwhnagVvA+QHNDL988xoMhB5r2D2IVSRuTB2EOg7LiWHUHIExaxVkbADODDj7YV2aQCJVv0gbDQJQKBgQCaABix5Fqci6NbPvXsczvM7K6uoZ8sWDjz5NyPzbqObs3ZpdWK3Ot4V270tnQbjTq9M4PqIlyGKp0qXO7ClQAskdq/6hxEU0UuMp2DzLNzlYPLvON/SH1czvZJnqEfzli+TMHJyaCpOGGf1Si7fhIk/f0cUGYnsCq2rHAU1hhRmQKBgE/BJTRs1MqyJxSwLEc9cZLCYntnYrr342nNLK1BZgbalvlVFDFFjgpqwTRTT54S6jR6nkBpdPmKAqBBcOOX7ftL0b4dTkQguZLqQkdeWyHK8aiPIetYyVixkoXM1xUkadqzcTSrIW1dPiniXnaVc9XSxtnqw1tKuSGuSCRUXN65AoGBAN/AmT1S4PAQpSWufC8NUJey8S0bURUNNjd52MQ7pWzGq2QC00+dBLkTPj3KOGYpXw9ScZPbxOthBFzHOxERWo16AFw3OeRtn4VB1QJ9XvoA/oz4lEhJKbwUfuFGGvSpYvg3vZcOHF2zlvcUu7C0ub/WhOjV9jZvU5B2Ev8x1neb" 19 | 20 | def test_load_config_as_string(self): 21 | conf = to_test.JweEncryptionConfig(self._test_config_file) 22 | self.__check_configuration(conf) 23 | 24 | def test_load_config_as_json(self): 25 | json_conf = json.loads(self._test_config_file) 26 | 27 | conf = to_test.JweEncryptionConfig(json_conf) 28 | self.__check_configuration(conf) 29 | 30 | def test_load_config_wrong_format(self): 31 | self.assertRaises(ValueError, to_test.JweEncryptionConfig, b"not a valid config format") 32 | 33 | def test_load_config_with_key_password(self): 34 | json_conf = json.loads(self._test_config_file) 35 | json_conf["decryptionKey"] = resource_path("keys/test_key.p12") 36 | json_conf["decryptionKeyPassword"] = "Password1" 37 | 38 | conf = to_test.JweEncryptionConfig(json_conf) 39 | self.assertIsNotNone(conf.decryption_key, "No key password set") 40 | 41 | def test_load_config_with_wrong_key_password(self): 42 | json_conf = json.loads(self._test_config_file) 43 | json_conf["decryptionKey"] = resource_path("keys/test_key.p12") 44 | json_conf["decryptionKeyPassword"] = "wrong_passwd" 45 | 46 | self.assertRaises(PrivateKeyError, to_test.JweEncryptionConfig, json_conf) 47 | 48 | def test_load_config_with_missing_required_key_password(self): 49 | json_conf = json.loads(self._test_config_file) 50 | json_conf["decryptionKey"] = resource_path("keys/test_key.p12") 51 | 52 | self.assertRaises(PrivateKeyError, to_test.JweEncryptionConfig, json_conf) 53 | 54 | def test_load_config_missing_paths(self): 55 | wrong_json = json.loads(self._test_config_file) 56 | del wrong_json["paths"]["$"] 57 | 58 | self.assertRaises(KeyError, to_test.JweEncryptionConfig, wrong_json) 59 | 60 | del wrong_json["paths"] 61 | 62 | self.assertRaises(KeyError, to_test.JweEncryptionConfig, wrong_json) 63 | 64 | def test_load_config_missing_path_to_encrypt(self): 65 | wrong_json = json.loads(self._test_config_file) 66 | del wrong_json["paths"]["$"]["toEncrypt"] 67 | 68 | self.assertRaises(KeyError, to_test.JweEncryptionConfig, wrong_json) 69 | 70 | def test_load_config_missing_path_to_decrypt(self): 71 | wrong_json = json.loads(self._test_config_file) 72 | del wrong_json["paths"]["$"]["toDecrypt"] 73 | 74 | self.assertRaises(KeyError, to_test.JweEncryptionConfig, wrong_json) 75 | 76 | def test_load_config_missing_encrypted_value_field_name(self): 77 | wrong_json = json.loads(self._test_config_file) 78 | del wrong_json["encryptedValueFieldName"] 79 | 80 | self.assertRaises(KeyError, to_test.JweEncryptionConfig, wrong_json) 81 | 82 | def test_load_config_missing_encryption_certificate(self): 83 | json_conf = json.loads(self._test_config_file) 84 | del json_conf["encryptionCertificate"] 85 | 86 | conf = to_test.JweEncryptionConfig(json_conf) 87 | self.assertIsNone(conf.encryption_certificate) 88 | self.assertIsNone(conf.encryption_key_fingerprint) 89 | 90 | def test_load_config_encryption_certificate_file_not_found(self): 91 | wrong_json = json.loads(self._test_config_file) 92 | wrong_json["encryptionCertificate"] = resource_path("certificates/wrong_certificate_name.pem") 93 | 94 | self.assertRaises(CertificateError, to_test.JweEncryptionConfig, wrong_json) 95 | 96 | def test_load_config_missing_decryption_key(self): 97 | json_conf = json.loads(self._test_config_file) 98 | del json_conf["decryptionKey"] 99 | 100 | conf = to_test.JweEncryptionConfig(json_conf) 101 | self.assertIsNone(conf.decryption_key) 102 | 103 | def test_load_config_decryption_key_file_not_found(self): 104 | wrong_json = json.loads(self._test_config_file) 105 | wrong_json["decryptionKey"] = resource_path("keys/wrong_private_key_name.pem") 106 | 107 | self.assertRaises(PrivateKeyError, to_test.JweEncryptionConfig, wrong_json) 108 | 109 | def __check_configuration(self, conf, encoding=ClientEncoding.BASE64, oaep_algo="SHA256"): 110 | self.assertIsNotNone(conf.paths["$"], "No resource to encrypt/decrypt fields of is set") 111 | resource = conf.paths["$"] 112 | self.assertIsInstance(resource, to_test.EncryptionPathConfig, "Must be EncryptionPathConfig") 113 | self.assertDictEqual({"node1.node2.colour": "node1.node2.enc"}, resource.to_encrypt, 114 | "Fields to be encrypted not set properly") 115 | self.assertDictEqual({"node1.node2.enc": "node1.node2.plainColour"}, resource.to_decrypt, 116 | "Fields to be decrypted not set properly") 117 | 118 | self.assertEqual("encryptedValue", conf.encrypted_value_field_name, "Encrypted value field name not set") 119 | self.assertEqual(encoding, conf.data_encoding, "Data encoding value not set") 120 | 121 | self.assertEqual(self._expected_cert, conf.encryption_certificate, "Wrong encryption certificate") 122 | self.assertIsInstance(conf.decryption_key, RSA.RsaKey, "Must be RSA key") 123 | self.assertEqual(self._expected_key, 124 | conf.decryption_key.export_key(pkcs=8).decode('utf-8').replace("\n", "")[27:-25], 125 | "Wrong decryption key") 126 | self.assertEqual("761b003c1eade3a5490e5000d37887baa5e6ec0e226c07706e599451fc032a79", 127 | conf.encryption_key_fingerprint, "Wrong public key fingerprint") 128 | 129 | self.assertEqual(oaep_algo, conf.oaep_padding_digest_algorithm, "Oaep padding algorithm not set") 130 | -------------------------------------------------------------------------------- /tests/test_session_key_params.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from tests import get_mastercard_config_for_test 3 | from binascii import Error 4 | import client_encryption.session_key_params as to_test 5 | from client_encryption.field_level_encryption_config import FieldLevelEncryptionConfig 6 | from client_encryption.encryption_exception import KeyWrappingError 7 | 8 | 9 | class SessionKeyParamsTest(unittest.TestCase): 10 | 11 | _expected_iv = b"\x14\x19iI|\xfa7\xc2\xac6\xb7\x84\xd6\xc8\x92\x15" 12 | _expected_private_key = b"\xd4\xd2\xfe\x88\xbe\xa2t\xc5\x9d\xc0\x10\xf0m\xbc7\xff" 13 | _iv_encoded = "FBlpSXz6N8KsNreE1siSFQ==" 14 | _wrapped_key = "VAJccUUNnqGU1aerzKahl/qLMd0BGWo7QC0sn5v9c5TL+9vMt5q/7h6Ae83mlovgjCmaDxBCkVwrLdB/fUMxhjYAEMTMT8Y8Z/RsVQq7osiLotO+UBycIDFJaKanRxCDnrDOrbBPMY+v/STFl99SR1dJOQx9udSkI+QOw2g7UayvM83Huw3ESH8GIKSo9PR0rPAS/vLRaDjeaJlDCFe/hwGWqdEa85JCJ6B0itkGjWag6bNdspYbmMruEPZ4J5/+LLCA5dNLiVObyBlGRAJDXbC3/nR1Tzg/5wzpRxFSGo1qcBPEIB9nSgJNIf2WDGEJTcINTEs181jKUQKvu2Kqeg==" 15 | 16 | def setUp(self): 17 | self._config = FieldLevelEncryptionConfig(get_mastercard_config_for_test()) 18 | 19 | def test_generate(self): 20 | key_params = to_test.SessionKeyParams.generate(self._config) 21 | 22 | self.assertIsNotNone(key_params) 23 | self.assertEqual(self._config, key_params.config) 24 | self.assertIsNotNone(key_params.iv_spec) 25 | self.assertIsNotNone(key_params.iv_value) 26 | self.assertIsNotNone(key_params.key) 27 | self.assertIsNotNone(key_params.encrypted_key_value) 28 | self.assertIsNotNone(key_params.oaep_padding_digest_algorithm_value) 29 | 30 | expected = to_test.SessionKeyParams._SessionKeyParams__unwrap_secret_key(key_params.encrypted_key_value, 31 | self._config, "SHA-256") 32 | self.assertEqual(expected, key_params.key) 33 | self.assertEqual(self._config.oaep_padding_digest_algorithm, key_params.oaep_padding_digest_algorithm_value) 34 | 35 | def test_get_key(self): 36 | key_params = to_test.SessionKeyParams(self._config, self._wrapped_key, self._iv_encoded) 37 | 38 | self.assertEqual(self._expected_private_key, key_params.key) 39 | 40 | def test_get_key_not_wrapped_key(self): 41 | key_params = to_test.SessionKeyParams(self._config, "this is not a private key!", self._iv_encoded) 42 | 43 | with self.assertRaises(ValueError): 44 | key_params.key 45 | 46 | def test_get_key_invalid_wrapped_key(self): 47 | wrong_wrapped_key = self._wrapped_key[0:-15]+"c29tZSBkYXRh==" 48 | key_params = to_test.SessionKeyParams(self._config, wrong_wrapped_key, self._iv_encoded) 49 | 50 | with self.assertRaises(KeyWrappingError): 51 | key_params.key 52 | 53 | def test_get_iv(self): 54 | key_params = to_test.SessionKeyParams(self._config, self._wrapped_key, self._iv_encoded) 55 | 56 | self.assertEqual(self._expected_iv, key_params.iv_spec) 57 | 58 | def test_get_iv_invalid_encoding(self): 59 | key_params = to_test.SessionKeyParams(self._config, self._wrapped_key, "df(sag") 60 | 61 | with self.assertRaises(Error): 62 | key_params.iv_spec 63 | 64 | def test_wrap_secret_key(self): 65 | prev_wrpd_key = "" 66 | for i in range(1, 4): 67 | wrpd_key = to_test.SessionKeyParams._SessionKeyParams__wrap_secret_key(self._expected_private_key, self._config) 68 | self.assertIsNotNone(wrpd_key) 69 | self.assertNotEqual(prev_wrpd_key, wrpd_key) 70 | 71 | prev_wrpd_key = wrpd_key # check 2 wraps for same key do not match (MGF1) 72 | 73 | plain_key = to_test.SessionKeyParams._SessionKeyParams__unwrap_secret_key(wrpd_key, self._config, "SHA-256") 74 | self.assertEqual(self._expected_private_key, plain_key) 75 | 76 | def test_wrap_secret_key_fail(self): 77 | self.assertRaises(KeyWrappingError, to_test.SessionKeyParams._SessionKeyParams__wrap_secret_key, 78 | None, self._config) 79 | 80 | def test_unwrap_secret_key(self): 81 | key = to_test.SessionKeyParams._SessionKeyParams__unwrap_secret_key(self._wrapped_key, self._config, "SHA-256") 82 | 83 | self.assertEqual(self._expected_private_key, key) 84 | 85 | def test_unwrap_secret_key_fail(self): 86 | self.assertRaises(KeyWrappingError, to_test.SessionKeyParams._SessionKeyParams__unwrap_secret_key, 87 | self._wrapped_key[0:-15]+"c29tZSBkYXRh==", self._config, "SHA-256") 88 | -------------------------------------------------------------------------------- /tests/utils/api_encryption_test_utils.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | from functools import wraps 3 | import json 4 | from tests import get_mastercard_config_for_test 5 | import client_encryption.field_level_encryption as encryption 6 | import client_encryption.field_level_encryption_config as encryption_config 7 | from client_encryption.session_key_params import SessionKeyParams 8 | 9 | 10 | def mock_signing(func): 11 | """Decorator to mock signing layer and avoid warnings.""" 12 | @wraps(func) 13 | def request_function(*args, **kwargs): 14 | return func(*args, **kwargs) 15 | 16 | request_function.__oauth__ = True 17 | return request_function 18 | 19 | 20 | class MockService(object): 21 | 22 | def __init__(self, api_client=None): 23 | if api_client is None: 24 | api_client = MockApiClient() 25 | self.api_client = api_client 26 | self.api_client.rest_client = api_client 27 | 28 | def do_something_get(self, **kwargs): 29 | return self.api_client.request("GET", "testservice", None, kwargs["headers"]) 30 | 31 | def do_something_post(self, **kwargs): 32 | return self.api_client.request("POST", "testservice", None, kwargs["headers"], post_params=None, body=kwargs["body"]) 33 | 34 | def do_something_delete(self, **kwargs): 35 | return self.api_client.request("DELETE", "testservice", None, kwargs["headers"], post_params=None, body=kwargs["body"]) 36 | 37 | def do_something_get_use_headers(self, **kwargs): 38 | return self.api_client.request("GET", "testservice/headers", None, kwargs["headers"]) 39 | 40 | def do_something_post_use_headers(self, **kwargs): 41 | return self.api_client.request("POST", "testservice/headers", None, headers=kwargs["headers"], post_params=None, body=kwargs["body"]) 42 | 43 | def do_something_delete_use_headers(self, **kwargs): 44 | return self.api_client.request("DELETE", "testservice/headers", None, headers=kwargs["headers"], post_params=None, body=kwargs["body"]) 45 | 46 | 47 | class MockRestApiClient(object): 48 | 49 | def __init__(self, request): 50 | self.request = request 51 | self.rest_client = request 52 | 53 | def call_api(self): 54 | pass 55 | 56 | 57 | class MockApiClient(object): 58 | 59 | def __init__(self, configuration=None, header_name=None, header_value=None, 60 | cookie=None): 61 | json_config = json.loads(get_mastercard_config_for_test()) 62 | json_config["paths"]["$"]["toEncrypt"] = {"data": "encryptedData"} 63 | json_config["paths"]["$"]["toDecrypt"] = {"encryptedData": "data"} 64 | self.rest_client = self 65 | self._config = encryption_config.FieldLevelEncryptionConfig(json_config) 66 | 67 | @mock_signing 68 | def request(self, method, url, query_params=None, headers=None, 69 | post_params=None, body=None, _preload_content=True, 70 | _request_timeout=None): 71 | check = -1 72 | 73 | if body: 74 | if url == "testservice/headers": 75 | iv = headers["x-iv"] 76 | encrypted_key = headers["x-key"] 77 | oaep_digest_algo = headers["x-oaep-digest"] if "x-oaep-digest" in headers else None 78 | 79 | params = SessionKeyParams(self._config, encrypted_key, iv, oaep_digest_algo) 80 | else: 81 | params = None 82 | 83 | plain = encryption.decrypt_payload(body, self._config, params) 84 | check = plain["data"]["secret2"] - plain["data"]["secret1"] 85 | res = {"data": {"secret": check}} 86 | else: 87 | res = {"data": {"secret": [53, 84, 75]}} 88 | 89 | if url == "testservice/headers" and method in ["GET", "POST", "PUT"]: 90 | params = SessionKeyParams.generate(self._config) 91 | json_resp = encryption.encrypt_payload(res, self._config, params) 92 | 93 | response_headers = {"Content-Type": "application/json", 94 | "x-iv": params.iv_value, 95 | "x-key": params.encrypted_key_value, 96 | "x-oaep-digest": self._config.oaep_padding_digest_algorithm 97 | } 98 | mock_headers = Mock(return_value=response_headers) 99 | else: 100 | json_resp = encryption.encrypt_payload(res, self._config) 101 | mock_headers = Mock(return_value={"Content-Type": "application/json"}) 102 | 103 | response = Mock() 104 | response.status = 200 105 | response.getheaders = mock_headers 106 | 107 | if method in ["GET", "POST", "PUT"]: 108 | response.response.data = json_resp 109 | else: 110 | response.response.data = "OK" if check == 0 else "KO" 111 | 112 | return response 113 | 114 | def call_api(self, resource_path, method, 115 | path_params=None, query_params=None, header_params=None, 116 | body=None, post_params=None, files=None, 117 | response_type=None, auth_settings=None, async_req=None, 118 | _return_http_data_only=None, collection_formats=None, 119 | _preload_content=True, _request_timeout=None, _check_type=None): 120 | pass 121 | --------------------------------------------------------------------------------