Skip to content

Commit 4f780a7

Browse files
authored
Merge pull request #4 from Mastercard/feature/decrypt_issue
Decrypt issue
2 parents 3a7aac2 + c9b5781 commit 4f780a7

10 files changed

+90
-55
lines changed

client_encryption/api_encryption.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,23 @@ def __init__(self, encryption_conf_file):
1818
self._encryption_conf = FieldLevelEncryptionConfig(json_file.read())
1919

2020
def field_encryption(self, func):
21-
"""Decorator for API request. func is APIClient.request"""
21+
"""Decorator for API call_api. func is APIClient.call_api"""
2222

2323
@wraps(func)
24-
def request_function(*args, **kwargs):
25-
"""Wrap request and add field encryption layer to it."""
24+
def call_api_function(*args, **kwargs):
25+
"""Wrap call_api and add field encryption layer to it."""
2626

2727
in_body = kwargs.get("body", None)
28-
kwargs["body"] = self._encrypt_payload(kwargs.get("headers", None), in_body) if in_body else in_body
28+
kwargs["body"] = self._encrypt_payload(kwargs.get("header_params", None), in_body) if in_body else in_body
29+
kwargs["_preload_content"] = False
2930

3031
response = func(*args, **kwargs)
31-
32-
if type(response.data) is not str:
33-
response_body = self._decrypt_payload(response.getheaders(), response.json())
34-
response._content = json.dumps(response_body, indent=4).encode('utf-8')
32+
response._body = self._decrypt_payload(response.getheaders(), response.data)
3533

3634
return response
3735

38-
request_function.__fle__ = True
39-
return request_function
36+
call_api_function.__fle__ = True
37+
return call_api_function
4038

4139
def _encrypt_payload(self, headers, body):
4240
"""Encryption enforcement based on configuration - encrypt and add session key params to header or body"""
@@ -62,6 +60,7 @@ def _decrypt_payload(self, headers, body):
6260
"""Encryption enforcement based on configuration - decrypt using session key params from header or body"""
6361

6462
conf = self._encryption_conf
63+
params = None
6564

6665
if conf.use_http_headers:
6766
if conf.iv_field_name in headers and conf.encrypted_key_field_name in headers:
@@ -75,30 +74,28 @@ def _decrypt_payload(self, headers, body):
7574
del headers[conf.encryption_key_fingerprint_field_name]
7675

7776
params = SessionKeyParams(conf, encrypted_key, iv, oaep_digest_algo)
78-
payload = decrypt_payload(body, conf, params)
7977
else:
80-
# skip decryption if not iv nor key is in headers
81-
payload = body
82-
else:
83-
payload = decrypt_payload(body, conf)
78+
# skip decryption and return original body if not iv nor key is in headers
79+
return body
80+
81+
decrypted_body = decrypt_payload(body, conf, params)
82+
payload = json.dumps(decrypted_body).encode('utf-8')
8483

8584
return payload
8685

8786

8887
def add_encryption_layer(api_client, encryption_conf_file):
89-
"""Decorate APIClient.request with field level encryption"""
88+
"""Decorate APIClient.call_api with field level encryption"""
9089

9190
api_encryption = ApiEncryption(encryption_conf_file)
92-
api_client.request = api_encryption.field_encryption(api_client.request)
91+
api_client.call_api = api_encryption.field_encryption(api_client.call_api)
9392

9493
__check_oauth(api_client) # warn the user if authentication layer is missing/not set
9594

9695

9796
def __check_oauth(api_client):
9897
try:
99-
oauth_layer = getattr(api_client.request, "__wrapped__").__oauth__
100-
if not oauth_layer or type(oauth_layer) is not bool:
101-
__oauth_warn()
98+
api_client.request.__wrapped__
10299
except AttributeError:
103100
__oauth_warn()
104101

client_encryption/encryption_utils.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,21 @@ def __get_crypto_file_type(file_content):
5454
return FILETYPE_ASN1
5555

5656

57-
def load_hash_algorithm(algo_str):
58-
"""Load a hash algorithm object of Crypto.Hash from a list of supported ones."""
57+
def validate_hash_algorithm(algo_str):
58+
"""Validate a hash algorithm against a list of supported ones."""
5959

6060
if algo_str:
6161
algo_key = algo_str.replace("-", "").upper()
6262

6363
if algo_key in _SUPPORTED_HASH:
64-
return _SUPPORTED_HASH[algo_key]
64+
return algo_key
6565
else:
6666
raise HashAlgorithmError("Hash algorithm invalid or not supported.")
6767
else:
6868
raise HashAlgorithmError("No hash algorithm provided.")
69+
70+
71+
def load_hash_algorithm(algo_str):
72+
"""Load a hash algorithm object of Crypto.Hash from a list of supported ones."""
73+
74+
return _SUPPORTED_HASH[validate_hash_algorithm(algo_str)]

client_encryption/field_level_encryption.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ def encrypt_payload(payload, config, _params=None):
1212
"""Encrypt some fields of a JSON payload using the given configuration."""
1313

1414
try:
15-
if type(payload) is str:
16-
json_payload = json.loads(payload)
17-
else:
15+
if type(payload) is dict:
1816
json_payload = copy.deepcopy(payload)
17+
else:
18+
json_payload = json.loads(payload)
1919

2020
for elem, target in config.paths["$"].to_encrypt.items():
2121
if not _params:
@@ -50,10 +50,13 @@ def decrypt_payload(payload, config, _params=None):
5050
"""Decrypt some fields of a JSON payload using the given configuration."""
5151

5252
try:
53-
if type(payload) is str:
54-
json_payload = json.loads(payload)
55-
else:
53+
if type(payload) is dict:
5654
json_payload = payload
55+
else:
56+
try:
57+
json_payload = json.loads(payload)
58+
except json.JSONDecodeError: # not a json response - return it as is
59+
return payload
5760

5861
for elem, target in config.paths["$"].to_decrypt.items():
5962
try:
@@ -90,7 +93,7 @@ def decrypt_payload(payload, config, _params=None):
9093
return json_payload
9194

9295
except (IOError, ValueError, TypeError) as e:
93-
raise EncryptionError("Payload encryption failed!", e)
96+
raise EncryptionError("Payload decryption failed!", e)
9497

9598

9699
def _encrypt_value(_key, iv, node_str):

client_encryption/field_level_encryption_config.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from OpenSSL.crypto import dump_certificate, FILETYPE_ASN1, dump_publickey
33
from Crypto.Hash import SHA256
44
from client_encryption.encoding_utils import Encoding
5-
from client_encryption.encryption_utils import load_encryption_certificate, load_decryption_key, load_hash_algorithm
5+
from client_encryption.encryption_utils import load_encryption_certificate, load_decryption_key, validate_hash_algorithm
66

77

88
class FieldLevelEncryptionConfig(object):
@@ -44,9 +44,7 @@ def __init__(self, conf):
4444
else:
4545
self._decryption_key = None
4646

47-
digest_algo = json_config["oaepPaddingDigestAlgorithm"]
48-
if load_hash_algorithm(digest_algo) is not None:
49-
self._oaep_padding_digest_algorithm = digest_algo
47+
self._oaep_padding_digest_algorithm = validate_hash_algorithm(json_config["oaepPaddingDigestAlgorithm"])
5048

5149
data_enc = Encoding(json_config["dataEncoding"].upper())
5250
self._data_encoding = data_enc

client_encryption/session_key_params.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from binascii import Error
22
from Crypto.Cipher import PKCS1_OAEP, AES
3-
from Crypto import Random
3+
from Crypto.Random import get_random_bytes
44
from Crypto.PublicKey import RSA
55
from client_encryption.encoding_utils import encode_bytes, decode_value
66
from client_encryption.encryption_utils import load_hash_algorithm
@@ -62,11 +62,11 @@ def generate(config):
6262
encoding = config.data_encoding
6363

6464
# Generate a random IV
65-
iv = Random.new().read(SessionKeyParams._BLOCK_SIZE)
65+
iv = get_random_bytes(SessionKeyParams._BLOCK_SIZE)
6666
iv_encoded = encode_bytes(iv, encoding)
6767

6868
# Generate an AES secret key
69-
secret_key = Random.new().read(SessionKeyParams._KEY_SIZE)
69+
secret_key = get_random_bytes(SessionKeyParams._KEY_SIZE)
7070

7171
# Encrypt the secret key
7272
secret_key_encrypted = SessionKeyParams.__wrap_secret_key(secret_key, config)

client_encryption/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
3-
__version__ = "1.0.3"
3+
__version__ = "1.1.0"

tests/test_api_encryption.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@ def test_decrypt_payload_with_params_in_body(self):
6262

6363
test_headers = {"Content-Type": "application/json"}
6464

65-
decrypted = api_encryption._decrypt_payload(body={
65+
decrypted = json.loads(api_encryption._decrypt_payload(body={
6666
"encryptedData": {
6767
"iv": "uldLBySPY3VrznePihFYGQ==",
6868
"encryptedKey": "Jmh/bQPScUVFHSC9qinMGZ4lM7uetzUXcuMdEpC5g4C0Pb9HuaM3zC7K/509n7RTBZUPEzgsWtgi7m33nhpXsUo8WMcQkBIZlKn3ce+WRyZpZxcYtVoPqNn3benhcv7cq7yH1ktamUiZ5Dq7Ga+oQCaQEsOXtbGNS6vA5Bwa1pjbmMiRIbvlstInz8XTw8h/T0yLBLUJ0yYZmzmt+9i8qL8KFQ/PPDe5cXOCr1Aq2NTSixe5F2K/EI00q6D7QMpBDC7K6zDWgAOvINzifZ0DTkxVe4EE6F+FneDrcJsj+ZeIabrlRcfxtiFziH6unnXktta0sB1xcszIxXdMDbUcJA==",
6969
"encryptedValue": "KGfmdUWy89BwhQChzqZJ4w==",
7070
"oaepHashingAlgo": "SHA256"
7171
}
72-
}, headers=test_headers)
72+
}, headers=test_headers))
7373

7474
self.assertNotIn("encryptedData", decrypted)
7575
self.assertDictEqual({"data": {}}, decrypted)
@@ -112,11 +112,11 @@ def test_decrypt_payload_with_params_in_headers(self):
112112
}
113113

114114
api_encryption = to_test.ApiEncryption(self._json_config)
115-
decrypted = api_encryption._decrypt_payload(body={
115+
decrypted = json.loads(api_encryption._decrypt_payload(body={
116116
"encryptedData": {
117117
"encryptedValue": "KGfmdUWy89BwhQChzqZJ4w=="
118118
}
119-
}, headers=test_headers)
119+
}, headers=test_headers))
120120

121121
self.assertNotIn("encryptedData", decrypted)
122122
self.assertDictEqual({"data": {}}, decrypted)

tests/test_encryption_utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,30 @@ def test_load_hash_algorithm_underscore(self):
131131
def test_load_hash_algorithm_none(self):
132132
self.assertRaises(HashAlgorithmError, to_test.load_hash_algorithm, None)
133133

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+
134158
@staticmethod
135159
def __strip_key(rsa_key):
136160
return rsa_key.export_key(pkcs=8).decode('utf-8').replace("\n", "")[27:-25]

tests/test_field_level_encryption_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def test_load_config_SHA512_oaep_padding_algorithm(self):
151151
json_conf["oaepPaddingDigestAlgorithm"] = oaep_algo_test
152152

153153
conf = to_test.FieldLevelEncryptionConfig(json_conf)
154-
self.__check_configuration(conf, oaep_algo=oaep_algo_test)
154+
self.__check_configuration(conf, oaep_algo="SHA512")
155155

156156
def test_load_config_wrong_oaep_padding_algorithm(self):
157157
oaep_algo_test = "sha_512"

tests/utils/api_encryption_test_utils.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,22 @@ def __init__(self, api_client=None):
2525
self.api_client = api_client
2626

2727
def do_something_get(self, **kwargs):
28-
return self.api_client.request("GET", "localhost/testservice", headers=kwargs["headers"])
28+
return self.api_client.call_api("testservice", "GET", header_params=kwargs["headers"])
2929

3030
def do_something_post(self, **kwargs):
31-
return self.api_client.request("POST", "localhost/testservice", headers=kwargs["headers"], body=kwargs["body"])
31+
return self.api_client.call_api("testservice", "POST", header_params=kwargs["headers"], body=kwargs["body"])
3232

3333
def do_something_delete(self, **kwargs):
34-
return self.api_client.request("DELETE", "localhost/testservice", headers=kwargs["headers"], body=kwargs["body"])
34+
return self.api_client.call_api("testservice", "DELETE", header_params=kwargs["headers"], body=kwargs["body"])
3535

3636
def do_something_get_use_headers(self, **kwargs):
37-
return self.api_client.request("GET", "localhost/testservice/headers", headers=kwargs["headers"])
37+
return self.api_client.call_api("testservice/headers", "GET", header_params=kwargs["headers"])
3838

3939
def do_something_post_use_headers(self, **kwargs):
40-
return self.api_client.request("POST", "localhost/testservice/headers", headers=kwargs["headers"], body=kwargs["body"])
40+
return self.api_client.call_api("testservice/headers", "POST", header_params=kwargs["headers"], body=kwargs["body"])
4141

4242
def do_something_delete_use_headers(self, **kwargs):
43-
return self.api_client.request("DELETE", "localhost/testservice/headers", headers=kwargs["headers"], body=kwargs["body"])
43+
return self.api_client.call_api("testservice/headers", "DELETE", header_params=kwargs["headers"], body=kwargs["body"])
4444

4545

4646
class MockApiClient(object):
@@ -56,13 +56,21 @@ def __init__(self, configuration=None, header_name=None, header_value=None,
5656
def request(self, method, url, query_params=None, headers=None,
5757
post_params=None, body=None, _preload_content=True,
5858
_request_timeout=None):
59+
pass
60+
61+
def call_api(self, resource_path, method,
62+
path_params=None, query_params=None, header_params=None,
63+
body=None, post_params=None, files=None,
64+
response_type=None, auth_settings=None, async_req=None,
65+
_return_http_data_only=None, collection_formats=None,
66+
_preload_content=True, _request_timeout=None):
5967
check = -1
6068

6169
if body:
62-
if url == "localhost/testservice/headers":
63-
iv = headers["x-iv"]
64-
encrypted_key = headers["x-key"]
65-
oaep_digest_algo = headers["x-oaep-digest"] if "x-oaep-digest" in headers else None
70+
if resource_path == "testservice/headers":
71+
iv = header_params["x-iv"]
72+
encrypted_key = header_params["x-key"]
73+
oaep_digest_algo = header_params["x-oaep-digest"] if "x-oaep-digest" in header_params else None
6674

6775
params = SessionKeyParams(self._config, encrypted_key, iv, oaep_digest_algo)
6876
else:
@@ -74,7 +82,7 @@ def request(self, method, url, query_params=None, headers=None,
7482
else:
7583
res = {"data": {"secret": [53, 84, 75]}}
7684

77-
if url == "localhost/testservice/headers" and method in ["GET", "POST", "PUT"]:
85+
if resource_path == "testservice/headers" and method in ["GET", "POST", "PUT"]:
7886
params = SessionKeyParams.generate(self._config)
7987
json_resp = encryption.encrypt_payload(res, self._config, params)
8088

@@ -94,7 +102,6 @@ def request(self, method, url, query_params=None, headers=None,
94102

95103
if method in ["GET", "POST", "PUT"]:
96104
response.data = json_resp
97-
response.json = Mock(return_value=json_resp)
98105
else:
99106
response.data = "OK" if check == 0 else "KO"
100107

0 commit comments

Comments
 (0)