From f8b0d5683792ebd803a77ccfac75fc06d10cd3b2 Mon Sep 17 00:00:00 2001 From: aseveryn-epam Date: Fri, 6 Jul 2018 01:02:37 +0300 Subject: [PATCH 1/7] Added Transfers endpoint --- CHANGELOG.rst | 7 +- hyperwallet/__init__.py | 3 +- hyperwallet/api.py | 110 +++++++++++++++++++++++++++++++ hyperwallet/models.py | 44 +++++++++++++ hyperwallet/tests/test_api.py | 99 ++++++++++++++++++++++++++++ hyperwallet/tests/test_models.py | 19 ++++++ 6 files changed, 280 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 43d6536..0f667c3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,12 @@ Changelog ========= -1.1.2 (current) +1.1.3 (current) +------------------ + +- Added transfer endpoint + +1.1.2 (2018-03-20) ------------------ - Added bank card endpoint diff --git a/hyperwallet/__init__.py b/hyperwallet/__init__.py index 8dd1f18..b6339d7 100644 --- a/hyperwallet/__init__.py +++ b/hyperwallet/__init__.py @@ -6,7 +6,7 @@ __email__ = 'devsupport@hyperwallet.com' __copyright__ = 'Copyright (c) 2017 Hyperwallet' __license__ = 'MIT' -__version__ = '1.1.2' +__version__ = '1.1.3' __url__ = 'https://github.com/hyperwallet/python-sdk' __download_url__ = 'https://pypi.python.org/pypi/hyperwallet-sdk' __description__ = 'A Python wrapper around the Hyperwallet API' @@ -20,6 +20,7 @@ BankCard, # noqa PrepaidCard, # noqa PaperCheck, # noqa + Transfer, # noqa Payment, # noqa Balance, # noqa Receipt, # noqa diff --git a/hyperwallet/api.py b/hyperwallet/api.py index ae8bd40..5ab66c1 100644 --- a/hyperwallet/api.py +++ b/hyperwallet/api.py @@ -13,6 +13,7 @@ BankCard, PrepaidCard, PaperCheck, + Transfer, Payment, Balance, Receipt, @@ -1450,6 +1451,115 @@ def deactivatePaperCheck(self, ''' + Transfers + https://portal.hyperwallet.com/docs/api/v3/resources/transfers + + ''' + + def createTransfer(self, + data=None): + ''' + Create a Transfer. + + :param data: + A dictionary containing Transfer information. **REQUIRED** + :returns: + A Transfer. + ''' + + if not data: + raise HyperwalletException('data is required') + + if not('sourceToken' in data) or not(data['sourceToken']): + raise HyperwalletException('sourceToken is required') + + if not('destinationToken' in data) or not(data['destinationToken']): + raise HyperwalletException('destinationToken is required') + + if not('clientTransferId' in data) or not(data['clientTransferId']): + raise HyperwalletException('clientTransferId is required') + + response = self.apiClient.doPost( + os.path.join('transfers'), + data + ) + + return Transfer(response) + + def getTransfer(self, + transferToken=None): + ''' + Retrieve a Transfer. + + :param transferToken: + A token identifying the Transfer. **REQUIRED** + :returns: + A Transfer. + ''' + + if not transferToken: + raise HyperwalletException('transferToken is required') + + response = self.apiClient.doGet( + os.path.join( + 'transfers', + transferToken + ) + ) + + return Transfer(response) + + def listTransfers(self, + params=None): + ''' + List Transfers. + + :param params: + A dictionary containing query parameters. + :returns: + An array of Transfers. + ''' + + response = self.apiClient.doGet( + os.path.join('transfers'), + params + ) + + return [Transfer(x) for x in response.get('data', [])] + + def createTransferStatusTransition(self, + transferToken=None, + data=None): + ''' + Create a Transfer Status Transition. + + :param transferToken: + A token identifying the Transfer. **REQUIRED** + :param data: + A dictionary containing Transfer Status Transition information. **REQUIRED** + :returns: + A Transfer Status Transition. + ''' + + if not transferToken: + raise HyperwalletException('transferToken is required') + + if not data: + raise HyperwalletException('data is required') + + response = self.apiClient.doPost( + os.path.join( + 'transfers', + transferToken, + 'status-transitions' + ), + data + ) + + return StatusTransition(response) + + ''' + Payments https://portal.hyperwallet.com/docs/api/v3/resources/payments diff --git a/hyperwallet/models.py b/hyperwallet/models.py index a6d8668..e9e0346 100644 --- a/hyperwallet/models.py +++ b/hyperwallet/models.py @@ -363,6 +363,50 @@ def __repr__(self): ) +class Transfer(HyperwalletModel): + ''' + The Transfer Model. + + :param data: + A dictionary containing the attributes for the Transfer. + ''' + + def __init__(self, data): + ''' + Create a new Transfer with the provided attributes. + ''' + + super(Transfer, self).__init__(data) + + self.defaults = { + 'token': None, + 'status': None, + 'createdOn': None, + 'clientTransferId': None, + 'sourceToken': None, + 'sourceAmount': None, + 'sourceFeeAmount': None, + 'sourceCurrency': None, + 'destinationToken': None, + 'destinationAmount': None, + 'destinationFeeAmount': None, + 'destinationCurrency': None, + 'foreignExchanges': None, + 'notes': None, + 'memo': None, + 'expiresOn': None + } + + for (param, default) in self.defaults.items(): + setattr(self, param, data.get(param, default)) + + def __repr__(self): + return "Transfer({date}, {token})".format( + date=self.createdOn, + token=self.token + ) + + class Payment(HyperwalletModel): ''' The Payment Model. diff --git a/hyperwallet/tests/test_api.py b/hyperwallet/tests/test_api.py index d3765d7..575873b 100644 --- a/hyperwallet/tests/test_api.py +++ b/hyperwallet/tests/test_api.py @@ -1163,6 +1163,105 @@ def test_deactivate_paper_check_success_with_notes(self, mock_post): ''' + Transfers + + ''' + + def test_create_transfer_fail_need_data(self): + + with self.assertRaises(HyperwalletException) as exc: + self.api.createTransfer() + + self.assertEqual(exc.exception.message, 'data is required') + + def test_create_transfer_fail_need_source_token(self): + + with self.assertRaises(HyperwalletException) as exc: + self.api.createTransfer(self.data) + + self.assertEqual(exc.exception.message, 'sourceToken is required') + + def test_create_transfer_fail_need_destination_token(self): + + transfer_data = { + 'sourceToken' : 'test-source-token' + } + with self.assertRaises(HyperwalletException) as exc: + self.api.createTransfer(transfer_data) + + self.assertEqual(exc.exception.message, 'destinationToken is required') + + def test_create_transfer_fail_need_client_transfer_id(self): + + transfer_data = { + 'sourceToken' : 'test-source-token', + 'destinationToken' : 'test-destination-token' + } + with self.assertRaises(HyperwalletException) as exc: + self.api.createTransfer(transfer_data) + + self.assertEqual(exc.exception.message, 'clientTransferId is required') + + @mock.patch('hyperwallet.utils.ApiClient._makeRequest') + def test_create_transfer_success(self, mock_post): + + transfer_data = { + 'sourceToken' : 'test-source-token', + 'destinationToken' : 'test-destination-token', + 'clientTransferId' : 'test-clientTransferId' + } + mock_post.return_value = transfer_data + response = self.api.createTransfer(transfer_data) + + self.assertTrue(response.sourceToken, transfer_data.get('sourceToken')) + + def test_get_transfer_fail_need_transfer_token(self): + + with self.assertRaises(HyperwalletException) as exc: + self.api.getTransfer() + + self.assertEqual(exc.exception.message, 'transferToken is required') + + @mock.patch('hyperwallet.utils.ApiClient._makeRequest') + def test_get_transfer_success(self, mock_get): + + mock_get.return_value = self.data + response = self.api.getTransfer('token') + + self.assertTrue(response.token, self.data.get('token')) + + @mock.patch('hyperwallet.utils.ApiClient._makeRequest') + def test_list_transfers_success(self, mock_get): + + mock_get.return_value = {'data': [self.data]} + response = self.api.listTransfers('token') + + self.assertTrue(response[0].token, self.data.get('token')) + + def test_create_transfer_status_transition_fail_need_transfer_token(self): + + with self.assertRaises(HyperwalletException) as exc: + self.api.createTransferStatusTransition() + + self.assertEqual(exc.exception.message, 'transferToken is required') + + def test_create_transfer_status_transition_fail_need_data(self): + + with self.assertRaises(HyperwalletException) as exc: + self.api.createTransferStatusTransition('token') + + self.assertEqual(exc.exception.message, 'data is required') + + @mock.patch('hyperwallet.utils.ApiClient._makeRequest') + def test_create_transfer_status_transition_success(self, mock_post): + + mock_post.return_value = self.data + response = self.api.createTransferStatusTransition('token', self.data) + + self.assertTrue(response.token, self.data.get('token')) + + ''' + Payments ''' diff --git a/hyperwallet/tests/test_models.py b/hyperwallet/tests/test_models.py index 08ceb9b..74af22c 100644 --- a/hyperwallet/tests/test_models.py +++ b/hyperwallet/tests/test_models.py @@ -11,6 +11,7 @@ BankCard, PrepaidCard, PaperCheck, + Transfer, Payment, Balance, Receipt, @@ -223,6 +224,24 @@ def test_paper_check_model(self): ''' + Transfer + + ''' + + def test_transfer_model(self): + + test_transfer = Transfer(self.transfer_method_data) + + self.assertEqual( + test_transfer.__repr__(), + 'Transfer({date}, {token})'.format( + date=self.transfer_method_data.get('createdOn'), + token=self.transfer_method_data.get('token') + ) + ) + + ''' + Payment ''' From d77beb3da22926a7a6b67367304b411292ea6eed Mon Sep 17 00:00:00 2001 From: aseveryn-epam Date: Fri, 6 Jul 2018 01:04:59 +0300 Subject: [PATCH 2/7] Added PayPalAccounts endpoint --- CHANGELOG.rst | 7 ++- hyperwallet/__init__.py | 3 +- hyperwallet/api.py | 100 ++++++++++++++++++++++++++++- hyperwallet/models.py | 27 ++++++++ hyperwallet/tests/test_api.py | 104 +++++++++++++++++++++++++++++++ hyperwallet/tests/test_models.py | 19 ++++++ 6 files changed, 257 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0f667c3..b00390c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,12 @@ Changelog ========= -1.1.3 (current) +1.1.4 (current) +------------------ + +- Added PayPal account endpoint + +1.1.3 (2018-07-05) ------------------ - Added transfer endpoint diff --git a/hyperwallet/__init__.py b/hyperwallet/__init__.py index b6339d7..ac791e8 100644 --- a/hyperwallet/__init__.py +++ b/hyperwallet/__init__.py @@ -6,7 +6,7 @@ __email__ = 'devsupport@hyperwallet.com' __copyright__ = 'Copyright (c) 2017 Hyperwallet' __license__ = 'MIT' -__version__ = '1.1.3' +__version__ = '1.1.4' __url__ = 'https://github.com/hyperwallet/python-sdk' __download_url__ = 'https://pypi.python.org/pypi/hyperwallet-sdk' __description__ = 'A Python wrapper around the Hyperwallet API' @@ -21,6 +21,7 @@ PrepaidCard, # noqa PaperCheck, # noqa Transfer, # noqa + PayPalAccount, # noqa Payment, # noqa Balance, # noqa Receipt, # noqa diff --git a/hyperwallet/api.py b/hyperwallet/api.py index 5ab66c1..76d5bb8 100644 --- a/hyperwallet/api.py +++ b/hyperwallet/api.py @@ -14,6 +14,7 @@ PrepaidCard, PaperCheck, Transfer, + PayPalAccount, Payment, Balance, Receipt, @@ -1457,7 +1458,7 @@ def deactivatePaperCheck(self, ''' def createTransfer(self, - data=None): + data=None): ''' Create a Transfer. @@ -1560,6 +1561,103 @@ def createTransferStatusTransition(self, ''' + PayPal Accounts + + ''' + + def createPayPalAccount(self, + userToken=None, + data=None): + ''' + Create a PayPal Account. + + :param userToken: + A token identifying the User. **REQUIRED** + :param data: + A dictionary containing PayPal Account information. **REQUIRED** + :returns: + A PayPal Account. + ''' + + if not userToken: + raise HyperwalletException('userToken is required') + + if not data: + raise HyperwalletException('data is required') + + if not('transferMethodCountry' in data) or not(data['transferMethodCountry']): + raise HyperwalletException('transferMethodCountry is required') + + if not('transferMethodCurrency' in data) or not(data['transferMethodCurrency']): + raise HyperwalletException('transferMethodCurrency is required') + + if not('email' in data) or not(data['email']): + raise HyperwalletException('email is required') + + response = self.apiClient.doPost( + os.path.join('users', userToken, 'paypal-accounts'), + data + ) + + return PayPalAccount(response) + + def getPayPalAccount(self, + userToken=None, + payPalAccountToken=None): + ''' + Retrieve a PayPal Account. + + :param userToken: + A token identifying the User. **REQUIRED** + :param payPalAccountToken: + A token identifying the PayPal Account. **REQUIRED** + :returns: + A PayPal Account. + ''' + + if not userToken: + raise HyperwalletException('userToken is required') + + if not payPalAccountToken: + raise HyperwalletException('payPalAccountToken is required') + + response = self.apiClient.doGet( + os.path.join( + 'users', + userToken, + 'paypal-accounts', + payPalAccountToken + ) + ) + + return PayPalAccount(response) + + def listPayPalAccounts(self, + userToken=None, + params=None): + ''' + List PayPal Accounts. + + :param userToken: + A token identifying the User. **REQUIRED** + :param params: + A dictionary containing query parameters. + :returns: + An array of PayPal Accounts. + ''' + + if not userToken: + raise HyperwalletException('userToken is required') + + response = self.apiClient.doGet( + os.path.join('users', userToken, 'paypal-accounts'), + params + ) + + return [PayPalAccount(x) for x in response.get('data', [])] + + ''' + Payments https://portal.hyperwallet.com/docs/api/v3/resources/payments diff --git a/hyperwallet/models.py b/hyperwallet/models.py index e9e0346..c509210 100644 --- a/hyperwallet/models.py +++ b/hyperwallet/models.py @@ -406,6 +406,33 @@ def __repr__(self): token=self.token ) +class PayPalAccount(TransferMethod): + ''' + The PayPalAccount Model. + + :param data: + A dictionary containing the attributes for the PayPal Account. + ''' + + def __init__(self, data): + ''' + Create a new PayPal Account with the provided attributes. + ''' + + super(PayPalAccount, self).__init__(data) + + self.defaults = { + 'email': None + } + + for (param, default) in self.defaults.items(): + setattr(self, param, data.get(param, default)) + + def __repr__(self): + return "PayPalAccount({date}, {token})".format( + date=self.createdOn, + token=self.token + ) class Payment(HyperwalletModel): ''' diff --git a/hyperwallet/tests/test_api.py b/hyperwallet/tests/test_api.py index 575873b..7ec092d 100644 --- a/hyperwallet/tests/test_api.py +++ b/hyperwallet/tests/test_api.py @@ -1262,6 +1262,110 @@ def test_create_transfer_status_transition_success(self, mock_post): ''' + PayPal Accounts + + ''' + + def test_create_paypal_account_fail_need_user_token(self): + + with self.assertRaises(HyperwalletException) as exc: + self.api.createPayPalAccount() + + self.assertEqual(exc.exception.message, 'userToken is required') + + def test_create_paypal_account_fail_need_data(self): + + with self.assertRaises(HyperwalletException) as exc: + self.api.createPayPalAccount('token') + + self.assertEqual(exc.exception.message, 'data is required') + + def test_create_paypal_account_fail_need_transfer_method_country(self): + + paypal_account_data = { + 'token': 'test-token' + } + with self.assertRaises(HyperwalletException) as exc: + self.api.createPayPalAccount('token', paypal_account_data) + + self.assertEqual(exc.exception.message, 'transferMethodCountry is required') + + def test_create_paypal_account_fail_need_transfer_method_currency(self): + + paypal_account_data = { + 'transferMethodCountry': 'test-transfer-method-country' + } + with self.assertRaises(HyperwalletException) as exc: + self.api.createPayPalAccount('token', paypal_account_data) + + self.assertEqual(exc.exception.message, 'transferMethodCurrency is required') + + def test_create_paypal_account_fail_need_email(self): + + paypal_account_data = { + 'transferMethodCountry': 'test-transfer-method-country', + 'transferMethodCurrency': 'test-transfer-method-currency' + } + with self.assertRaises(HyperwalletException) as exc: + self.api.createPayPalAccount('token', paypal_account_data) + + self.assertEqual(exc.exception.message, 'email is required') + + @mock.patch('hyperwallet.utils.ApiClient._makeRequest') + def test_create_paypal_account_success(self, mock_post): + + paypal_account_data = { + 'transferMethodCountry': 'test-transfer-method-country', + 'transferMethodCurrency': 'test-transfer-method-currency', + 'email': 'test-email' + } + mock_post.return_value = paypal_account_data + response = self.api.createPayPalAccount('token', paypal_account_data) + + self.assertTrue(response.email, paypal_account_data.get('token')) + + def test_get_paypal_account_fail_need_user_token(self): + + with self.assertRaises(HyperwalletException) as exc: + self.api.getPayPalAccount() + + self.assertEqual(exc.exception.message, 'userToken is required') + + def test_get_paypal_account_fail_need_paypal_account_token(self): + + with self.assertRaises(HyperwalletException) as exc: + self.api.getPayPalAccount('token') + + self.assertEqual(exc.exception.message, 'payPalAccountToken is required') + + @mock.patch('hyperwallet.utils.ApiClient._makeRequest') + def test_get_paypal_account_success(self, mock_get): + + paypal_account_data = { + 'email': 'test-email' + } + mock_get.return_value = paypal_account_data + response = self.api.getPayPalAccount('token', 'token') + + self.assertTrue(response.email, paypal_account_data.get('email')) + + def test_list_paypal_accounts_fail_need_user_token(self): + + with self.assertRaises(HyperwalletException) as exc: + self.api.listPayPalAccounts() + + self.assertEqual(exc.exception.message, 'userToken is required') + + @mock.patch('hyperwallet.utils.ApiClient._makeRequest') + def test_list_paypal_accounts_success(self, mock_get): + + mock_get.return_value = {'data': [self.data]} + response = self.api.listPayPalAccounts('token') + + self.assertTrue(response[0].token, self.data.get('token')) + + ''' + Payments ''' diff --git a/hyperwallet/tests/test_models.py b/hyperwallet/tests/test_models.py index 74af22c..9103842 100644 --- a/hyperwallet/tests/test_models.py +++ b/hyperwallet/tests/test_models.py @@ -12,6 +12,7 @@ PrepaidCard, PaperCheck, Transfer, + PayPalAccount, Payment, Balance, Receipt, @@ -242,6 +243,24 @@ def test_transfer_model(self): ''' + PayPal Account + + ''' + + def test_paypal_account_model(self): + + test_paypal_account = PayPalAccount(self.transfer_method_data) + + self.assertEqual( + test_paypal_account.__repr__(), + 'PayPalAccount({date}, {token})'.format( + date=self.transfer_method_data.get('createdOn'), + token=self.transfer_method_data.get('token') + ) + ) + + ''' + Payment ''' From 3092f6504b0737c871586d38d4eb5a5d9dfbbe6b Mon Sep 17 00:00:00 2001 From: aseveryn-epam Date: Thu, 18 Oct 2018 19:10:40 +0300 Subject: [PATCH 3/7] Fixed code formatting warnings --- hyperwallet/api.py | 27 ++++++++++----------------- hyperwallet/models.py | 2 ++ hyperwallet/tests/test_api.py | 12 ++++++------ 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/hyperwallet/api.py b/hyperwallet/api.py index 76d5bb8..3a3a141 100644 --- a/hyperwallet/api.py +++ b/hyperwallet/api.py @@ -1461,7 +1461,6 @@ def createTransfer(self, data=None): ''' Create a Transfer. - :param data: A dictionary containing Transfer information. **REQUIRED** :returns: @@ -1488,10 +1487,9 @@ def createTransfer(self, return Transfer(response) def getTransfer(self, - transferToken=None): + transferToken=None): ''' Retrieve a Transfer. - :param transferToken: A token identifying the Transfer. **REQUIRED** :returns: @@ -1511,10 +1509,9 @@ def getTransfer(self, return Transfer(response) def listTransfers(self, - params=None): + params=None): ''' List Transfers. - :param params: A dictionary containing query parameters. :returns: @@ -1529,11 +1526,10 @@ def listTransfers(self, return [Transfer(x) for x in response.get('data', [])] def createTransferStatusTransition(self, - transferToken=None, - data=None): + transferToken=None, + data=None): ''' Create a Transfer Status Transition. - :param transferToken: A token identifying the Transfer. **REQUIRED** :param data: @@ -1566,11 +1562,10 @@ def createTransferStatusTransition(self, ''' def createPayPalAccount(self, - userToken=None, - data=None): + userToken=None, + data=None): ''' Create a PayPal Account. - :param userToken: A token identifying the User. **REQUIRED** :param data: @@ -1602,11 +1597,10 @@ def createPayPalAccount(self, return PayPalAccount(response) def getPayPalAccount(self, - userToken=None, - payPalAccountToken=None): + userToken=None, + payPalAccountToken=None): ''' Retrieve a PayPal Account. - :param userToken: A token identifying the User. **REQUIRED** :param payPalAccountToken: @@ -1633,11 +1627,10 @@ def getPayPalAccount(self, return PayPalAccount(response) def listPayPalAccounts(self, - userToken=None, - params=None): + userToken=None, + params=None): ''' List PayPal Accounts. - :param userToken: A token identifying the User. **REQUIRED** :param params: diff --git a/hyperwallet/models.py b/hyperwallet/models.py index c509210..3a2dea1 100644 --- a/hyperwallet/models.py +++ b/hyperwallet/models.py @@ -406,6 +406,7 @@ def __repr__(self): token=self.token ) + class PayPalAccount(TransferMethod): ''' The PayPalAccount Model. @@ -434,6 +435,7 @@ def __repr__(self): token=self.token ) + class Payment(HyperwalletModel): ''' The Payment Model. diff --git a/hyperwallet/tests/test_api.py b/hyperwallet/tests/test_api.py index 7ec092d..06e1f77 100644 --- a/hyperwallet/tests/test_api.py +++ b/hyperwallet/tests/test_api.py @@ -1184,7 +1184,7 @@ def test_create_transfer_fail_need_source_token(self): def test_create_transfer_fail_need_destination_token(self): transfer_data = { - 'sourceToken' : 'test-source-token' + 'sourceToken': 'test-source-token' } with self.assertRaises(HyperwalletException) as exc: self.api.createTransfer(transfer_data) @@ -1194,8 +1194,8 @@ def test_create_transfer_fail_need_destination_token(self): def test_create_transfer_fail_need_client_transfer_id(self): transfer_data = { - 'sourceToken' : 'test-source-token', - 'destinationToken' : 'test-destination-token' + 'sourceToken': 'test-source-token', + 'destinationToken': 'test-destination-token' } with self.assertRaises(HyperwalletException) as exc: self.api.createTransfer(transfer_data) @@ -1206,9 +1206,9 @@ def test_create_transfer_fail_need_client_transfer_id(self): def test_create_transfer_success(self, mock_post): transfer_data = { - 'sourceToken' : 'test-source-token', - 'destinationToken' : 'test-destination-token', - 'clientTransferId' : 'test-clientTransferId' + 'sourceToken': 'test-source-token', + 'destinationToken': 'test-destination-token', + 'clientTransferId': 'test-clientTransferId' } mock_post.return_value = transfer_data response = self.api.createTransfer(transfer_data) From 46b2260c051883601d049a24c014282f7c10c114 Mon Sep 17 00:00:00 2001 From: aseveryn-epam Date: Wed, 8 Aug 2018 13:50:34 +0300 Subject: [PATCH 4/7] Added layer 7 encryption for requests/responses of Hyperwallet client --- CHANGELOG.rst | 9 +- hyperwallet/api.py | 7 +- hyperwallet/tests/resources/private-jwkset1 | 32 ++++ hyperwallet/tests/resources/private-jwkset2 | 32 ++++ hyperwallet/tests/resources/public-jwkset1 | 20 +++ hyperwallet/tests/resources/public-jwkset2 | 20 +++ hyperwallet/tests/test_encryption.py | 188 ++++++++++++++++++++ hyperwallet/utils/apiclient.py | 18 +- hyperwallet/utils/encryption.py | 181 +++++++++++++++++++ requirements.txt | 3 + setup.py | 2 +- 11 files changed, 504 insertions(+), 8 deletions(-) create mode 100644 hyperwallet/tests/resources/private-jwkset1 create mode 100644 hyperwallet/tests/resources/private-jwkset2 create mode 100644 hyperwallet/tests/resources/public-jwkset1 create mode 100644 hyperwallet/tests/resources/public-jwkset2 create mode 100644 hyperwallet/tests/test_encryption.py create mode 100644 hyperwallet/utils/encryption.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b00390c..e071a8e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,12 @@ Changelog ========= -1.1.4 (current) +1.1.5 (2018-09-05) +------------------ + +- Added Layer 7 encryption for Hyperwallet client + +1.1.4 (2018-08-05) ------------------ - Added PayPal account endpoint @@ -11,7 +16,7 @@ Changelog - Added transfer endpoint -1.1.2 (2018-03-20) +1.1.2 (2018-08-03) ------------------ - Added bank card endpoint diff --git a/hyperwallet/api.py b/hyperwallet/api.py index 3a3a141..51f170e 100644 --- a/hyperwallet/api.py +++ b/hyperwallet/api.py @@ -38,6 +38,8 @@ class Api(object): The token for the program this user is accessing. **REQUIRED** :param server: Your UAT or Production API URL if applicable. + :param encryptionData: + Dictionary with params for encrypted requests (keys: clientPrivateKeySetLocation, hyperwalletKeySetLocation, etc). .. note:: **server** defaults to the Hyperwallet Sandbox URL if not provided. @@ -48,7 +50,8 @@ def __init__(self, username=None, password=None, programToken=None, - server=SERVER): + server=SERVER, + encryptionData=None): ''' Create an instance of the API interface. This is the main interface the user will call to interact with the API. @@ -68,7 +71,7 @@ def __init__(self, self.programToken = programToken self.server = server - self.apiClient = ApiClient(self.username, self.password, self.server) + self.apiClient = ApiClient(self.username, self.password, self.server, encryptionData) ''' diff --git a/hyperwallet/tests/resources/private-jwkset1 b/hyperwallet/tests/resources/private-jwkset1 new file mode 100644 index 0000000..7e05b6f --- /dev/null +++ b/hyperwallet/tests/resources/private-jwkset1 @@ -0,0 +1,32 @@ +{ + "keys": [ + { + "p": "5zx2tRWX6hKlVAsjkfqCyrSFeUVqgzNdtEwS7ZsOOvEKy4Ber3DId_IaGHMEWNu5W2Y5p5BukaCXg1b8UCOgXEHKC4A0SzJT5Y1ObSrAZn3gzo4NjMwzM3PRZdxA5UnqnHpvh3j47OcLYbHQGhcKRb1NktmyBwhoTnxoQUGW5As", + "kty": "RSA", + "q": "xl3k5jfvezJbyxzHAtZYxZd3V0o9jjX7VE6LUZFz4oY4EYL39-WqywoYLaWGFDBHWZ6woBRQHUFQDN2U1GzeXtjo9Tk4VLdU6a8PAMWc1x6EizA3Yj5K9MEcSMhjK--TfXSkL7Mg9x5ppIGI7Pj6Vh5syalTxWsqNYKhq5JohcM", + "d": "iBkyW3X3WFQGKD4IzU7qj9jVJ0i7Qrnb9X1aed7t_2f895uGYgjtAB9twMHyjJ5PbK0VB3_5EAPVUCRS2VjUnoKczNfDmzkNO2xVP0By_M1QgYZpmOKiMNQnoZEZ5ghY6OpKY89DYRAvLnv76yDoyVtlblfrxkqjENr4lIhbIuBFyGCvT6hm0BeXNePsZvmKn_anC4C31mNXeTXYnOcdIkFxpfrmB21lnc3MA2jj0BGFe5SiKTG9qNNlS1KWTaoZnncR2tnlZ1ntHIk56fMBe9DOHaZ1LafY1G4k0mnViJZWcJEe-OU9uDfSBzx6HIQCslIlUJjcFMDdRcgAjInfEQ", + "e": "AQAB", + "use": "enc", + "kid": "2018_enc_rsa_RSA-OAEP-256", + "alg": "RSA-OAEP-256", + "qi": "NjwtLMBXGwfL9TKl8rD2gOvDLFZ4LBYvzoefoKZq9jAJ_qNTTpUahfzdVFCDqEyRABv4VMa7ReKFIBxZxtOWAslS6lFU1odqQvfIF-IwvPCzFEMdXl4rCZgOjwh6NeVD91UI4-sWbWRaszDSSLmomE4x9WuQ8OUeBI746T1UzvM", + "dp": "2oUkNTrDxjt6q7KfGbvgUAlKvXDhGD25hsIBfTNzvjW-GtQkJq1xdRCAoqxG5mY2g25We8idBNf7du4EIQOZ7rVpZ3bvdESKTjs8ayPkkLbSdMB_g5gRpsUDlLwRQ92XbeqybRbgPpiVA-zSmWU-musrXOSHPegvEkS1DT4bh0M", + "dq": "GEJ9bwZiPG_hOArx78_lFW07xCopMw45CYt9kGE4ifiePM4Go4OsCp7WbCa1KhzpbPVyZnF1hs5pCtnCjOQvoevnOa5gzEOLl_S34gFI-CocTaV88H_rzNkdK22Oa14mbI5qUgcXPgGzK9JHu6uLeiLIbTVPMHK1u8uZGBGUxN8", + "n": "sy2TiPsOk2JsDuojkCLYhNIXffvGizTWwLYXCPSVKzVSGEaWcK6D50wJyuKpsweR9o-MvxWp5EvPJRjYD4dyRAsVfk5LLhTR15uJfbxVpDdHmZgtFkIrqx4Y1mO-YCh6MgC3ZZszSLGl73pDTVlwrSSWtWCMXS0ePG0SfYasZcQNYfIUDbJ2Xb4ULcuIGPzlZ57NZ4Ww-Oz8faQeZgrmHh8xjgxaX_Nr2slvbaAAHEKkwOuAohx_1I9JG2ibUbyKpKDHbLx1ym4QASJIAK5t91ROuuYOeSrb-U9Q98essDmTcDIjDezgFEvfymOrYomIClMTMSnHqrTGo0J8ROFrYQ" + }, + { + "p": "2g6lVmoodmar_eao8bU_7YEL97tWQ7tqxGEWhWDR-6fP8Gmn4tgZ2L5Q2YSXiL1ImSFBtUy7LbUfKAH3QovFf2tMHEhDVVjyEtIBhjWz9JPr_QXm_G3gzBktMPRjusEBXUVZ6rn9cfgAcs7VOBN2Gq7Ed3RNs1vHt39w9ZPiQlU", + "kty": "RSA", + "q": "ngDddzAi_TuRLTzqpcE6IDrTRIzKSxdbnq5-PZ9g1kOVAIxOmLeIwKJ_6j9-RmOD1x1yv3LW-4yJXyD2NNVIzHp9loA8DGzqGO5vs7TtI9ptDBPp2tPGlvax9Oknfnyn_drs5TyX4pnHSRoepHRbzLYOHmzQl7vj28_VKgJE5ME", + "d": "CFElMB9H3YGuL0KpqBqc6k3qlrjAOfF2Ao59wIk_GgzrZ0fUvTM1NNs-CwDpsPXNf5w23G4oAP8pwFPtPdoJB1GZNv3xiDZYP_cAZcxudWydF7xNq20XCoGKjzUFAVbkwbp5FykDcuieLevHk46AGdtfgmtUnbvHZUTZq5sYJLOoXCgYAvFqOlng2kMbBw3LH03IaZ5hOXECiV7lYYw1jkfnqB7aPX00fWBW1owZYLdkcfqI_66wB6-yXBIFVVgG8RaUiow8b561hkcLUqSJwbb6xFKJ5P0YC6oMTJrSobRhZUow1JPoiR2KL6vvgmJI6BrprfLv3vs2uvSHzT1OAQ", + "e": "AQAB", + "use": "sig", + "kid": "2018_sig_rsa_RS256_2048", + "alg": "RS256", + "qi": "Lru1wRh_htXyKIH7UOiCnUiHqMrgHBbNux8G-G2nfzNFJftpkIgkSAo4DqDSce13ozdm2STVaLKw-91n3Jc-OovZEtVzBxlk7imbVxi6DLM52h8FUKW5n-Rmt0c7Lc-m-kbHdBBw3yvpCu3svU0UAkLSGpGNkZ2ve7CjFkjapz4", + "dp": "sK7ZTRGrQ3Shu5LgJSlFaT384ngKx8reEczRILV4rz4kAJq7i9Sp7LMYc4c5-XPVlS4bPbm0mK5_Vj1xiZwTJNFd1DTBSjBNxO4gigyNiYkp19SmerbVRMrJkTcUb8ffQSHmX4jgUS4vvtbUcSFjuu8NBfVY2BFv28EJWBLBbBk", + "dq": "iEa8vQkSlJFk5LyusaoYBSZXg79e1yddSV682U92iTce86sQOx3JYESHyTVcJz-7vbTTfJaDH9EVxqu6TtVKhbp8SWtu31StEDXOuBOrmQnSleEzCR8xIJHD6TWTb7_6cLP7MLhzU-lIfh9-IF-Psd-wC8PUoZpXrAX0l9f_LcE", + "n": "hpXGr4AP-okE_WYUJjIga-N8T5pKwgjYrz_cin4k3sDBb1PRSjR9aZa_y1fn2IZpxra00iyG9ndC2Afv7kFqJtm6gadJVVNW_xFXqNKhrobsLJaHQIWnu8Un3pk78hZh42FlHNSM06GBvaCFdRHNtSBSyrQ2rjs8XlMd_YUaqWVIAmRYut_xR7qnKmaKk_wGN9IR5K9Wzt7pN14Ryc_nMGKpaKhgyBwWUVF5O_2IIbtx-Z2CalgdxqJ2by7Jo2LDTvBcAQ5swya0WPDCLV7AFGIrEEpYDF4AoQHpOaX58BqSOWhylmyxjXyYtyahYuRxV6ndXGqjtSGiHIhqSTu2FQ" + } + ] +} \ No newline at end of file diff --git a/hyperwallet/tests/resources/private-jwkset2 b/hyperwallet/tests/resources/private-jwkset2 new file mode 100644 index 0000000..3e83032 --- /dev/null +++ b/hyperwallet/tests/resources/private-jwkset2 @@ -0,0 +1,32 @@ +{ + "keys": [ + { + "p": "8_PYykGGgfX0xwyzIdBQThStS6VUjpyJNub5X3l83ellWv0x_sAYtWb8xSmJfMbjHYn4-yzp98fgtk6fHZzuBqR0ey6JLyhDx0wGK6niaUHFGqQuaXg-2-T3bcBuZEWK-1lEwiMCYaqUY-BETF0Cg9GJJwzYkxZDmGl_62VdQtc", + "kty": "RSA", + "q": "qC_ugGj7UnLHgZiocRBZ5WmsCMGIDIe4TAZYC029wJ7IWu2YswAt_qGtpsI2NM2Bp_MmQt5nHPJ7KfSZl132V_vq1mCuxFOC2Y1LyqlY8LdIAUFHxbQuWCP-nNvbJbNs6Ph7XDgidygpv2vgQriqPbjbtVp5GQfGTYPzSk3JKrE", + "d": "KLkNEtG3WcHuB5f4x8IXMxyIDAUgcULruPy45NBDqrY3yS4CKl_anpMp8_YJDriLAz79vePeVJ7_mKDcTysMVMJMK77HgxB-ixDyxw8Q1qHUu4IyzcmzPHWoDNmtoG7szPcIzwbUkLOWo1pg2whl0zYDsuQsJiIsuKf03HZpHKydHtcGO9nOA9k2_uatxQYZJbr_PM-Gri0gDbENCRhWYwePcrgpAekhju3TOA9N_ndSLre3XhdUKQbu4KoBoX8jUB2L3jKIsLZ5t2iSJ6kHRXDllinxXem3WTtqxDsNZqIhDgkSeej_mmotPFv73y0S2rYCZdV8FL9qFjFTBprVgQ", + "e": "AQAB", + "use": "enc", + "kid": "2018_enc_rsa_RSA-OAEP-256", + "alg": "RSA-OAEP-256", + "qi": "S0vgkBYTYbuQRUzM7NLhDf93TUCkzx8OjRkCrkwbdpJo5gY1AybksqM2O8iu3sEQf2A01qHpZl0YxvPJhIs5BqqzoorPB6D3LmFjAmMZBs7alCuI_lgGoSgTIt6wqm6Y1nQ3mvPRBjB_8t7oSJPaJGwPgBue_TJsH1RkyRVXSW0", + "dp": "KSh97fnKKMkHaEHTQyQzOEkyx614K6trVxD3B82mbIZBLG3FbpaYVJqwkM8mPCAOF2C82hvEyaI2Xmu7WrKsUgCTCmlaidNARDKmY92AroODLrB-iBraeB0URbcOqOo2vZtdB2gCsdmmuYcP3tZeY0EJ48W-EGrUMrWx-FQcvPc", + "dq": "gW0JS7X-GW-MifVxQjjEBSAxrDdKO-JBd_e1z1UO_ejy485NoQo1WusOV_LChhXTfexGeFTv4r3S-_FoNKyxQvnwuPKD6z8cxc_PEHELqYpRle2njsPemiNw70LdPQD7gbieLdRg6XN11QHt_Upgb8kPAltSL2nlN4egNIDxmrE", + "n": "oEWzUJmlScBJ8HkGst54PU41I69D7RyMY8ATNbPBMuJ1sML83qPGK99qjkwVeRtEv0cKd7E6vAIIW4Kgvv2n0IFl3ZvQjg0vOIFZuFOB5tQVW-rR8NGWcqOFS9lL-koej5YSqhZQ60dib5DiIrOy8R2vVWnRDc92qmkd5IA9S6urMooPXlEINmvkqIUH9TOsgubXsFQFD4eGqPQcZ_7miCWhqL23AonJARerCvcR6Bt6_T5UNbpFQJYrxWsLXyOo1p-UZkznS8wzO95wVT-jvgT6D2S2dVgxES6tcQX7ZMKiF7uTbNieFlokoLdJlVnkpoKUZD4VzG2fqhrciEF8pw" + }, + { + "p": "_z4REUkCaGuU3k4CrlYu4MwCFj5YbIeW8MFjIl95PY-DfNTp7V_XPFrHSN5IGnyfuvxesysKmb0edwjiFv5Xpn4Sx07tqghmYYt3eNT6q7zu9PPjTXUThFKceuPgnBLJZVdkhlcm2WZ8NcsyXtRpmUW6QtMfM1b4cqYu9JfRkYM", + "kty": "RSA", + "q": "9er99XCYiUycDXjQZRCsMW84XtslUhnvNsd_x_XgTQtk-8nmgNCMiScF9rQpKZuQXiCnNpnl9n3IjR4Yod_pL9-oAcnCoyG84I0i21jKkg_rPqid_aGy6f-H1WmmsfIgurihMzQBMVNg6N_8X47BJ5jgsU0HEVSYdeZNzQlqHfE", + "d": "xYP2-OlUD6cW5vUbRXDOLaWYRoN8ged2v_p6AicoN05D8Tztj05TVu08-D8eExFbAVwrFKeptGiza9vNvbHF2jBzPToxsNuIN_yAny_Ii11YxzyVEiUKHRF0fAvoTFsB8f3gIr_cin2JTbo4ACNR92rw9jRmYptwTQZGse2EvSBquHWqAIUIPGmGDPmu7yPiMPKO5so7-_a2rQZaMACaFA5Ioey6mV8qehIMm0yx8COwbz36mcU07TSwi1Yj1sSCpzK8jxoPbbVfLnxPCJmkKChQ5qRjEr1ibctj_z6WqW2NGJvKhgVifQP8axt9sdywx1alCZrl8CxxYniVR3VgwQ", + "e": "AQAB", + "use": "sig", + "kid": "2018_sig_rsa_RS256_2048", + "alg": "RS256", + "qi": "0dO4SMNaxUHxZsP5OVitZB0BjVGRJ0Hl5puLPe3uRjYFnhDQCkItpTBE5xnT3lM3BQjMDpewqqIxqXwRbx0K4tW2v3wcrHYwfmMqZUb0rdqS08lZBaAi3uD11MlGhQzsRqo8bfQmpHR11k1JGRK86ioFYrNFraJopsAL7K4X3vc", + "dp": "s_KSHdmXNP8DyWa-RSLFkf7CSeRSetFs_PeaaJVe6KPRU6TX915mZEqrzRfJRcMu6akbKr3hj1nhrJI6s3NFYD_qBVIEBKg_Ze3poOqmf4WIAnIfgnBT_iov4APgSqiEDEp8uKmg3gx-7X4AWRLwD_s0wgAOMyfRqSK4YADY4vk", + "dq": "VZdWkMiBrrflUKMOFT76T7JgMlOf57VzFuPUy6n-SZJ_sUsSWR886reUUcte0EZ-tuQyjsR9z47z8HnbJOwj4y-67_RjNBgX_yfgS-vZhYDY5dQWOSLAfMUdZ9__zstxLMv5_zJIf_x_LE5ZLoEnJTsGaW9f2F5TOiXQSl7OemE", + "n": "9TCyRjIzsgrzqg5EWE-ei9hYHgEyo--iAOrlnVZkaAiWvpJCr2Een3VgEQy0vm228qlAvRY_xl3Hj1fruAm3G-6R3fxx_I2steuAUE1PzSIR7MP_4KCU-Px3vVtUEuhqbnXNl6sKnIR-eorigAzPyOZjpL9sY_oAVPysFWUdhiqrXzvmx8xtbOdfgZQ43pKdGiHpLq1ipUzLHtpHvze-eTz20AxWhDp59JI_oIfFgnUhutKKrbFG9BXRYgtA2-N9mSgNcpo9qnxjRcOo4F-ZfpdZJcSipgwFMJXs8v18KabhoI6ZYkTWg3g1aADsmHW0BQ4rJNpNEHACqRFAhQPTUw" + } + ] +} \ No newline at end of file diff --git a/hyperwallet/tests/resources/public-jwkset1 b/hyperwallet/tests/resources/public-jwkset1 new file mode 100644 index 0000000..6cf5477 --- /dev/null +++ b/hyperwallet/tests/resources/public-jwkset1 @@ -0,0 +1,20 @@ +{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "use": "enc", + "kid": "2018_enc_rsa_RSA-OAEP-256", + "alg": "RSA-OAEP-256", + "n": "sy2TiPsOk2JsDuojkCLYhNIXffvGizTWwLYXCPSVKzVSGEaWcK6D50wJyuKpsweR9o-MvxWp5EvPJRjYD4dyRAsVfk5LLhTR15uJfbxVpDdHmZgtFkIrqx4Y1mO-YCh6MgC3ZZszSLGl73pDTVlwrSSWtWCMXS0ePG0SfYasZcQNYfIUDbJ2Xb4ULcuIGPzlZ57NZ4Ww-Oz8faQeZgrmHh8xjgxaX_Nr2slvbaAAHEKkwOuAohx_1I9JG2ibUbyKpKDHbLx1ym4QASJIAK5t91ROuuYOeSrb-U9Q98essDmTcDIjDezgFEvfymOrYomIClMTMSnHqrTGo0J8ROFrYQ" + }, + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "2018_sig_rsa_RS256_2048", + "alg": "RS256", + "n": "hpXGr4AP-okE_WYUJjIga-N8T5pKwgjYrz_cin4k3sDBb1PRSjR9aZa_y1fn2IZpxra00iyG9ndC2Afv7kFqJtm6gadJVVNW_xFXqNKhrobsLJaHQIWnu8Un3pk78hZh42FlHNSM06GBvaCFdRHNtSBSyrQ2rjs8XlMd_YUaqWVIAmRYut_xR7qnKmaKk_wGN9IR5K9Wzt7pN14Ryc_nMGKpaKhgyBwWUVF5O_2IIbtx-Z2CalgdxqJ2by7Jo2LDTvBcAQ5swya0WPDCLV7AFGIrEEpYDF4AoQHpOaX58BqSOWhylmyxjXyYtyahYuRxV6ndXGqjtSGiHIhqSTu2FQ" + } + ] +} \ No newline at end of file diff --git a/hyperwallet/tests/resources/public-jwkset2 b/hyperwallet/tests/resources/public-jwkset2 new file mode 100644 index 0000000..a4de0b6 --- /dev/null +++ b/hyperwallet/tests/resources/public-jwkset2 @@ -0,0 +1,20 @@ +{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "use": "enc", + "kid": "2018_enc_rsa_RSA-OAEP-256", + "alg": "RSA-OAEP-256", + "n": "oEWzUJmlScBJ8HkGst54PU41I69D7RyMY8ATNbPBMuJ1sML83qPGK99qjkwVeRtEv0cKd7E6vAIIW4Kgvv2n0IFl3ZvQjg0vOIFZuFOB5tQVW-rR8NGWcqOFS9lL-koej5YSqhZQ60dib5DiIrOy8R2vVWnRDc92qmkd5IA9S6urMooPXlEINmvkqIUH9TOsgubXsFQFD4eGqPQcZ_7miCWhqL23AonJARerCvcR6Bt6_T5UNbpFQJYrxWsLXyOo1p-UZkznS8wzO95wVT-jvgT6D2S2dVgxES6tcQX7ZMKiF7uTbNieFlokoLdJlVnkpoKUZD4VzG2fqhrciEF8pw" + }, + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "2018_sig_rsa_RS256_2048", + "alg": "RS256", + "n": "9TCyRjIzsgrzqg5EWE-ei9hYHgEyo--iAOrlnVZkaAiWvpJCr2Een3VgEQy0vm228qlAvRY_xl3Hj1fruAm3G-6R3fxx_I2steuAUE1PzSIR7MP_4KCU-Px3vVtUEuhqbnXNl6sKnIR-eorigAzPyOZjpL9sY_oAVPysFWUdhiqrXzvmx8xtbOdfgZQ43pKdGiHpLq1ipUzLHtpHvze-eTz20AxWhDp59JI_oIfFgnUhutKKrbFG9BXRYgtA2-N9mSgNcpo9qnxjRcOo4F-ZfpdZJcSipgwFMJXs8v18KabhoI6ZYkTWg3g1aADsmHW0BQ4rJNpNEHACqRFAhQPTUw" + } + ] +} \ No newline at end of file diff --git a/hyperwallet/tests/test_encryption.py b/hyperwallet/tests/test_encryption.py new file mode 100644 index 0000000..2b25a71 --- /dev/null +++ b/hyperwallet/tests/test_encryption.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +import unittest +import time +import json +import os.path + +from jwcrypto import jwk, jws as cryptoJWS +from jwcrypto.common import json_encode +from hyperwallet.exceptions import HyperwalletException +from hyperwallet.utils.encryption import Encryption +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError + + +class EncryptionTest(unittest.TestCase): + + def test_should_successfully_encrypt_and_decrypt_text_message(self): + + localDir = os.path.abspath(os.path.dirname(__file__)) + clientPath = os.path.join(localDir, 'resources', 'private-jwkset1') + hyperwalletPath = os.path.join(localDir, 'resources', 'public-jwkset1') + encryption = Encryption(clientPath, hyperwalletPath) + testMessage = 'Message for test' + encryptedMessage = encryption.encrypt(testMessage) + decryptedMessage = encryption.decrypt(encryptedMessage) + self.assertEqual(decryptedMessage, testMessage) + + def test_should_fail_decryption_when_wrong_private_key_is_used(self): + + localDir = os.path.abspath(os.path.dirname(__file__)) + clientPath1 = os.path.join(localDir, 'resources', 'private-jwkset1') + hyperwalletPath1 = os.path.join(localDir, 'resources', 'public-jwkset1') + clientPath2 = os.path.join(localDir, 'resources', 'private-jwkset2') + hyperwalletPath2 = os.path.join(localDir, 'resources', 'public-jwkset2') + encryption1 = Encryption(clientPath1, hyperwalletPath1) + encryption2 = Encryption(clientPath2, hyperwalletPath2) + testMessage = 'Message for test' + encryptedMessage = encryption1.encrypt(testMessage) + + with self.assertRaises(HyperwalletException) as exc: + encryption2.decrypt(encryptedMessage) + + self.assertEqual(exc.exception.message, 'No recipient matched the provided key["Failed: [ValueError(\'Decryption failed.\',)]"]') + + def test_should_fail_signature_verification_when_wrong_public_key_is_used(self): + + localDir = os.path.abspath(os.path.dirname(__file__)) + clientPath1 = os.path.join(localDir, 'resources', 'private-jwkset1') + hyperwalletPath1 = os.path.join(localDir, 'resources', 'public-jwkset1') + clientPath2 = os.path.join(localDir, 'resources', 'private-jwkset2') + hyperwalletPath2 = os.path.join(localDir, 'resources', 'public-jwkset2') + encryption1 = Encryption(clientPath1, hyperwalletPath1) + encryption2 = Encryption(clientPath1, hyperwalletPath2) + testMessage = 'Message for test' + encryptedMessage = encryption1.encrypt(testMessage) + + with self.assertRaises(HyperwalletException) as exc: + encryption2.decrypt(encryptedMessage) + + self.assertEqual(exc.exception.message, 'Signature verification failed.') + + def test_should_throw_exception_when_wrong_jwk_key_set_location_is_given(self): + + localDir = os.path.abspath(os.path.dirname(__file__)) + clientPath = 'wrong_keyset_path' + hyperwalletPath = os.path.join(localDir, 'resources', 'public-jwkset1') + encryption = Encryption(clientPath, hyperwalletPath) + testMessage = 'Message for test' + + with self.assertRaises(HyperwalletException) as exc: + encryptedMessage = encryption.encrypt(testMessage) + + self.assertEqual(exc.exception.message, 'Wrong JWK key set location path = wrong_keyset_path') + + def test_should_throw_exception_when_not_supported_encryption_algorithm_is_given(self): + + localDir = os.path.abspath(os.path.dirname(__file__)) + clientPath = os.path.join(localDir, 'resources', 'private-jwkset1') + hyperwalletPath = os.path.join(localDir, 'resources', 'public-jwkset1') + encryption = Encryption(clientPath, hyperwalletPath, 'unsupported_encryption_algorithm') + testMessage = 'Message for test' + + with self.assertRaises(HyperwalletException) as exc: + encryptedMessage = encryption.encrypt(testMessage) + + self.assertEqual(exc.exception.message, 'JWK set doesn\'t contain key with algorithm = unsupported_encryption_algorithm') + + def test_should_throw_exception_when_jws_signature_does_not_contain_exp_header_param(self): + + localDir = os.path.abspath(os.path.dirname(__file__)) + clientPath = os.path.join(localDir, 'resources', 'private-jwkset1') + hyperwalletPath = '/public-jwkset1' + encryption = Encryption(clientPath, hyperwalletPath) + + jwsKeySet = self.__getJwkKeySet(location=clientPath) + jwkSignKey = self.__findJwkKeyByAlgorithm(jwkKeySet=jwsKeySet, algorithm='RS256') + privateKeyToSign = jwk.JWK(**jwkSignKey) + body = "Test message" + jwsToken = cryptoJWS.JWS(body.encode('utf-8')) + jwsToken.add_signature(privateKeyToSign, None, json_encode({ + "alg": "RS256", + "kid": jwkSignKey['kid'] + })) + signedBody = jwsToken.serialize(True) + + with self.assertRaises(HyperwalletException) as exc: + encryption.checkJwsExpiration(signedBody) + + self.assertEqual(exc.exception.message, 'While trying to verify JWS signature no [exp] header is found') + + def test_should_throw_exception_when_jws_signature_exp_header_param_is_not_integer(self): + + localDir = os.path.abspath(os.path.dirname(__file__)) + clientPath = os.path.join(localDir, 'resources', 'private-jwkset1') + hyperwalletPath = '/public-jwkset1' + encryption = Encryption(clientPath, hyperwalletPath) + + jwsKeySet = self.__getJwkKeySet(location=clientPath) + jwkSignKey = self.__findJwkKeyByAlgorithm(jwkKeySet=jwsKeySet, algorithm='RS256') + privateKeyToSign = jwk.JWK(**jwkSignKey) + body = "Test message" + jwsToken = cryptoJWS.JWS(body.encode('utf-8')) + jwsToken.add_signature(privateKeyToSign, None, json_encode({ + "alg": "RS256", + "exp": "153356exp", + "kid": jwkSignKey['kid'] + })) + signedBody = jwsToken.serialize(True) + + with self.assertRaises(HyperwalletException) as exc: + encryption.checkJwsExpiration(signedBody) + + self.assertEqual(exc.exception.message, 'Wrong value in [exp] header of JWS signature, must be integer') + + def test_should_throw_exception_when_jws_signature_has_expired(self): + + localDir = os.path.abspath(os.path.dirname(__file__)) + clientPath = os.path.join(localDir, 'resources', 'private-jwkset1') + hyperwalletPath = '/public-jwkset1' + encryption = Encryption(clientPath, hyperwalletPath) + + jwsKeySet = self.__getJwkKeySet(location=clientPath) + jwkSignKey = self.__findJwkKeyByAlgorithm(jwkKeySet=jwsKeySet, algorithm='RS256') + privateKeyToSign = jwk.JWK(**jwkSignKey) + body = "Test message" + jwsToken = cryptoJWS.JWS(body.encode('utf-8')) + jwsToken.add_signature(privateKeyToSign, None, json_encode({ + "alg": "RS256", + "exp": int(time.time()) - 6000, + "kid": jwkSignKey['kid'] + })) + signedBody = jwsToken.serialize(True) + + with self.assertRaises(HyperwalletException) as exc: + encryption.checkJwsExpiration(signedBody) + + self.assertEqual(exc.exception.message, 'JWS signature has expired, checked by [exp] JWS header') + + + def __getJwkKeySet(self, location): + + try: + URLValidator()(location) + except ValidationError: + if os.path.isfile(location): + with open(location) as f: + return f.read() + else: + raise HyperwalletException('Wrong JWK key set location path = ' + location) + + return requests.get(location).text + + def __findJwkKeyByAlgorithm(self, jwkKeySet, algorithm): + + try: + keySet = json.loads(jwkKeySet) + except ValueError: + raise HyperwalletException('Wrong JWK key set' + jwkKeySet) + + for key in keySet['keys']: + if key['alg'] == algorithm: + return key + + raise HyperwalletException('JWK set doesn\'t contain key with algorithm = ' + algorithm) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/hyperwallet/utils/apiclient.py b/hyperwallet/utils/apiclient.py index 9b2461c..e03672f 100644 --- a/hyperwallet/utils/apiclient.py +++ b/hyperwallet/utils/apiclient.py @@ -7,6 +7,7 @@ from hyperwallet.exceptions import HyperwalletAPIException from requests_toolbelt.adapters.ssl import SSLAdapter from hyperwallet import __version__ +from hyperwallet.utils.encryption import Encryption try: from urllib.parse import urljoin except ImportError: @@ -23,20 +24,25 @@ class ApiClient(object): The password of this API user. **REQUIRED** :param server: The base URL of the API. **REQUIRED** + :param encryptionData: + Array with params for encrypted requests(Fields: clientPrivateKeySetLocation, hyperwalletKeySetLocation). ''' - def __init__(self, username, password, server): + def __init__(self, username, password, server, encryptionData=None): ''' Create an instance of the API client. This client is used to make the calls to the Hyperwallet API. ''' + # Setup encryption for request/responses. + self.encryption = Encryption(**encryptionData) if encryptionData is not None else None + # Base headers and the custom User-Agent to identify this client as the # Hyperwallet SDK. self.baseHeaders = { 'User-Agent': 'Hyperwallet Python SDK v{}'.format(__version__), 'Accept': 'application/json', - 'Content-Type': 'application/json' + 'Content-Type': 'application/jose+json' if self.encrypted else 'application/json' } self.username = username @@ -54,6 +60,10 @@ def __init__(self, username, password, server): self.session = defaultSession + @property + def encrypted(self): + return self.encryption is not None + def _makeRequest(self, method=None, url=None, @@ -84,7 +94,7 @@ def _makeRequest(self, response = self.session.request( method=method, url=urljoin(self.baseUrl, url), - data=data, + data=(data if data is None else self.encryption.encrypt(data)) if self.encrypted else data, headers=headers, params=params ) @@ -107,6 +117,8 @@ def _makeRequest(self, if hasattr(content, 'decode'): # Python 2 content = content.decode('utf-8') + content = self.encryption.decrypt(content) if self.encrypted else content + try: json_body = json.loads(content) except ValueError as e: diff --git a/hyperwallet/utils/encryption.py b/hyperwallet/utils/encryption.py new file mode 100644 index 0000000..3f89756 --- /dev/null +++ b/hyperwallet/utils/encryption.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python + +import os +import json +import requests +import time +import sys + +from jwcrypto import jwk, jws as cryptoJWS, jwe +from jwcrypto.common import json_encode, json_decode +from jwcrypto.common import base64url_decode, base64url_encode +from jose import jws + +from hyperwallet.exceptions import HyperwalletException +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError + + +class Encryption(object): + ''' + The Hyperwallet API Client. + + :param clientPrivateKeySetLocation: + The location(url or path to file) of client's private JWK key set. **REQUIRED** + :param hyperwalletKeySetLocation: + The location(url or path to file) of hyperwallet public JWK key set. **REQUIRED** + :param encryptionAlgorithm: + JWE encryption algorithm. + :param signAlgorithm: + JWS signature algorithm. + :param encryptionMethod: + JWE body encryption method. + :param jwsExpirationMinutes: + Time in minutes when JWS signature is valid after creation. + ''' + + def __init__(self, + clientPrivateKeySetLocation, hyperwalletKeySetLocation, + encryptionAlgorithm='RSA-OAEP-256', signAlgorithm='RS256', encryptionMethod='A256CBC-HS512', + jwsExpirationMinutes=5): + ''' + Encryption service for hyperwallet client + ''' + + self.clientPrivateKeySetLocation = clientPrivateKeySetLocation + self.hyperwalletKeySetLocation = hyperwalletKeySetLocation + self.encryptionAlgorithm = encryptionAlgorithm + self.signAlgorithm = signAlgorithm + self.encryptionMethod = encryptionMethod + self.jwsExpirationMinutes = jwsExpirationMinutes + self.integer_types = (int, long,) if sys.version_info < (3,) else (int,) + + def encrypt(self, body): + ''' + :param body: + Body message to be 1) signed and 2) encrypted. **REQUIRED** + :returns: + String as a result of signature and encryption of input message body + ''' + + jwsKeySet = self.__getJwkKeySet(location=self.clientPrivateKeySetLocation) + jwkSignKey = self.__findJwkKeyByAlgorithm(jwkKeySet=jwsKeySet, algorithm=self.signAlgorithm) + privateKeyToSign = jwk.JWK(**jwkSignKey) + jwsToken = cryptoJWS.JWS(body.encode('utf-8')) + jwsToken.add_signature(privateKeyToSign, None, json_encode({ + "alg": self.signAlgorithm, + "kid": jwkSignKey['kid'], + "exp": self.__getJwsExpirationTime() + })) + signedBody = jwsToken.serialize(True) + + jweKeySet = self.__getJwkKeySet(location=self.hyperwalletKeySetLocation) + jwkEncryptKey = self.__findJwkKeyByAlgorithm(jwkKeySet=jweKeySet, algorithm=self.encryptionAlgorithm) + publicKeyToEncrypt = jwk.JWK(**jwkEncryptKey) + protected_header = { + "alg": self.encryptionAlgorithm, + "enc": self.encryptionMethod, + "typ": "JWE", + "kid": jwkEncryptKey['kid'], + } + jweToken = jwe.JWE(signedBody.encode('utf-8'), recipient=publicKeyToEncrypt, protected=protected_header) + return jweToken.serialize(True) + + def decrypt(self, body): + ''' + :param body: + Body message to be 1) decrypted and 2) check for correct signature. **REQUIRED** + :returns: + Decrypted body message + ''' + + jweKeySet = self.__getJwkKeySet(location=self.clientPrivateKeySetLocation) + jwkDecryptKey = self.__findJwkKeyByAlgorithm(jwkKeySet=jweKeySet, algorithm=self.encryptionAlgorithm) + privateKeyToDecrypt = jwk.JWK(**jwkDecryptKey) + jweToken = jwe.JWE() + try: + jweToken.deserialize(body, key=privateKeyToDecrypt) + except Exception as e: + raise HyperwalletException(e.message) + payload = jweToken.payload + + self.checkJwsExpiration(payload) + jwsKeySet = self.__getJwkKeySet(location=self.hyperwalletKeySetLocation) + jwkCheckSignKey = self.__findJwkKeyByAlgorithm(jwkKeySet=jwsKeySet, algorithm=self.signAlgorithm) + try: + return jws.verify(payload, json.dumps(jwkCheckSignKey), algorithms=self.signAlgorithm) + except Exception as e: + raise HyperwalletException(e.message) + + def __getJwkKeySet(self, location): + ''' + Retrieves JWK key data from given location. + + :param location: + Location(can be a URL or path to file) of JWK key data. **REQUIRED** + :returns: + JWK key set found at given location. + ''' + + try: + URLValidator()(location) + except ValidationError: + if os.path.isfile(location): + with open(location) as f: + return f.read() + else: + raise HyperwalletException('Wrong JWK key set location path = ' + location) + + return requests.get(location).text + + def __findJwkKeyByAlgorithm(self, jwkKeySet, algorithm): + ''' + Finds JWK key by given algorithm. + + :param jwkKeySet: + JSON representation of JWK key set. **REQUIRED** + :param algorithm: + Algorithm of the JWK key to be found in key set. **REQUIRED** + :returns: + JWK key with given algorithm. + ''' + + try: + keySet = json.loads(jwkKeySet) + except ValueError: + raise HyperwalletException('Wrong JWK key set' + jwkKeySet) + + for key in keySet['keys']: + if key['alg'] == algorithm: + return key + + raise HyperwalletException('JWK set doesn\'t contain key with algorithm = ' + algorithm) + + def __getJwsExpirationTime(self): + ''' + Calculates the expiration time (in seconds) of JWS signature. + + :returns: + JWS expiration time in seconds since the UNIX epoch (January 1, 1970 00:00:00 UTC). + ''' + + secondsInMinute = 60 + return int(time.time() + self.jwsExpirationMinutes * secondsInMinute) + + def checkJwsExpiration(self, payload): + ''' + Check if JWS signature has not expired. + ''' + + header = jws.get_unverified_header(payload) + + if 'exp' not in header: + raise HyperwalletException('While trying to verify JWS signature no [exp] header is found') + + exp = header['exp'] + + if not isinstance(exp, self.integer_types): + raise HyperwalletException('Wrong value in [exp] header of JWS signature, must be integer') + + if exp < time.time(): + raise HyperwalletException('JWS signature has expired, checked by [exp] JWS header') diff --git a/requirements.txt b/requirements.txt index d78f2d4..3523b75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ requests requests-toolbelt +jwcrypto +python-jose +django \ No newline at end of file diff --git a/setup.py b/setup.py index 7d2f940..3ac4bb6 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ def extract_metaitem(meta): maintainer = extract_metaitem('author'), maintainer_email = extract_metaitem('email'), packages = find_packages(exclude = ('tests', 'doc')), - install_requires = ['requests', 'requests-toolbelt'], + install_requires = ['requests', 'requests-toolbelt', 'jwcrypto', 'python-jose', 'django<2'], test_suite = 'nose.collector', tests_require = [ 'mock', 'nose'], keywords='hyperwallet api', From f66cd1796a098318a16593989ee5c4473462edc9 Mon Sep 17 00:00:00 2001 From: aseveryn-epam Date: Wed, 17 Oct 2018 00:41:23 +0300 Subject: [PATCH 5/7] Removed django dependency --- hyperwallet/tests/test_encryption.py | 4 ++-- hyperwallet/utils/apiclient.py | 14 ++++++++++- hyperwallet/utils/encryption.py | 35 ++++++++++++++++------------ requirements.txt | 3 +-- setup.py | 2 +- 5 files changed, 37 insertions(+), 21 deletions(-) diff --git a/hyperwallet/tests/test_encryption.py b/hyperwallet/tests/test_encryption.py index 2b25a71..953f111 100644 --- a/hyperwallet/tests/test_encryption.py +++ b/hyperwallet/tests/test_encryption.py @@ -157,7 +157,6 @@ def test_should_throw_exception_when_jws_signature_has_expired(self): self.assertEqual(exc.exception.message, 'JWS signature has expired, checked by [exp] JWS header') - def __getJwkKeySet(self, location): try: @@ -184,5 +183,6 @@ def __findJwkKeyByAlgorithm(self, jwkKeySet, algorithm): raise HyperwalletException('JWK set doesn\'t contain key with algorithm = ' + algorithm) + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/hyperwallet/utils/apiclient.py b/hyperwallet/utils/apiclient.py index e03672f..bba8b7e 100644 --- a/hyperwallet/utils/apiclient.py +++ b/hyperwallet/utils/apiclient.py @@ -94,7 +94,7 @@ def _makeRequest(self, response = self.session.request( method=method, url=urljoin(self.baseUrl, url), - data=(data if data is None else self.encryption.encrypt(data)) if self.encrypted else data, + data=self.__getRequestData(data), headers=headers, params=params ) @@ -192,3 +192,15 @@ def doPut(self, partialUrl, data): url=partialUrl, data=json.dumps(data).encode('utf-8') ) + + def __getRequestData(data): + ''' + If encryption is enabled try to encrypt request data, otherwise no action required. + + :param data: + Not encrypted request data. **REQUIRED** + :returns: + Request data, encrypted if necessary. + ''' + + return (data if data is None else self.encryption.encrypt(data)) if self.encrypted else data diff --git a/hyperwallet/utils/encryption.py b/hyperwallet/utils/encryption.py index 3f89756..e52316f 100644 --- a/hyperwallet/utils/encryption.py +++ b/hyperwallet/utils/encryption.py @@ -12,8 +12,10 @@ from jose import jws from hyperwallet.exceptions import HyperwalletException -from django.core.validators import URLValidator -from django.core.exceptions import ValidationError +try: + from urlparse import urlparse +except: + from urllib.parse import urlparse class Encryption(object): @@ -35,8 +37,11 @@ class Encryption(object): ''' def __init__(self, - clientPrivateKeySetLocation, hyperwalletKeySetLocation, - encryptionAlgorithm='RSA-OAEP-256', signAlgorithm='RS256', encryptionMethod='A256CBC-HS512', + clientPrivateKeySetLocation, + hyperwalletKeySetLocation, + encryptionAlgorithm='RSA-OAEP-256', + signAlgorithm='RS256', + encryptionMethod='A256CBC-HS512', jwsExpirationMinutes=5): ''' Encryption service for hyperwallet client @@ -116,17 +121,17 @@ def __getJwkKeySet(self, location): :returns: JWK key set found at given location. ''' - - try: - URLValidator()(location) - except ValidationError: - if os.path.isfile(location): - with open(location) as f: - return f.read() - else: - raise HyperwalletException('Wrong JWK key set location path = ' + location) - - return requests.get(location).text + try: + url = urlparse(location) + if url.scheme and url.netloc and url.path: + return requests.get(location).text + raise HyperwalletException('Failed to parse url from string = ' + location) + except: + if os.path.isfile(location): + with open(location) as f: + return f.read() + else: + raise HyperwalletException('Wrong JWK key set location path = ' + location) def __findJwkKeyByAlgorithm(self, jwkKeySet, algorithm): ''' diff --git a/requirements.txt b/requirements.txt index 3523b75..27341de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ requests requests-toolbelt jwcrypto -python-jose -django \ No newline at end of file +python-jose \ No newline at end of file diff --git a/setup.py b/setup.py index 3ac4bb6..548e7df 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ def extract_metaitem(meta): maintainer = extract_metaitem('author'), maintainer_email = extract_metaitem('email'), packages = find_packages(exclude = ('tests', 'doc')), - install_requires = ['requests', 'requests-toolbelt', 'jwcrypto', 'python-jose', 'django<2'], + install_requires = ['requests', 'requests-toolbelt', 'jwcrypto', 'python-jose'], test_suite = 'nose.collector', tests_require = [ 'mock', 'nose'], keywords='hyperwallet api', From e4fa6f84acf8d755e504b94a055e2e1f1e615aa4 Mon Sep 17 00:00:00 2001 From: aseveryn-epam Date: Thu, 18 Oct 2018 23:42:23 +0300 Subject: [PATCH 6/7] Fixed code formatting warnings --- hyperwallet/utils/apiclient.py | 2 +- hyperwallet/utils/encryption.py | 27 ++++++++++++--------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/hyperwallet/utils/apiclient.py b/hyperwallet/utils/apiclient.py index bba8b7e..5962ff5 100644 --- a/hyperwallet/utils/apiclient.py +++ b/hyperwallet/utils/apiclient.py @@ -193,7 +193,7 @@ def doPut(self, partialUrl, data): data=json.dumps(data).encode('utf-8') ) - def __getRequestData(data): + def __getRequestData(self, data): ''' If encryption is enabled try to encrypt request data, otherwise no action required. diff --git a/hyperwallet/utils/encryption.py b/hyperwallet/utils/encryption.py index e52316f..bb8515d 100644 --- a/hyperwallet/utils/encryption.py +++ b/hyperwallet/utils/encryption.py @@ -12,10 +12,7 @@ from jose import jws from hyperwallet.exceptions import HyperwalletException -try: - from urlparse import urlparse -except: - from urllib.parse import urlparse +from six.moves.urllib.parse import urlparse class Encryption(object): @@ -121,17 +118,17 @@ def __getJwkKeySet(self, location): :returns: JWK key set found at given location. ''' - try: - url = urlparse(location) - if url.scheme and url.netloc and url.path: - return requests.get(location).text - raise HyperwalletException('Failed to parse url from string = ' + location) - except: - if os.path.isfile(location): - with open(location) as f: - return f.read() - else: - raise HyperwalletException('Wrong JWK key set location path = ' + location) + try: + url = urlparse(location) + if url.scheme and url.netloc and url.path: + return requests.get(location).text + raise HyperwalletException('Failed to parse url from string = ' + location) + except Exception as e: + if os.path.isfile(location): + with open(location) as f: + return f.read() + else: + raise HyperwalletException('Wrong JWK key set location path = ' + location) def __findJwkKeyByAlgorithm(self, jwkKeySet, algorithm): ''' From 4e01a049f6fe268acc9f9aa7cd1c0ec61b9eac18 Mon Sep 17 00:00:00 2001 From: aseveryn-epam Date: Fri, 30 Nov 2018 13:33:54 +0200 Subject: [PATCH 7/7] Related resources for HyperwalletAPIException --- hyperwallet/tests/test_client.py | 14 ++++++++++++- hyperwallet/tests/test_encryption.py | 31 +++++++++++++++++++++------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/hyperwallet/tests/test_client.py b/hyperwallet/tests/test_client.py index 54dd470..5f2521a 100644 --- a/hyperwallet/tests/test_client.py +++ b/hyperwallet/tests/test_client.py @@ -62,7 +62,9 @@ def test_receive_valid_json_error_response(self, session_mock): data = { "errors": [{ "message": "Houston, we have a problem", - "code": "FORBIDDEN" + "code": "FORBIDDEN", + "relatedResources": ["trm-f3d38df1-adb7-4127-9858-e72ebe682a79", + "trm-601b1401-4464-4f3f-97b3-09079ee7723b"] }] } @@ -79,6 +81,16 @@ def test_receive_valid_json_error_response(self, session_mock): 'FORBIDDEN' ) + self.assertEqual( + exc.exception.message.get('errors')[0].get('relatedResources')[0], + 'trm-f3d38df1-adb7-4127-9858-e72ebe682a79' + ) + + self.assertEqual( + exc.exception.message.get('errors')[0].get('relatedResources')[1], + 'trm-601b1401-4464-4f3f-97b3-09079ee7723b' + ) + @mock.patch('requests.Session.request') def test_receive_valid_json_response(self, session_mock): diff --git a/hyperwallet/tests/test_encryption.py b/hyperwallet/tests/test_encryption.py index 953f111..0265a68 100644 --- a/hyperwallet/tests/test_encryption.py +++ b/hyperwallet/tests/test_encryption.py @@ -9,8 +9,7 @@ from jwcrypto.common import json_encode from hyperwallet.exceptions import HyperwalletException from hyperwallet.utils.encryption import Encryption -from django.core.validators import URLValidator -from django.core.exceptions import ValidationError +from six.moves.urllib.parse import urlparse class EncryptionTest(unittest.TestCase): @@ -158,19 +157,37 @@ def test_should_throw_exception_when_jws_signature_has_expired(self): self.assertEqual(exc.exception.message, 'JWS signature has expired, checked by [exp] JWS header') def __getJwkKeySet(self, location): - + ''' + Retrieves JWK key data from given location. + + :param location: + Location(can be a URL or path to file) of JWK key data. **REQUIRED** + :returns: + JWK key set found at given location. + ''' try: - URLValidator()(location) - except ValidationError: + url = urlparse(location) + if url.scheme and url.netloc and url.path: + return requests.get(location).text + raise HyperwalletException('Failed to parse url from string = ' + location) + except Exception as e: if os.path.isfile(location): with open(location) as f: return f.read() else: raise HyperwalletException('Wrong JWK key set location path = ' + location) - return requests.get(location).text - def __findJwkKeyByAlgorithm(self, jwkKeySet, algorithm): + ''' + Finds JWK key by given algorithm. + + :param jwkKeySet: + JSON representation of JWK key set. **REQUIRED** + :param algorithm: + Algorithm of the JWK key to be found in key set. **REQUIRED** + :returns: + JWK key with given algorithm. + ''' try: keySet = json.loads(jwkKeySet)