Skip to content

Commit c9b8984

Browse files
authored
Merge pull request #14 from Mastercard/fix_array_payload
Fix array payload issue
2 parents ff95dcf + 1ddc9e1 commit c9b8984

File tree

6 files changed

+240
-15
lines changed

6 files changed

+240
-15
lines changed

client_encryption/field_level_encryption.py

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

1414
try:
15-
json_payload = copy.deepcopy(payload) if type(payload) is dict else json.loads(payload)
15+
json_payload = copy.deepcopy(payload) if type(payload) is dict or type(payload) is list else json.loads(payload)
1616

1717
for elem, target in config.paths["$"].to_encrypt.items():
1818
if not _params:
@@ -47,7 +47,7 @@ def decrypt_payload(payload, config, _params=None):
4747
"""Decrypt some fields of a JSON payload using the given configuration."""
4848

4949
try:
50-
json_payload = payload if type(payload) is dict else json.loads(payload)
50+
json_payload = payload if type(payload) is dict or type(payload) is list else json.loads(payload)
5151

5252
for elem, target in config.paths["$"].to_decrypt.items():
5353
try:

client_encryption/json_path_utils.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,16 @@ def update_node(tree, path, node_str):
3232
if __not_root(path):
3333
parent = path.split(_SEPARATOR)
3434
to_set = parent.pop()
35-
if parent:
36-
current_node = __get_node(tree, parent, False)
37-
else:
38-
current_node = tree
35+
current_node = __get_node(tree, parent, False) if parent else tree
3936

4037
try:
4138
node_json = json.loads(node_str)
4239
except json.JSONDecodeError:
4340
node_json = node_str
4441

45-
if to_set in current_node and type(current_node[to_set]) is dict and type(node_json) is dict:
42+
if type(current_node) is list:
43+
update_node_list(to_set, current_node, node_json)
44+
elif to_set in current_node and type(current_node[to_set]) is dict and type(node_json) is dict:
4645
current_node[to_set].update(node_json)
4746
else:
4847
current_node[to_set] = node_json
@@ -53,6 +52,13 @@ def update_node(tree, path, node_str):
5352
return tree
5453

5554

55+
def update_node_list(to_set, current_node, node_json):
56+
if to_set in current_node[0] and type(current_node[0][to_set]) is dict and type(node_json) is dict:
57+
current_node[0][to_set].update(node_json)
58+
else:
59+
current_node[0][to_set] = node_json
60+
61+
5662
def pop_node(tree, path):
5763
"""Retrieve and delete json or value given a path"""
5864

@@ -66,7 +72,10 @@ def pop_node(tree, path):
6672
else:
6773
node = tree
6874

69-
deleted_elem = node.pop(to_delete)
75+
if type(node) is list:
76+
deleted_elem = node[0].pop(to_delete)
77+
else:
78+
deleted_elem = node.pop(to_delete)
7079
if isinstance(deleted_elem, str):
7180
return deleted_elem
7281
else:
@@ -91,8 +100,9 @@ def cleanup_node(tree, path, target):
91100
node = __get_node(tree, parent, False)
92101
else:
93102
node = tree
94-
95-
if not node[to_delete]:
103+
if type(node) is list and not node[0][to_delete]:
104+
del node[0][to_delete]
105+
elif not node[to_delete]:
96106
del node[to_delete]
97107

98108
else:
@@ -107,12 +117,23 @@ def __get_node(tree, node_list, create):
107117
last_node = node_list.pop()
108118

109119
for node in node_list:
110-
current = current[node]
120+
if type(current) is list:
121+
current = current[0][node]
122+
else:
123+
current = current[node]
111124

112-
if type(current) is not dict:
125+
if type(current) is not dict and type(current) is not list:
113126
raise ValueError("'" + current + "' is not of dict type")
114127

115-
if last_node not in current and create:
128+
if type(current) is list:
129+
if not current and create:
130+
d = dict()
131+
d[last_node] = {}
132+
current.append(d)
133+
elif last_node not in current[0] and create:
134+
current[0][last_node] = {}
135+
return current[0][last_node]
136+
elif last_node not in current and create:
116137
current[last_node] = {}
117138

118139
return current[last_node]

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pycryptodome==3.8.1
2-
pyOpenSSL==19.0.0
2+
pyOpenSSL==22.0.0
33
setuptools>=39.0.1
44
coverage>=4.5.3
5+
cryptography==37.0.4

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@
2222
'Topic :: Software Development :: Libraries :: Python Modules'
2323
],
2424
tests_require=['coverage'],
25-
install_requires=['pycryptodome>=3.8.1', 'pyOpenSSL>=19.0.0', 'setuptools>=39.0.1']
25+
install_requires=['pycryptodome>=3.8.1', 'pyOpenSSL>=22.0.0', 'setuptools>=39.0.1', 'cryptography==37.0.4']
2626
)

tests/test_field_level_encryption.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,20 @@ def __assert_payload_encrypted(self, payload, encrypted, config):
7878
del payload["encryptedData"]
7979
self.assertEqual(payload, to_test.decrypt_payload(encrypted, config))
8080

81+
def __assert_array_payload_encrypted(self, payload, encrypted, config):
82+
self.assertNotIn("data", encrypted[0])
83+
self.assertIn("encryptedData", encrypted[0])
84+
enc_data = encrypted[0]["encryptedData"]
85+
self.assertEqual(6, len(enc_data.keys()))
86+
self.assertIsNotNone(enc_data["iv"])
87+
self.assertIsNotNone(enc_data["encryptedKey"])
88+
self.assertIsNotNone(enc_data["encryptedValue"])
89+
self.assertEqual("SHA256", enc_data["oaepHashingAlgo"])
90+
self.assertEqual("761b003c1eade3a5490e5000d37887baa5e6ec0e226c07706e599451fc032a79", enc_data["keyFingerprint"])
91+
self.assertEqual("80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279", enc_data["certFingerprint"])
92+
del payload[0]["encryptedData"]
93+
self.assertEqual(payload, to_test.decrypt_payload(encrypted, config))
94+
8195
def test_encrypt_payload_base64_field_encoding(self):
8296
payload = {
8397
"data": {
@@ -155,6 +169,155 @@ def test_encrypt_payload_with_type_list(self):
155169
encrypted_payload = to_test.encrypt_payload(payload, self._config)
156170
self.__assert_payload_encrypted(payload, encrypted_payload, self._config)
157171

172+
def test_encrypt_array_payload_with_type_string(self):
173+
payload = [{
174+
"data": "item1",
175+
"encryptedData": {}
176+
}]
177+
178+
encrypted_payload = to_test.encrypt_payload(payload, self._config)
179+
self.__assert_array_payload_encrypted(payload, encrypted_payload, self._config)
180+
181+
def test_encrypt_array_payload_with_type_list(self):
182+
payload = [{
183+
"data": ["item1", "item2", "item3"],
184+
"encryptedData": {}
185+
}]
186+
187+
encrypted_payload = to_test.encrypt_payload(payload, self._config)
188+
self.__assert_array_payload_encrypted(payload, encrypted_payload, self._config)
189+
190+
def test_encrypt_array_payload_with_type_object(self):
191+
192+
payload = [{
193+
"data": {
194+
"field1": "value1",
195+
"field2": "value2"
196+
},
197+
"encryptedData": {}
198+
}]
199+
200+
encrypted_payload = to_test.encrypt_payload(payload, self._config)
201+
self.__assert_array_payload_encrypted(payload, encrypted_payload, self._config)
202+
203+
def test_encrypt_array_payload_with_type_multiple_object(self):
204+
205+
payload = [{
206+
"data": {
207+
"field1": "value1",
208+
"field2": "value2"
209+
},
210+
"encryptedData": {}
211+
},
212+
{
213+
"data": {
214+
"field1": "value1",
215+
"field2": "value2"
216+
},
217+
"encryptedData": {}
218+
}
219+
]
220+
221+
encrypted_payload = to_test.encrypt_payload(payload, self._config)
222+
self.__assert_array_payload_encrypted(payload, encrypted_payload, self._config)
223+
224+
def test_encrypt_array_payload_skip_when_in_path_does_not_exist(self):
225+
payload = [{
226+
"dataNotToEncrypt": {
227+
"field1": "value1",
228+
"field2": "value2"
229+
},
230+
"encryptedData": {}
231+
}]
232+
233+
encrypted_payload = to_test.encrypt_payload(payload, self._config)
234+
235+
self.assertEqual(payload, encrypted_payload)
236+
237+
def test_encrypt_array_payload_create_node_when_out_path_parent_exists(self):
238+
self._config._paths["$"]._to_encrypt = {"data": "encryptedDataParent.encryptedData"}
239+
240+
payload = [{
241+
"data": {
242+
"field1": "value1",
243+
"field2": "value2"
244+
},
245+
"encryptedDataParent": {}
246+
}]
247+
248+
encrypted_payload = to_test.encrypt_payload(payload, self._config)
249+
250+
self.assertNotIn("data", encrypted_payload[0])
251+
self.assertIn("encryptedDataParent", encrypted_payload[0])
252+
self.assertIn("encryptedData", encrypted_payload[0]["encryptedDataParent"])
253+
254+
def test_encrypt_array_payload_with_multiple_encryption_paths(self):
255+
self._config._paths["$"]._to_encrypt = {"data1": "encryptedData1", "data2": "encryptedData2"}
256+
257+
payload = [{
258+
"data1": {
259+
"field1": "value1",
260+
"field2": "value2"
261+
},
262+
"data2": {
263+
"field3": "value3",
264+
"field4": "value4"
265+
},
266+
"encryptedData1": {},
267+
"encryptedData2": {}
268+
}]
269+
270+
encrypted_payload = to_test.encrypt_payload(payload, self._config)
271+
272+
self.assertNotIn("data1", encrypted_payload[0])
273+
self.assertNotIn("data2", encrypted_payload[0])
274+
enc_data1 = encrypted_payload[0]["encryptedData1"]
275+
enc_data2 = encrypted_payload[0]["encryptedData2"]
276+
self.assertIsNotNone(enc_data1["iv"])
277+
self.assertIsNotNone(enc_data1["encryptedKey"])
278+
self.assertIsNotNone(enc_data1["encryptedValue"])
279+
self.assertIsNotNone(enc_data2["iv"])
280+
self.assertIsNotNone(enc_data2["encryptedKey"])
281+
self.assertIsNotNone(enc_data2["encryptedValue"])
282+
self.assertNotEqual(enc_data1["iv"], enc_data2["iv"], "using same set of params")
283+
284+
def test_encrypt_array_payload_when_root_as_in_path(self):
285+
self._config._paths["$"]._to_encrypt = {"$": "encryptedData"}
286+
287+
payload = [{
288+
"field1": "value1",
289+
"field2": "value2"
290+
}]
291+
292+
encrypted_payload = to_test.encrypt_payload(payload, self._config)
293+
294+
self.assertNotIn("field1", encrypted_payload[0])
295+
self.assertNotIn("field2", encrypted_payload[0])
296+
self.assertIn("encryptedData", encrypted_payload[0])
297+
self.assertEqual(6, len(encrypted_payload[0]["encryptedData"].keys()))
298+
299+
def test_encrypt_array_payload_when_out_path_same_as_in_path(self):
300+
self._config._paths["$"]._to_encrypt = {"data": "data"}
301+
302+
payload = [{
303+
"data": {
304+
"field1": "value1",
305+
"field2": "value2"
306+
}
307+
}]
308+
309+
encrypted_payload = to_test.encrypt_payload(payload, self._config)
310+
311+
self.assertIn("data", encrypted_payload[0])
312+
self.assertNotIn("field1", encrypted_payload[0]["data"])
313+
self.assertNotIn("field2", encrypted_payload[0]["data"])
314+
self.assertIn("iv", encrypted_payload[0]["data"])
315+
self.assertIn("encryptedKey", encrypted_payload[0]["data"])
316+
self.assertIn("encryptedValue", encrypted_payload[0]["data"])
317+
self.assertIn("certFingerprint", encrypted_payload[0]["data"])
318+
self.assertIn("keyFingerprint", encrypted_payload[0]["data"])
319+
self.assertIn("oaepHashingAlgo", encrypted_payload[0]["data"])
320+
158321
def test_encrypt_payload_skip_when_in_path_does_not_exist(self):
159322
payload = {
160323
"dataNotToEncrypt": {

tests/test_json_path_utils.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,23 @@ def __get_sample_json():
2121
}
2222
}
2323

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+
2441
def test_get_node(self):
2542
sample_json = self.__get_sample_json()
2643

@@ -151,6 +168,29 @@ def test_update_node_not_json(self):
151168

152169
self.assertIsInstance(node["node1"]["node2"], str, "not a json string")
153170

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+
154194
def test_update_node_primitive_type(self):
155195
sample_json = self.__get_sample_json()
156196

0 commit comments

Comments
 (0)