├── .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/)
3 |
4 | [](https://github.com/Mastercard/client-encryption-python/actions?query=workflow%3A%22Build+%26+Test%22)
5 | [](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-python)
6 | [](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-python)
7 | [](https://sonarcloud.io/dashboard?id=Mastercard_client-encryption-python)
8 | [](https://github.com/Mastercard/client-encryption-python/actions?query=workflow%3A%22broken+links%3F%22)
9 | [](https://pypi.org/project/mastercard-client-encryption)
10 | [](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 |
--------------------------------------------------------------------------------