diff --git a/Adyen/__init__.py b/Adyen/__init__.py index 5b6ad1c8..e50d5944 100644 --- a/Adyen/__init__.py +++ b/Adyen/__init__.py @@ -3,7 +3,6 @@ from __future__ import absolute_import, division, unicode_literals from . import util -from .util import generate_hpp_sig from .exceptions import ( AdyenAPICommunicationError, AdyenAPIAuthenticationError, diff --git a/Adyen/util.py b/Adyen/util.py index faaed913..347fc07b 100644 --- a/Adyen/util.py +++ b/Adyen/util.py @@ -1,66 +1,16 @@ from __future__ import absolute_import, division, unicode_literals -from itertools import chain -from collections import OrderedDict import base64 import hmac import hashlib import binascii -def generate_hpp_sig(dict_object, hmac_key): - if 'issuerId' in dict_object: - if dict_object['issuerId'] == "": - del dict_object['issuerId'] - - if not isinstance(dict_object, dict): - raise ValueError("Must Provide dictionary object") - - def escape_val(val): - if isinstance(val, int): - return val - return val.replace('\\', '\\\\').replace(':', '\\:') - - hmac_key = binascii.a2b_hex(hmac_key) - - ordered_request = OrderedDict(sorted(dict_object.items(), - key=lambda t: t[0])) - - signing_string = ':'.join( - map(escape_val, chain(map(str, ordered_request.keys()), - map(str, ordered_request.values())))) - - hm = hmac.new(hmac_key, signing_string.encode('utf-8'), hashlib.sha256) - return base64.b64encode(hm.digest()) - - -def is_valid_hmac(dict_object, hmac_key): - dict_object = dict_object.copy() - - if 'additionalData' in dict_object: - if dict_object['additionalData']['hmacSignature'] == "": - raise ValueError("Must Provide hmacSignature in additionalData") - else: - expected_sign = dict_object['additionalData']['hmacSignature'] - del dict_object['additionalData'] - merchant_sign = generate_hpp_sig(dict_object, hmac_key) - merchant_sign_str = merchant_sign.decode("utf-8") - return hmac.compare_digest(merchant_sign_str, expected_sign) - - def generate_notification_sig(dict_object, hmac_key): - if 'issuerId' in dict_object: - if dict_object['issuerId'] == "": - del dict_object['issuerId'] if not isinstance(dict_object, dict): raise ValueError("Must Provide dictionary object") - def escape_val(val): - if isinstance(val, int): - return val - return val.replace('\\', '\\\\') - hmac_key = binascii.a2b_hex(hmac_key) request_dict = dict(dict_object) @@ -78,9 +28,7 @@ def escape_val(val): 'success', ] - signing_string = ':'.join( - map(escape_val, map(str, ( - request_dict.get(element, '') for element in element_orders)))) + signing_string = ':'.join(map(str, (request_dict.get(element, '') for element in element_orders))) hm = hmac.new(hmac_key, signing_string.encode('utf-8'), hashlib.sha256) return base64.b64encode(hm.digest()) diff --git a/test/UtilTest.py b/test/UtilTest.py index 4aee2a49..0efc8101 100644 --- a/test/UtilTest.py +++ b/test/UtilTest.py @@ -1,9 +1,7 @@ import unittest - +from json import load import Adyen from Adyen.util import ( - generate_hpp_sig, - is_valid_hmac, generate_notification_sig, is_valid_hmac_notification, get_query @@ -14,32 +12,6 @@ class UtilTest(unittest.TestCase): ady = Adyen.Adyen() client = ady.client - def test_hpp_request_item_hmac(self): - request = { - "pspReference": "pspReference", - "originalReference": "originalReference", - "merchantAccount": "merchantAccount", - "amount": { - "currency": "EUR", - "value": 100000 - }, - "eventCode": "EVENT", - "Success": "true" - } - key = "DFB1EB5485895CFA84146406857104AB" \ - "B4CBCABDC8AAF103A624C8F6A3EAAB00" - hmac_calculation = generate_hpp_sig(request, key) - hmac_calculation_str = hmac_calculation.decode("utf-8") - expected_hmac = "+xK25vgc9XcZFwu7WNLIwqVewyumVsgp+X+C0a2e+DE=" - self.assertTrue(hmac_calculation_str != "") - self.assertEqual(hmac_calculation_str, expected_hmac) - request['additionalData'] = {'hmacSignature': hmac_calculation_str} - hmac_validate = is_valid_hmac(request, key) - self.assertIn('additionalData', request) - self.assertDictEqual(request['additionalData'], - {'hmacSignature': hmac_calculation_str}) - self.assertTrue(hmac_validate) - def test_notification_request_item_hmac(self): request = { "pspReference": "7914073381342284", @@ -73,6 +45,22 @@ def test_notification_request_item_hmac(self): {'hmacSignature': hmac_calculation_str}) self.assertTrue(hmac_validate) + def test_notifications_with_slashes(self): + hmac_key = "74F490DD33F7327BAECC88B2947C011FC02D014A473AAA33A8EC93E4DC069174" + with open('test/mocks/util/backslash_notification.json') as file: + backslash_notification = load(file) + self.assertTrue(is_valid_hmac_notification(backslash_notification, hmac_key)) + with open('test/mocks/util/colon_notification.json') as file: + colon_notification = load(file) + self.assertTrue(is_valid_hmac_notification(colon_notification, hmac_key)) + with open('test/mocks/util/forwardslash_notification.json') as file: + forwardslash_notification = load(file) + self.assertTrue(is_valid_hmac_notification(forwardslash_notification, hmac_key)) + with open('test/mocks/util/mixed_notification.json') as file: + mixed_notification = load(file) + self.assertTrue(is_valid_hmac_notification(mixed_notification, hmac_key)) + + def test_query_string_creation(self): query_parameters = { "pageSize":7, diff --git a/test/mocks/util/backslash_notification.json b/test/mocks/util/backslash_notification.json new file mode 100644 index 00000000..f183465e --- /dev/null +++ b/test/mocks/util/backslash_notification.json @@ -0,0 +1,41 @@ +{ + "additionalData": { + "acquirerCode": "TestPmmAcquirer", + "acquirerReference": "DZMKWLXW6N6", + "authCode": "076181", + "avsResult": "5 No AVS data provided", + "avsResultRaw": "5", + "cardSummary": "1111", + "checkout.cardAddedBrand": "visa", + "cvcResult": "1 Matches", + "cvcResultRaw": "M", + "expiryDate": "03/2030", + "hmacSignature": "nIgT81gaB5oJpn2jPXupDq68iRo2wUlBsuYjtYfwKqo=", + "paymentMethod": "visa", + "refusalReasonRaw": "AUTHORISED", + "retry.attempt1.acquirer": "TestPmmAcquirer", + "retry.attempt1.acquirerAccount": "TestPmmAcquirerAccount", + "retry.attempt1.avsResultRaw": "5", + "retry.attempt1.rawResponse": "AUTHORISED", + "retry.attempt1.responseCode": "Approved", + "retry.attempt1.scaExemptionRequested": "lowValue", + "scaExemptionRequested": "lowValue" + }, + "amount": { + "currency": "EUR", + "value": 1000 + }, + "eventCode": "AUTHORISATION", + "eventDate": "2023-01-09T16:27:29+01:00", + "merchantAccountCode": "AntoniStroinski", + "merchantReference": "\\\\slashes are fun", + "operations": [ + "CANCEL", + "CAPTURE", + "REFUND" + ], + "paymentMethod": "visa", + "pspReference": "T7FD4VM4D3RZNN82", + "reason": "076181:1111:03/2030", + "success": "true" +} \ No newline at end of file diff --git a/test/mocks/util/colon_notification.json b/test/mocks/util/colon_notification.json new file mode 100644 index 00000000..839baf76 --- /dev/null +++ b/test/mocks/util/colon_notification.json @@ -0,0 +1,41 @@ +{ + "additionalData": { + "acquirerCode": "TestPmmAcquirer", + "acquirerReference": "8NQH5BNF58M", + "authCode": "039404", + "avsResult": "5 No AVS data provided", + "avsResultRaw": "5", + "cardSummary": "1111", + "checkout.cardAddedBrand": "visa", + "cvcResult": "1 Matches", + "cvcResultRaw": "M", + "expiryDate": "03/2030", + "hmacSignature": "2EQYm7YJpKO4EtHSPu55SQTyWf8dkW5u2nD1tJFpViA=", + "paymentMethod": "visa", + "refusalReasonRaw": "AUTHORISED", + "retry.attempt1.acquirer": "TestPmmAcquirer", + "retry.attempt1.acquirerAccount": "TestPmmAcquirerAccount", + "retry.attempt1.avsResultRaw": "5", + "retry.attempt1.rawResponse": "AUTHORISED", + "retry.attempt1.responseCode": "Approved", + "retry.attempt1.scaExemptionRequested": "lowValue", + "scaExemptionRequested": "lowValue" + }, + "amount": { + "currency": "EUR", + "value": 1000 + }, + "eventCode": "AUTHORISATION", + "eventDate": "2023-01-10T13:40:54+01:00", + "merchantAccountCode": "AntoniStroinski", + "merchantReference": ":slashes are fun", + "operations": [ + "CANCEL", + "CAPTURE", + "REFUND" + ], + "paymentMethod": "visa", + "pspReference": "M8NB66SBZSGLNK82", + "reason": "039404:1111:03/2030", + "success": "true" +} \ No newline at end of file diff --git a/test/mocks/util/forwardslash_notification.json b/test/mocks/util/forwardslash_notification.json new file mode 100644 index 00000000..bf9eb17d --- /dev/null +++ b/test/mocks/util/forwardslash_notification.json @@ -0,0 +1,41 @@ +{ + "amount": { + "value": 1000, + "currency": "EUR" + }, + "reason": "087330:1111:03/2030", + "success": "true", + "eventCode": "AUTHORISATION", + "eventDate": "2023-01-10T13:37:30+01:00", + "operations": [ + "CANCEL", + "CAPTURE", + "REFUND" + ], + "pspReference": "X3GWNS6KJ8NKGK82", + "paymentMethod": "visa", + "additionalData": { + "authCode": "087330", + "avsResult": "5 No AVS data provided", + "cvcResult": "1 Matches", + "expiryDate": "03/2030", + "cardSummary": "1111", + "acquirerCode": "TestPmmAcquirer", + "avsResultRaw": "5", + "cvcResultRaw": "M", + "hmacSignature": "9Z0xdpG9Xi3zcmXv14t/BvMBut77O/Xq9D4CQXSDUi4=", + "paymentMethod": "visa", + "refusalReasonRaw": "AUTHORISED", + "acquirerReference": "HHCCC326PH6", + "scaExemptionRequested": "lowValue", + "checkout.cardAddedBrand": "visa", + "retry.attempt1.acquirer": "TestPmmAcquirer", + "retry.attempt1.rawResponse": "AUTHORISED", + "retry.attempt1.avsResultRaw": "5", + "retry.attempt1.responseCode": "Approved", + "retry.attempt1.acquirerAccount": "TestPmmAcquirerAccount", + "retry.attempt1.scaExemptionRequested": "lowValue" + }, + "merchantReference": "//slashes are fun", + "merchantAccountCode": "AntoniStroinski" +} \ No newline at end of file diff --git a/test/mocks/util/mixed_notification.json b/test/mocks/util/mixed_notification.json new file mode 100644 index 00000000..cbcfde3c --- /dev/null +++ b/test/mocks/util/mixed_notification.json @@ -0,0 +1,41 @@ +{ + "additionalData": { + "acquirerCode": "TestPmmAcquirer", + "acquirerReference": "J8DXDJ2PV6P", + "authCode": "052095", + "avsResult": "5 No AVS data provided", + "avsResultRaw": "5", + "cardSummary": "1111", + "checkout.cardAddedBrand": "visa", + "cvcResult": "1 Matches", + "cvcResultRaw": "M", + "expiryDate": "03/2030", + "hmacSignature": "CZErGCNQaSsxbaQfZaJlakqo7KPP+mIa8a+wx3yNs9A=", + "paymentMethod": "visa", + "refusalReasonRaw": "AUTHORISED", + "retry.attempt1.acquirer": "TestPmmAcquirer", + "retry.attempt1.acquirerAccount": "TestPmmAcquirerAccount", + "retry.attempt1.avsResultRaw": "5", + "retry.attempt1.rawResponse": "AUTHORISED", + "retry.attempt1.responseCode": "Approved", + "retry.attempt1.scaExemptionRequested": "lowValue", + "scaExemptionRequested": "lowValue" + }, + "amount": { + "currency": "EUR", + "value": 1000 + }, + "eventCode": "AUTHORISATION", + "eventDate": "2023-01-10T13:42:29+01:00", + "merchantAccountCode": "AntoniStroinski", + "merchantReference": "\\:/\\/slashes are fun", + "operations": [ + "CANCEL", + "CAPTURE", + "REFUND" + ], + "paymentMethod": "visa", + "pspReference": "ZVWN7D3WSMK2WN82", + "reason": "052095:1111:03/2030", + "success": "true" +} \ No newline at end of file