From 1acdeea7003c599261ae61e5f0a678c88899d85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Mon, 29 Jan 2024 14:54:59 +0800 Subject: [PATCH] feat: add method for decoding sealed results --- .swagger-codegen-ignore | 1 + README.md | 35 +++++++++ docs/DecryptionKey.md | 12 ++++ docs/SealedResults.md | 14 ++++ fingerprint_pro_server_api_sdk/sealed.py | 86 ++++++++++++++++++++++ generate.sh | 2 +- requirements.txt | 1 + sealed_results_example.py | 23 ++++++ template/README.mustache | 35 +++++++++ test/mocks/sealed_result.json | 78 ++++++++++++++++++++ test/test_sealed.py | 92 ++++++++++++++++++++++++ 11 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 docs/DecryptionKey.md create mode 100644 docs/SealedResults.md create mode 100644 fingerprint_pro_server_api_sdk/sealed.py create mode 100644 sealed_results_example.py create mode 100644 test/mocks/sealed_result.json create mode 100644 test/test_sealed.py diff --git a/.swagger-codegen-ignore b/.swagger-codegen-ignore index 70240175..4929ccd4 100644 --- a/.swagger-codegen-ignore +++ b/.swagger-codegen-ignore @@ -28,3 +28,4 @@ git_push.sh tox.ini test/*.py fingerprint_pro_server_api_sdk/models/many_requests_response.py +requirements.txt \ No newline at end of file diff --git a/README.md b/README.md index f6a340d2..1ea9d9ff 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,36 @@ except ApiException as e: print("Exception when calling DefaultApi->get_event: %s\n" % e) ``` +## Sealed results + +This SDK provides utility methods for decoding [sealed results](https://dev.fingerprint.com/docs/sealed-client-results). +```python +import base64 +import os + +from dotenv import load_dotenv + +from fingerprint_pro_server_api_sdk import EventResponse +from fingerprint_pro_server_api_sdk.sealed import unseal_events_response, DecryptionKey, DecryptionAlgorithm + +load_dotenv() + +sealed_result = base64.b64decode(os.environ["BASE64_SEALED_RESULT"]) +key = base64.b64decode(os.environ["BASE64_KEY"]) + +try: + events_response: EventResponse = unseal_events_response(sealed_result, [DecryptionKey(key, DecryptionAlgorithm['Aes256Gcm'])]) + print("\n\n\nEvent response: \n", events_response.products) +except Exception as e: + print("Exception when calling unsealing events response: %s\n" % e) + exit(1) + +print("Unseal successful!") + +exit(0) +``` +To learn more, refer to example located in [sealed_results_example.py](sealed_results_example.py). + ## Documentation for API Endpoints All URIs are relative to *https://api.fpjs.io* @@ -253,6 +283,11 @@ Class | Method | HTTP request | Description - **Location**: URL query string +## Documentation for sealed results + +- [SealedResults](docs/SealedResults.md) +- [DecryptionKey](docs/DecryptionKey.md) + ## Support To report problems, ask questions or provide feedback, please use [Issues](https://github.com/fingerprintjs/fingerprint-pro-server-api-python-sdk/issues). diff --git a/docs/DecryptionKey.md b/docs/DecryptionKey.md new file mode 100644 index 00000000..7a109ebc --- /dev/null +++ b/docs/DecryptionKey.md @@ -0,0 +1,12 @@ +# DecryptionKey + +## Properties + +| Name | Type | Description | Notes | +|---------------|-------------------------|-----------------------------------------------------------------------------------|-------| +| **Key** | **bytes** | Key generated in dashboard that will be used to decrypt sealed result | | +| **Algorithm** | **DecryptionAlgorithm** | Algorithm to use for decryption. Currently only "aes-256-gcm" value is supported. | | + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/docs/SealedResults.md b/docs/SealedResults.md new file mode 100644 index 00000000..469235e6 --- /dev/null +++ b/docs/SealedResults.md @@ -0,0 +1,14 @@ +# Sealed results + +## **UnsealEventsResponse** +> unseal_events_response(sealed bytes, keys DecryptionKey[]) -> EventResponse + +Decrypts the sealed response with provided keys. +### Required Parameters + +| Name | Type | Description | Notes | +|------------|---------------------|------------------------------------------------------------------------------------------|-------| +| **sealed** | **bytes** | Base64 encoded sealed data | | +| **keys** | **DecryptionKey[]** | Decryption keys. The SDK will try to decrypt the result with each key until it succeeds. | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/fingerprint_pro_server_api_sdk/sealed.py b/fingerprint_pro_server_api_sdk/sealed.py new file mode 100644 index 00000000..a2b50e82 --- /dev/null +++ b/fingerprint_pro_server_api_sdk/sealed.py @@ -0,0 +1,86 @@ +import json +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend +import zlib + +from fingerprint_pro_server_api_sdk.models.event_response import EventResponse + +SEALED_HEADER = bytes([0x9e, 0x85, 0xdc, 0xed]) +DecryptionAlgorithm = { + 'Aes256Gcm': 'aes-256-gcm', +} + + +class DecryptionKey: + def __init__(self, key, algorithm): + self.key = key + self.algorithm = algorithm + + +class UnsealError(Exception): + exception: Exception + key: DecryptionKey + + def __init__(self, exception, key): + self.exception = exception + self.key = key + + +class UnsealAggregateError(Exception): + def __init__(self, errors): + self.errors = errors + super().__init__("Unable to decrypt sealed data") + + +def parse_events_response(unsealed): + json_data = json.loads(unsealed) + + if 'products' not in json_data: + raise ValueError('Sealed data is not valid events response') + + return EventResponse(json_data['products']) + + +def unseal_events_response(sealed_data, decryption_keys): + unsealed = unseal(sealed_data, decryption_keys) + return parse_events_response(unsealed) + + +def unseal(sealed_data, decryption_keys): + if sealed_data[:len(SEALED_HEADER)].hex() != SEALED_HEADER.hex(): + raise ValueError('Invalid sealed data header') + + errors = [] + for decryption_key in decryption_keys: + if decryption_key.algorithm == DecryptionAlgorithm['Aes256Gcm']: + try: + return unseal_aes256gcm(sealed_data, decryption_key.key) + except Exception as e: + errors.append(UnsealError(e, decryption_key)) + continue + else: + raise ValueError(f"Unsupported decryption algorithm: {decryption_key.algorithm}") + + raise UnsealAggregateError(errors) + + +def unseal_aes256gcm(sealed_data, decryption_key): + nonce_length = 12 + nonce = sealed_data[len(SEALED_HEADER):len(SEALED_HEADER) + nonce_length] + + auth_tag_length = 16 + auth_tag = sealed_data[-auth_tag_length:] + + ciphertext = sealed_data[len(SEALED_HEADER) + nonce_length:-auth_tag_length] + + decipher = Cipher( + algorithms.AES(decryption_key), + modes.GCM(nonce, auth_tag), + backend=default_backend() + ).decryptor() + + compressed = decipher.update(ciphertext) + decipher.finalize() + + payload = zlib.decompress(compressed, -zlib.MAX_WBITS) + + return payload.decode('utf-8') diff --git a/generate.sh b/generate.sh index edd65745..2b78cc51 100755 --- a/generate.sh +++ b/generate.sh @@ -19,7 +19,7 @@ fi # jar was downloaded from here https://repo1.maven.org/maven2/io/swagger/codegen/v3/swagger-codegen-cli/3.0.34/ -rm docs/* +find ./docs -type f ! -name "DecryptionKey.md" ! -name "SealedResults.md" -exec rm {} + cd fingerprint_pro_server_api_sdk/models shopt -s extglob rm !("many_requests_response.py") diff --git a/requirements.txt b/requirements.txt index 4c5e8293..46478e10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ python_dateutil >= 2.5.3 setuptools >= 21.0.0 urllib3<1.27,>=1.21.1 python-dotenv +cryptography \ No newline at end of file diff --git a/sealed_results_example.py b/sealed_results_example.py new file mode 100644 index 00000000..dd78ed9f --- /dev/null +++ b/sealed_results_example.py @@ -0,0 +1,23 @@ +import base64 +import os + +from dotenv import load_dotenv + +from fingerprint_pro_server_api_sdk import EventResponse +from fingerprint_pro_server_api_sdk.sealed import unseal_events_response, DecryptionKey, DecryptionAlgorithm + +load_dotenv() + +sealed_result = base64.b64decode(os.environ["BASE64_SEALED_RESULT"]) +key = base64.b64decode(os.environ["BASE64_KEY"]) + +try: + events_response: EventResponse = unseal_events_response(sealed_result, [DecryptionKey(key, DecryptionAlgorithm['Aes256Gcm'])]) + print("\n\n\nEvent response: \n", events_response.products) +except Exception as e: + print("Exception when calling unsealing events response: %s\n" % e) + exit(1) + +print("Unseal successful!") + +exit(0) diff --git a/template/README.mustache b/template/README.mustache index 8e23bece..7a9ceda1 100644 --- a/template/README.mustache +++ b/template/README.mustache @@ -153,6 +153,36 @@ except ApiException as e: print("Exception when calling DefaultApi->get_event: %s\n" % e) ``` +## Sealed results + +This SDK provides utility methods for decoding [sealed results](https://dev.fingerprint.com/docs/sealed-client-results). +```python +import base64 +import os + +from dotenv import load_dotenv + +from fingerprint_pro_server_api_sdk import EventResponse +from fingerprint_pro_server_api_sdk.sealed import unseal_events_response, DecryptionKey, DecryptionAlgorithm + +load_dotenv() + +sealed_result = base64.b64decode(os.environ["BASE64_SEALED_RESULT"]) +key = base64.b64decode(os.environ["BASE64_KEY"]) + +try: + events_response: EventResponse = unseal_events_response(sealed_result, [DecryptionKey(key, DecryptionAlgorithm['Aes256Gcm'])]) + print("\n\n\nEvent response: \n", events_response.products) +except Exception as e: + print("Exception when calling unsealing events response: %s\n" % e) + exit(1) + +print("Unseal successful!") + +exit(0) +``` +To learn more, refer to example located in [sealed_results_example.py](sealed_results_example.py). + ## Documentation for API Endpoints All URIs are relative to *{{basePath}}* @@ -189,6 +219,11 @@ Class | Method | HTTP request | Description {{/authMethods}} +## Documentation for sealed results + +- [SealedResults](docs/SealedResults.md) +- [DecryptionKey](docs/DecryptionKey.md) + ## Support To report problems, ask questions or provide feedback, please use [Issues](https://github.com/fingerprintjs/fingerprint-pro-server-api-python-sdk/issues). diff --git a/test/mocks/sealed_result.json b/test/mocks/sealed_result.json new file mode 100644 index 00000000..718764bc --- /dev/null +++ b/test/mocks/sealed_result.json @@ -0,0 +1,78 @@ +{ + "products": { + "identification": { + "data": { + "visitorId": "2ZEDCZEfOfXjEmMuE3tq", + "requestId": "1703067132750.Z5hutJ", + "browserDetails": { + "browserName": "Safari", + "browserMajorVersion": "17", + "browserFullVersion": "17.3", + "os": "Mac OS X", + "osVersion": "10.15.7", + "device": "Other", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15" + }, + "incognito": false, + "ip": "::1", + "ipLocation": { + "accuracyRadius": 1000, + "latitude": 59.3241, + "longitude": 18.0517, + "postalCode": "100 05", + "timezone": "Europe/Stockholm", + "city": { + "name": "Stockholm" + }, + "country": { + "code": "SE", + "name": "Sweden" + }, + "continent": { + "code": "EU", + "name": "Europe" + }, + "subdivisions": [ + { + "isoCode": "AB", + "name": "Stockholm County" + } + ] + }, + "timestamp": 1703067136286, + "time": "2023-12-20T10:12:16Z", + "url": "http://localhost:8080/", + "tag": { + "foo": "bar" + }, + "confidence": { + "score": 1 + }, + "visitorFound": true, + "firstSeenAt": { + "global": "2023-12-15T12:13:55.103Z", + "subscription": "2023-12-15T12:13:55.103Z" + }, + "lastSeenAt": { + "global": "2023-12-19T11:39:51.52Z", + "subscription": "2023-12-19T11:39:51.52Z" + } + } + }, + "botd": { + "data": { + "bot": { + "result": "notDetected" + }, + "meta": { + "foo": "bar" + }, + "url": "http://localhost:8080/", + "ip": "::1", + "time": "2023-12-20T10:12:13.894Z", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15", + "requestId": "1703067132750.Z5hutJ" + } + } + } +} \ No newline at end of file diff --git a/test/test_sealed.py b/test/test_sealed.py new file mode 100644 index 00000000..ee88e098 --- /dev/null +++ b/test/test_sealed.py @@ -0,0 +1,92 @@ +import base64 +import io +import json +import unittest + +from fingerprint_pro_server_api_sdk import EventResponse +from fingerprint_pro_server_api_sdk.sealed import DecryptionAlgorithm, DecryptionKey, unseal_events_response, \ + UnsealError, UnsealAggregateError + + +class TestSealed(unittest.TestCase): + valid_key = base64.b64decode('p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq53=') + invalid_key = base64.b64decode('a2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq53=') + + def test_unseal_aes256gcm(self): + with io.open("./test/mocks/sealed_result.json", 'r', encoding='utf-8') as f: + expected_result = EventResponse(json.load(f)['products']) + + sealed_data = base64.b64decode( + 'noXc7SXO+mqeAGrvBMgObi/S0fXTpP3zupk8qFqsO/1zdtWCD169iLA3VkkZh9ICHpZ0oWRzqG0M9/TnCeKFohgBLqDp6O0zEfXOv6i5q++aucItznQdLwrKLP+O0blfb4dWVI8/aSbd4ELAZuJJxj9bCoVZ1vk+ShbUXCRZTD30OIEAr3eiG9aw00y1UZIqMgX6CkFlU9L9OnKLsNsyomPIaRHTmgVTI5kNhrnVNyNsnzt9rY7fUD52DQxJILVPrUJ1Q+qW7VyNslzGYBPG0DyYlKbRAomKJDQIkdj/Uwa6bhSTq4XYNVvbk5AJ/dGwvsVdOnkMT2Ipd67KwbKfw5bqQj/cw6bj8Cp2FD4Dy4Ud4daBpPRsCyxBM2jOjVz1B/lAyrOp8BweXOXYugwdPyEn38MBZ5oL4D38jIwR/QiVnMHpERh93jtgwh9Abza6i4/zZaDAbPhtZLXSM5ztdctv8bAb63CppLU541Kf4OaLO3QLvfLRXK2n8bwEwzVAqQ22dyzt6/vPiRbZ5akh8JB6QFXG0QJF9DejsIspKF3JvOKjG2edmC9o+GfL3hwDBiihYXCGY9lElZICAdt+7rZm5UxMx7STrVKy81xcvfaIp1BwGh/HyMsJnkE8IczzRFpLlHGYuNDxdLoBjiifrmHvOCUDcV8UvhSV+UAZtAVejdNGo5G/bz0NF21HUO4pVRPu6RqZIs/aX4hlm6iO/0Ru00ct8pfadUIgRcephTuFC2fHyZxNBC6NApRtLSNLfzYTTo/uSjgcu6rLWiNo5G7yfrM45RXjalFEFzk75Z/fu9lCJJa5uLFgDNKlU+IaFjArfXJCll3apbZp4/LNKiU35ZlB7ZmjDTrji1wLep8iRVVEGht/DW00MTok7Zn7Fv+MlxgWmbZB3BuezwTmXb/fNw==') + + result = unseal_events_response(sealed_data, [ + DecryptionKey(self.invalid_key, DecryptionAlgorithm['Aes256Gcm']), + DecryptionKey(self.valid_key, DecryptionAlgorithm['Aes256Gcm']), + ]) + + self.assertEqual(result, expected_result) + + def test_unseal_invalid_header(self): + sealed_data = base64.b64decode( + 'xzXc7SXO+mqeAGrvBMgObi/S0fXTpP3zupk8qFqsO/1zdtWCD169iLA3VkkZh9ICHpZ0oWRzqG0M9/TnCeKFohgBLqDp6O0zEfXOv6i5q++aucItznQdLwrKLP+O0blfb4dWVI8/aSbd4ELAZuJJxj9bCoVZ1vk+ShbUXCRZTD30OIEAr3eiG9aw00y1UZIqMgX6CkFlU9L9OnKLsNsyomPIaRHTmgVTI5kNhrnVNyNsnzt9rY7fUD52DQxJILVPrUJ1Q+qW7VyNslzGYBPG0DyYlKbRAomKJDQIkdj/Uwa6bhSTq4XYNVvbk5AJ/dGwvsVdOnkMT2Ipd67KwbKfw5bqQj/cw6bj8Cp2FD4Dy4Ud4daBpPRsCyxBM2jOjVz1B/lAyrOp8BweXOXYugwdPyEn38MBZ5oL4D38jIwR/QiVnMHpERh93jtgwh9Abza6i4/zZaDAbPhtZLXSM5ztdctv8bAb63CppLU541Kf4OaLO3QLvfLRXK2n8bwEwzVAqQ22dyzt6/vPiRbZ5akh8JB6QFXG0QJF9DejsIspKF3JvOKjG2edmC9o+GfL3hwDBiihYXCGY9lElZICAdt+7rZm5UxMx7STrVKy81xcvfaIp1BwGh/HyMsJnkE8IczzRFpLlHGYuNDxdLoBjiifrmHvOCUDcV8UvhSV+UAZtAVejdNGo5G/bz0NF21HUO4pVRPu6RqZIs/aX4hlm6iO/0Ru00ct8pfadUIgRcephTuFC2fHyZxNBC6NApRtLSNLfzYTTo/uSjgcu6rLWiNo5G7yfrM45RXjalFEFzk75Z/fu9lCJJa5uLFgDNKlU+IaFjArfXJCll3apbZp4/LNKiU35ZlB7ZmjDTrji1wLep8iRVVEGht/DW00MTok7Zn7Fv+MlxgWmbZB3BuezwTmXb/fNw==') + + with self.assertRaisesRegex(Exception, "Invalid sealed data header"): + unseal_events_response(sealed_data, [ + DecryptionKey(self.invalid_key, DecryptionAlgorithm['Aes256Gcm']), + DecryptionKey(self.valid_key, DecryptionAlgorithm['Aes256Gcm']), + ]) + + def test_unseal_invalid_algorithm(self): + sealed_data = base64.b64decode( + 'noXc7SXO+mqeAGrvBMgObi/S0fXTpP3zupk8qFqsO/1zdtWCD169iLA3VkkZh9ICHpZ0oWRzqG0M9/TnCeKFohgBLqDp6O0zEfXOv6i5q++aucItznQdLwrKLP+O0blfb4dWVI8/aSbd4ELAZuJJxj9bCoVZ1vk+ShbUXCRZTD30OIEAr3eiG9aw00y1UZIqMgX6CkFlU9L9OnKLsNsyomPIaRHTmgVTI5kNhrnVNyNsnzt9rY7fUD52DQxJILVPrUJ1Q+qW7VyNslzGYBPG0DyYlKbRAomKJDQIkdj/Uwa6bhSTq4XYNVvbk5AJ/dGwvsVdOnkMT2Ipd67KwbKfw5bqQj/cw6bj8Cp2FD4Dy4Ud4daBpPRsCyxBM2jOjVz1B/lAyrOp8BweXOXYugwdPyEn38MBZ5oL4D38jIwR/QiVnMHpERh93jtgwh9Abza6i4/zZaDAbPhtZLXSM5ztdctv8bAb63CppLU541Kf4OaLO3QLvfLRXK2n8bwEwzVAqQ22dyzt6/vPiRbZ5akh8JB6QFXG0QJF9DejsIspKF3JvOKjG2edmC9o+GfL3hwDBiihYXCGY9lElZICAdt+7rZm5UxMx7STrVKy81xcvfaIp1BwGh/HyMsJnkE8IczzRFpLlHGYuNDxdLoBjiifrmHvOCUDcV8UvhSV+UAZtAVejdNGo5G/bz0NF21HUO4pVRPu6RqZIs/aX4hlm6iO/0Ru00ct8pfadUIgRcephTuFC2fHyZxNBC6NApRtLSNLfzYTTo/uSjgcu6rLWiNo5G7yfrM45RXjalFEFzk75Z/fu9lCJJa5uLFgDNKlU+IaFjArfXJCll3apbZp4/LNKiU35ZlB7ZmjDTrji1wLep8iRVVEGht/DW00MTok7Zn7Fv+MlxgWmbZB3BuezwTmXb/fNw==') + + with self.assertRaisesRegex(Exception, "Unsupported decryption algorithm: invalid"): + unseal_events_response(sealed_data, [ + DecryptionKey(self.invalid_key, 'invalid'), + DecryptionKey(self.valid_key, DecryptionAlgorithm['Aes256Gcm']), + ]) + + def test_unseal_invalid_data(self): + sealed_data = base64.b64decode( + # "{\"invalid\":true}" + 'noXc7VOpBstjjcavDKSKr4HTavt4mdq8h6NC32T0hUtw9S0jXT8lPjZiWL8SyHxmrF3uTGqO+g==') + + with self.assertRaisesRegex(Exception, "Sealed data is not valid events response"): + unseal_events_response(sealed_data, [ + DecryptionKey(self.invalid_key, DecryptionAlgorithm['Aes256Gcm']), + DecryptionKey(self.valid_key, DecryptionAlgorithm['Aes256Gcm']), + ]) + + def test_unseal_not_compressed_data(self): + sealed_data = base64.b64decode( + 'noXc7dtuk0smGE+ZbaoXzrp6Rq8ySxLepejTsu7+jUXlPhV1w+WuHx9gbPhaENJnOQo8BcGmsaRhL5k2NVj+DRNzYO9cQD7wHxmXKCyTbl/dvSYOMoHziUZ2VbQ7tmaorFny26v8jROr/UBGfvPE0dLKC36IN9ZlJ3X0NZJO8SY+8bCr4mTrkVZsv/hpvZp+OjC4h7e5vxcpmnBWXzxfaO79Lq3aMRIEf9XfK7/bVIptHaEqtPKCTwl9rz1KUpUUNQSHTPM0NlqJe9bjYf5mr1uYvWHhcJoXSyRyVMxIv/quRiw3SKJzAMOTBiAvFICpWuRFa+T/xIMHK0g96w/IMQo0jdY1E067ZEvBUOBmsJnGJg1LllS3rbJVe+E2ClFNL8SzFphyvtlcfvYB+SVSD4bzI0w/YCldv5Sq42BFt5bn4n4aE5A6658DYsfSRYWqP6OpqPJx96cY34W7H1t/ZG0ulez6zF5NvWhc1HDQ1gMtXd+K/ogt1n+FyFtn8xzvtSGkmrc2jJgYNI5Pd0Z0ent73z0MKbJx9v2ta/emPEzPr3cndN5amdr6TmRkDU4bq0vyhAh87DJrAnJQLdrvYLddnrr8xTdeXxj1i1Yug6SGncPh9sbTYkdOfuamPAYOuiJVBAMcfYsYEiQndZe8mOQ4bpCr+hxAAqixhZ16pQ8CeUwa247+D2scRymLB8qJXlaERuFZtWGVAZ8VP/GS/9EXjrzpjGX9vlrIPeJP8fh2S5QPzw55cGNJ7JfAdOyManXnoEw2/QzDhSZQARVl+akFgSO0Y13YmbiL7H6HcKWGcJ2ipDKIaj2fJ7GE0Vzyt+CBEezSQR99Igd8x3p2JtvsVKp35iLPksjS1VqtSCTbuIRUlINlfQHNjeQiE/B/61jo3Mf7SmjYjqtvXt5e9RKb+CQku2qH4ZU8xN3DSg+4mLom3BgKBkm/MoyGBpMK41c96d2tRp3tp4hV0F6ac02Crg7P2lw8IUct+i2VJ8VUjcbRfTIPQs0HjNjM6/gLfLCkWOHYrlFjwusXWQCJz91Kq+hVxj7M9LtplPO4AUq6RUMNhlPGUmyOI2tcUMrjq9vMLXGlfdkH185zM4Mk+O7DRLC8683lXZFZvcBEmxr855PqLLH/9SpYKHBoGRatDRdQe3oRp6gHS0jpQ1SW/si4kvLKiUNjiBExvbQVOUV7/VFXvG1RpM9wbzSoOd40gg7ZzD/72QshUC/25DkM/Pm7RBzwtjgmnRKjT+mROeC/7VQLoz3amv09O8Mvbt+h/lX5+51Q834F7NgIGagbB20WtWcMtrmKrvCEZlaoiZrmYVSbi1RfknRK7CTPJkopw9IjO7Ut2EhKZ+jL4rwk6TlVm6EC6Kuj7KNqp6wB/UNe9eM2Eym/aiHAcja8XN4YQhSIuJD2Wxb0n3LkKnAjK1/GY65c8K6rZsVYQ0MQL1j4lMl0UZPjG/vzKyetIsVDyXc4J9ZhOEMYnt/LaxEeSt4EMJGBA9wpTmz33X4h3ij0Y3DY/rH7lrEScUknw20swTZRm5T6q1bnimj7M1OiOkebdI09MZ0nyaTWRHdB7B52C/moh89Q7qa2Fulp5h8Us1FYRkWBLt37a5rGI1IfVeP38KaPbagND+XzWpNqX4HVrAVPLQVK5EwUvGamED3ooJ0FMieTc0IH0N+IeUYG7Q8XmrRVBcw32W8pEfYLO9L71An/J0jQZCIP8DuQnUG0mOvunOuloBGvP/9LvkBlkamh68F0a5f5ny1jloyIFJhRh5dt2SBlbsXS9AKqUwARYSSsA9Ao4WJWOZMyjp8A+qIBAfW65MdhhUDKYMBgIAbMCc3uiptzElQQopE5TT5xIhwfYxa503jVzQbz1Q==') + + with self.assertRaisesRegex(Exception, "Unable to decrypt sealed data") as context: + unseal_events_response(sealed_data, [ + DecryptionKey(self.invalid_key, DecryptionAlgorithm['Aes256Gcm']), + DecryptionKey(self.valid_key, DecryptionAlgorithm['Aes256Gcm']), + ]) + + exception: UnsealAggregateError = context.exception + error: UnsealError = exception.errors[1] + + self.assertEqual(str(error.exception), 'Error -3 while decompressing data: invalid distance too far back') + + def test_unseal_all_keys_invalid(self): + sealed_data = base64.b64decode( + 'noXc7SXO+mqeAGrvBMgObi/S0fXTpP3zupk8qFqsO/1zdtWCD169iLA3VkkZh9ICHpZ0oWRzqG0M9/TnCeKFohgBLqDp6O0zEfXOv6i5q++aucItznQdLwrKLP+O0blfb4dWVI8/aSbd4ELAZuJJxj9bCoVZ1vk+ShbUXCRZTD30OIEAr3eiG9aw00y1UZIqMgX6CkFlU9L9OnKLsNsyomPIaRHTmgVTI5kNhrnVNyNsnzt9rY7fUD52DQxJILVPrUJ1Q+qW7VyNslzGYBPG0DyYlKbRAomKJDQIkdj/Uwa6bhSTq4XYNVvbk5AJ/dGwvsVdOnkMT2Ipd67KwbKfw5bqQj/cw6bj8Cp2FD4Dy4Ud4daBpPRsCyxBM2jOjVz1B/lAyrOp8BweXOXYugwdPyEn38MBZ5oL4D38jIwR/QiVnMHpERh93jtgwh9Abza6i4/zZaDAbPhtZLXSM5ztdctv8bAb63CppLU541Kf4OaLO3QLvfLRXK2n8bwEwzVAqQ22dyzt6/vPiRbZ5akh8JB6QFXG0QJF9DejsIspKF3JvOKjG2edmC9o+GfL3hwDBiihYXCGY9lElZICAdt+7rZm5UxMx7STrVKy81xcvfaIp1BwGh/HyMsJnkE8IczzRFpLlHGYuNDxdLoBjiifrmHvOCUDcV8UvhSV+UAZtAVejdNGo5G/bz0NF21HUO4pVRPu6RqZIs/aX4hlm6iO/0Ru00ct8pfadUIgRcephTuFC2fHyZxNBC6NApRtLSNLfzYTTo/uSjgcu6rLWiNo5G7yfrM45RXjalFEFzk75Z/fu9lCJJa5uLFgDNKlU+IaFjArfXJCll3apbZp4/LNKiU35ZlB7ZmjDTrji1wLep8iRVVEGht/DW00MTok7Zn7Fv+MlxgWmbZB3BuezwTmXb/fNw==') + + with self.assertRaisesRegex(Exception, 'Unable to decrypt sealed data'): + unseal_events_response(sealed_data, [ + DecryptionKey(self.invalid_key, DecryptionAlgorithm['Aes256Gcm']), + DecryptionKey(self.invalid_key, DecryptionAlgorithm['Aes256Gcm']), + ]) + + def test_unseal_empty_data(self): + sealed_data = bytearray(b'') + + with self.assertRaisesRegex(Exception, 'Invalid sealed data header'): + unseal_events_response(sealed_data, [ + DecryptionKey(self.invalid_key, DecryptionAlgorithm['Aes256Gcm']), + DecryptionKey(self.valid_key, DecryptionAlgorithm['Aes256Gcm']), + ])