From 543e9766a119238af44f9ea6b4d3f619b1fcd4cf Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Thu, 23 Oct 2025 11:47:41 +0900 Subject: [PATCH] Add symmetric encryption and tests --- pyproject.toml | 1 + src/corelibs/encryption_handling/__init__.py | 0 .../symmetric_encryption.py | 152 ++++ test-run/encryption/symmetric_encryption.py | 34 + tests/unit/encryption_handling/__init__.py | 3 + .../test_symmetric_encryption.py | 665 ++++++++++++++++++ uv.lock | 114 ++- 7 files changed, 968 insertions(+), 1 deletion(-) create mode 100644 src/corelibs/encryption_handling/__init__.py create mode 100644 src/corelibs/encryption_handling/symmetric_encryption.py create mode 100644 test-run/encryption/symmetric_encryption.py create mode 100644 tests/unit/encryption_handling/__init__.py create mode 100644 tests/unit/encryption_handling/test_symmetric_encryption.py diff --git a/pyproject.toml b/pyproject.toml index 3d9b62a..4b38a4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ description = "Collection of utils for Python scripts" readme = "README.md" requires-python = ">=3.13" dependencies = [ + "cryptography>=46.0.3", "jmespath>=1.0.1", "psutil>=7.0.0", "requests>=2.32.4", diff --git a/src/corelibs/encryption_handling/__init__.py b/src/corelibs/encryption_handling/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/corelibs/encryption_handling/symmetric_encryption.py b/src/corelibs/encryption_handling/symmetric_encryption.py new file mode 100644 index 0000000..026ab69 --- /dev/null +++ b/src/corelibs/encryption_handling/symmetric_encryption.py @@ -0,0 +1,152 @@ +""" +simple symmetric encryption +Will be moved to CoreLibs +TODO: set key per encryption run +""" + +import os +import json +import base64 +import hashlib +from typing import TypedDict, cast +from cryptography.fernet import Fernet +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + + +class PackageData(TypedDict): + """encryption package""" + encrypted_data: str + salt: str + key_hash: str + + +class SymmetricEncryption: + """ + simple encryption + + the encrypted package has "encrypted_data" and "salt" as fields, salt is needed to create the + key from the password to decrypt + """ + + def __init__(self, password: str): + if not password: + raise ValueError("A password must be set") + self.password = password + self.password_hash = hashlib.sha256(password.encode('utf-8')).hexdigest() + + def __derive_key_from_password(self, password: str, salt: bytes) -> bytes: + _password = password.encode('utf-8') + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + ) + key = base64.urlsafe_b64encode(kdf.derive(_password)) + return key + + def __encrypt_with_metadata(self, data: str | bytes) -> PackageData: + """Encrypt data and include salt if password-based""" + # convert to bytes (for encoding) + if isinstance(data, str): + data = data.encode('utf-8') + + # generate salt and key from password + salt = os.urandom(16) + key = self.__derive_key_from_password(self.password, salt) + # init the cypher suit + cipher_suite = Fernet(key) + + encrypted_data = cipher_suite.encrypt(data) + + # If using password, include salt in the result + return { + 'encrypted_data': base64.urlsafe_b64encode(encrypted_data).decode('utf-8'), + 'salt': base64.urlsafe_b64encode(salt).decode('utf-8'), + 'key_hash': hashlib.sha256(key).hexdigest() + } + + def encrypt_with_metadata(self, data: str | bytes, return_as: str = 'str') -> str | bytes | PackageData: + """encrypt with metadata, but returns data in string""" + match return_as: + case 'str': + return self.encrypt_with_metadata_return_str(data) + case 'json': + return self.encrypt_with_metadata_return_str(data) + case 'bytes': + return self.encrypt_with_metadata_return_bytes(data) + case 'dict': + return self.encrypt_with_metadata_return_dict(data) + case _: + # default is string json + return self.encrypt_with_metadata_return_str(data) + + def encrypt_with_metadata_return_dict(self, data: str | bytes) -> PackageData: + """encrypt with metadata, but returns data as PackageData dict""" + return self.__encrypt_with_metadata(data) + + def encrypt_with_metadata_return_str(self, data: str | bytes) -> str: + """encrypt with metadata, but returns data in string""" + return json.dumps(self.__encrypt_with_metadata(data)) + + def encrypt_with_metadata_return_bytes(self, data: str | bytes) -> bytes: + """encrypt with metadata, but returns data in bytes""" + return json.dumps(self.__encrypt_with_metadata(data)).encode('utf-8') + + def decrypt_with_metadata(self, encrypted_package: str | bytes | PackageData, password: str | None = None) -> str: + """Decrypt data that may include metadata""" + try: + # Try to parse as JSON (password-based encryption) + if isinstance(encrypted_package, bytes): + package_data = cast(PackageData, json.loads(encrypted_package.decode('utf-8'))) + elif isinstance(encrypted_package, str): + package_data = cast(PackageData, json.loads(str(encrypted_package))) + else: + package_data = encrypted_package + + encrypted_data = base64.urlsafe_b64decode(package_data['encrypted_data']) + salt = base64.urlsafe_b64decode(package_data['salt']) + pwd = password or self.password + key = self.__derive_key_from_password(pwd, salt) + if package_data['key_hash'] != hashlib.sha256(key).hexdigest(): + raise ValueError("Key hash is not matching, possible invalid password") + cipher_suite = Fernet(key) + decrypted_data = cipher_suite.decrypt(encrypted_data) + + except (json.JSONDecodeError, KeyError, UnicodeDecodeError) as e: + raise ValueError(f"Invalid encrypted package format {e}") from e + + return decrypted_data.decode('utf-8') + + @staticmethod + def encrypt_data(data: str | bytes, password: str) -> str: + """ + Static method to encrypt some data + + Arguments: + data {str | bytes} -- _description_ + password {str} -- _description_ + + Returns: + str -- _description_ + """ + encryptor = SymmetricEncryption(password) + return encryptor.encrypt_with_metadata_return_str(data) + + @staticmethod + def decrypt_data(data: str | bytes | PackageData, password: str) -> str: + """ + Static method to decrypt some data + + Arguments: + data {str | bytes | PackageData} -- _description_ + password {str} -- _description_ + + Returns: + str -- _description_ + """ + decryptor = SymmetricEncryption(password) + return decryptor.decrypt_with_metadata(data, password=password) + +# __END__ diff --git a/test-run/encryption/symmetric_encryption.py b/test-run/encryption/symmetric_encryption.py new file mode 100644 index 0000000..a7fcb76 --- /dev/null +++ b/test-run/encryption/symmetric_encryption.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +""" +Symmetric encryption test +""" + +import json +from corelibs.debug_handling.dump_data import dump_data +from corelibs.encryption_handling.symmetric_encryption import SymmetricEncryption + + +def main() -> None: + """ + Comment + """ + password = "strongpassword" + se = SymmetricEncryption(password) + + plaintext = "Hello, World!" + ciphertext = se.encrypt_with_metadata_return_str(plaintext) + decrypted = se.decrypt_with_metadata(ciphertext) + print(f"Encrypted: {dump_data(json.loads(ciphertext))}") + print(f"Input: {plaintext} -> {decrypted}") + + static_ciphertext = SymmetricEncryption.encrypt_data(plaintext, password) + decrypted = SymmetricEncryption.decrypt_data(static_ciphertext, password) + print(f"Static Encrypted: {dump_data(json.loads(static_ciphertext))}") + print(f"Input: {plaintext} -> {decrypted}") + + +if __name__ == "__main__": + main() + +# __END__ diff --git a/tests/unit/encryption_handling/__init__.py b/tests/unit/encryption_handling/__init__.py new file mode 100644 index 0000000..a81225c --- /dev/null +++ b/tests/unit/encryption_handling/__init__.py @@ -0,0 +1,3 @@ +""" +Unit tests for encryption_handling module +""" diff --git a/tests/unit/encryption_handling/test_symmetric_encryption.py b/tests/unit/encryption_handling/test_symmetric_encryption.py new file mode 100644 index 0000000..8e4fa1e --- /dev/null +++ b/tests/unit/encryption_handling/test_symmetric_encryption.py @@ -0,0 +1,665 @@ +""" +PyTest: encryption_handling/symmetric_encryption +""" +# pylint: disable=redefined-outer-name +# ^ Disabled because pytest fixtures intentionally redefine names + +import os +import json +import base64 +import hashlib +import pytest +from corelibs.encryption_handling.symmetric_encryption import ( + SymmetricEncryption +) + + +class TestSymmetricEncryptionInitialization: + """Tests for SymmetricEncryption initialization""" + + def test_valid_password_initialization(self): + """Test initialization with a valid password""" + encryptor = SymmetricEncryption("test_password") + assert encryptor.password == "test_password" + assert encryptor.password_hash == hashlib.sha256("test_password".encode('utf-8')).hexdigest() + + def test_empty_password_raises_error(self): + """Test that empty password raises ValueError""" + with pytest.raises(ValueError, match="A password must be set"): + SymmetricEncryption("") + + def test_password_hash_is_consistent(self): + """Test that password hash is consistently generated""" + encryptor1 = SymmetricEncryption("test_password") + encryptor2 = SymmetricEncryption("test_password") + assert encryptor1.password_hash == encryptor2.password_hash + + def test_different_passwords_different_hashes(self): + """Test that different passwords produce different hashes""" + encryptor1 = SymmetricEncryption("password1") + encryptor2 = SymmetricEncryption("password2") + assert encryptor1.password_hash != encryptor2.password_hash + + +class TestEncryptWithMetadataReturnDict: + """Tests for encrypt_with_metadata_return_dict method""" + + def test_encrypt_string_returns_package_data(self): + """Test encrypting a string returns PackageData dict""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata_return_dict("test data") + + assert isinstance(result, dict) + assert 'encrypted_data' in result + assert 'salt' in result + assert 'key_hash' in result + + def test_encrypt_bytes_returns_package_data(self): + """Test encrypting bytes returns PackageData dict""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata_return_dict(b"test data") + + assert isinstance(result, dict) + assert 'encrypted_data' in result + assert 'salt' in result + assert 'key_hash' in result + + def test_encrypted_data_is_base64_encoded(self): + """Test that encrypted_data is base64 encoded""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata_return_dict("test data") + + # Should not raise exception when decoding + base64.urlsafe_b64decode(result['encrypted_data']) + + def test_salt_is_base64_encoded(self): + """Test that salt is base64 encoded""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata_return_dict("test data") + + # Should not raise exception when decoding + salt = base64.urlsafe_b64decode(result['salt']) + # Salt should be 16 bytes + assert len(salt) == 16 + + def test_key_hash_is_valid_hex(self): + """Test that key_hash is a valid hex string""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata_return_dict("test data") + + # Should be 64 characters (SHA256 hex) + assert len(result['key_hash']) == 64 + # Should only contain hex characters + int(result['key_hash'], 16) + + def test_different_salts_for_each_encryption(self): + """Test that each encryption uses a different salt""" + encryptor = SymmetricEncryption("test_password") + result1 = encryptor.encrypt_with_metadata_return_dict("test data") + result2 = encryptor.encrypt_with_metadata_return_dict("test data") + + assert result1['salt'] != result2['salt'] + assert result1['encrypted_data'] != result2['encrypted_data'] + + +class TestEncryptWithMetadataReturnStr: + """Tests for encrypt_with_metadata_return_str method""" + + def test_returns_json_string(self): + """Test that method returns a valid JSON string""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata_return_str("test data") + + assert isinstance(result, str) + # Should be valid JSON + parsed = json.loads(result) + assert 'encrypted_data' in parsed + assert 'salt' in parsed + assert 'key_hash' in parsed + + def test_json_string_parseable(self): + """Test that returned JSON string can be parsed back""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata_return_str("test data") + + parsed = json.loads(result) + assert isinstance(parsed, dict) + + +class TestEncryptWithMetadataReturnBytes: + """Tests for encrypt_with_metadata_return_bytes method""" + + def test_returns_bytes(self): + """Test that method returns bytes""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata_return_bytes("test data") + + assert isinstance(result, bytes) + + def test_bytes_contains_valid_json(self): + """Test that returned bytes contain valid JSON""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata_return_bytes("test data") + + # Should be valid JSON when decoded + parsed = json.loads(result.decode('utf-8')) + assert 'encrypted_data' in parsed + assert 'salt' in parsed + assert 'key_hash' in parsed + + +class TestEncryptWithMetadata: + """Tests for encrypt_with_metadata method with different return types""" + + def test_return_as_str(self): + """Test encrypt_with_metadata with return_as='str'""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata("test data", return_as='str') + + assert isinstance(result, str) + json.loads(result) # Should be valid JSON + + def test_return_as_json(self): + """Test encrypt_with_metadata with return_as='json'""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata("test data", return_as='json') + + assert isinstance(result, str) + json.loads(result) # Should be valid JSON + + def test_return_as_bytes(self): + """Test encrypt_with_metadata with return_as='bytes'""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata("test data", return_as='bytes') + + assert isinstance(result, bytes) + + def test_return_as_dict(self): + """Test encrypt_with_metadata with return_as='dict'""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata("test data", return_as='dict') + + assert isinstance(result, dict) + assert 'encrypted_data' in result + + def test_default_return_type(self): + """Test encrypt_with_metadata default return type""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata("test data") + + # Default should be 'str' + assert isinstance(result, str) + + def test_invalid_return_type_defaults_to_str(self): + """Test that invalid return_as defaults to str""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata("test data", return_as='invalid') + + assert isinstance(result, str) + + +class TestDecryptWithMetadata: + """Tests for decrypt_with_metadata method""" + + def test_decrypt_string_package(self): + """Test decrypting a string JSON package""" + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str("test data") + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == "test data" + + def test_decrypt_bytes_package(self): + """Test decrypting a bytes JSON package""" + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_bytes("test data") + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == "test data" + + def test_decrypt_dict_package(self): + """Test decrypting a dict PackageData""" + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_dict("test data") + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == "test data" + + def test_decrypt_with_different_password_fails(self): + """Test that decrypting with wrong password fails""" + encryptor = SymmetricEncryption("password1") + encrypted = encryptor.encrypt_with_metadata_return_str("test data") + + decryptor = SymmetricEncryption("password2") + with pytest.raises(ValueError, match="Key hash is not matching"): + decryptor.decrypt_with_metadata(encrypted) + + def test_decrypt_with_explicit_password(self): + """Test decrypting with explicitly provided password""" + encryptor = SymmetricEncryption("password1") + encrypted = encryptor.encrypt_with_metadata_return_str("test data") + + # Decrypt with different password parameter + decryptor = SymmetricEncryption("password1") + decrypted = decryptor.decrypt_with_metadata(encrypted, password="password1") + + assert decrypted == "test data" + + def test_decrypt_invalid_json_raises_error(self): + """Test that invalid JSON raises ValueError""" + encryptor = SymmetricEncryption("test_password") + + with pytest.raises(ValueError, match="Invalid encrypted package format"): + encryptor.decrypt_with_metadata("not valid json") + + def test_decrypt_missing_fields_raises_error(self): + """Test that missing required fields raises ValueError""" + encryptor = SymmetricEncryption("test_password") + invalid_package = json.dumps({"encrypted_data": "test"}) + + with pytest.raises(ValueError, match="Invalid encrypted package format"): + encryptor.decrypt_with_metadata(invalid_package) + + def test_decrypt_unicode_data(self): + """Test encrypting and decrypting unicode data""" + encryptor = SymmetricEncryption("test_password") + unicode_data = "Hello δΈ–η•Œ 🌍" + encrypted = encryptor.encrypt_with_metadata_return_str(unicode_data) + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == unicode_data + + def test_decrypt_empty_string(self): + """Test encrypting and decrypting empty string""" + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str("") + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == "" + + def test_decrypt_long_data(self): + """Test encrypting and decrypting long data""" + encryptor = SymmetricEncryption("test_password") + long_data = "A" * 10000 + encrypted = encryptor.encrypt_with_metadata_return_str(long_data) + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == long_data + + +class TestStaticMethods: + """Tests for static methods encrypt_data and decrypt_data""" + + def test_encrypt_data_static_method(self): + """Test static encrypt_data method""" + encrypted = SymmetricEncryption.encrypt_data("test data", "test_password") + + assert isinstance(encrypted, str) + # Should be valid JSON + parsed = json.loads(encrypted) + assert 'encrypted_data' in parsed + assert 'salt' in parsed + assert 'key_hash' in parsed + + def test_decrypt_data_static_method(self): + """Test static decrypt_data method""" + encrypted = SymmetricEncryption.encrypt_data("test data", "test_password") + decrypted = SymmetricEncryption.decrypt_data(encrypted, "test_password") + + assert decrypted == "test data" + + def test_static_methods_roundtrip(self): + """Test complete roundtrip using static methods""" + original = "test data with special chars: !@#$%^&*()" + encrypted = SymmetricEncryption.encrypt_data(original, "test_password") + decrypted = SymmetricEncryption.decrypt_data(encrypted, "test_password") + + assert decrypted == original + + def test_static_decrypt_with_bytes(self): + """Test static decrypt_data with bytes input""" + encrypted = SymmetricEncryption.encrypt_data("test data", "test_password") + encrypted_bytes = encrypted.encode('utf-8') + decrypted = SymmetricEncryption.decrypt_data(encrypted_bytes, "test_password") + + assert decrypted == "test data" + + def test_static_decrypt_with_dict(self): + """Test static decrypt_data with PackageData dict""" + encryptor = SymmetricEncryption("test_password") + encrypted_dict = encryptor.encrypt_with_metadata_return_dict("test data") + decrypted = SymmetricEncryption.decrypt_data(encrypted_dict, "test_password") + + assert decrypted == "test data" + + def test_static_encrypt_bytes_data(self): + """Test static encrypt_data with bytes input""" + encrypted = SymmetricEncryption.encrypt_data(b"test data", "test_password") + decrypted = SymmetricEncryption.decrypt_data(encrypted, "test_password") + + assert decrypted == "test data" + + +class TestEncryptionSecurity: + """Security-related tests for encryption""" + + def test_same_data_different_encryption(self): + """Test that same data produces different encrypted outputs due to salt""" + encryptor = SymmetricEncryption("test_password") + encrypted1 = encryptor.encrypt_with_metadata_return_str("test data") + encrypted2 = encryptor.encrypt_with_metadata_return_str("test data") + + assert encrypted1 != encrypted2 + + def test_password_not_recoverable_from_hash(self): + """Test that password hash is one-way""" + encryptor = SymmetricEncryption("secret_password") + # The password_hash should be SHA256 hex (64 chars) + assert len(encryptor.password_hash) == 64 + # Password should not be easily derivable from hash + assert "secret_password" not in encryptor.password_hash + + def test_encrypted_data_not_plaintext(self): + """Test that encrypted data doesn't contain plaintext""" + encryptor = SymmetricEncryption("test_password") + plaintext = "very_secret_data_12345" + encrypted = encryptor.encrypt_with_metadata_return_str(plaintext) + + # Plaintext should not appear in encrypted output + assert plaintext not in encrypted + + def test_modified_encrypted_data_fails_decryption(self): + """Test that modified encrypted data fails to decrypt""" + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str("test data") + + # Modify the encrypted data + encrypted_dict = json.loads(encrypted) + encrypted_dict['encrypted_data'] = encrypted_dict['encrypted_data'][:-5] + "AAAAA" + modified_encrypted = json.dumps(encrypted_dict) + + # Should fail to decrypt + with pytest.raises(Exception): # Fernet will raise an exception + encryptor.decrypt_with_metadata(modified_encrypted) + + def test_modified_salt_fails_decryption(self): + """Test that modified salt fails to decrypt""" + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str("test data") + + # Modify the salt + encrypted_dict = json.loads(encrypted) + original_salt = base64.urlsafe_b64decode(encrypted_dict['salt']) + modified_salt = bytes([b ^ 1 for b in original_salt]) + encrypted_dict['salt'] = base64.urlsafe_b64encode(modified_salt).decode('utf-8') + modified_encrypted = json.dumps(encrypted_dict) + + # Should fail to decrypt due to key hash mismatch + with pytest.raises(ValueError, match="Key hash is not matching"): + encryptor.decrypt_with_metadata(modified_encrypted) + + +class TestEdgeCases: + """Edge case tests""" + + def test_very_long_password(self): + """Test with very long password""" + long_password = "a" * 1000 + encryptor = SymmetricEncryption(long_password) + encrypted = encryptor.encrypt_with_metadata_return_str("test data") + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == "test data" + + def test_special_characters_in_data(self): + """Test encryption of data with special characters""" + special_data = "!@#$%^&*()_+-=[]{}|;':\",./<>?\n\t\r" + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str(special_data) + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == special_data + + def test_binary_data_utf8_bytes(self): + """Test encryption of UTF-8 encoded bytes""" + # Test with UTF-8 encoded bytes + utf8_bytes = "Hello δΈ–η•Œ 🌍".encode('utf-8') + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str(utf8_bytes) + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == "Hello δΈ–η•Œ 🌍" + + def test_binary_data_with_base64_encoding(self): + """Test encryption of arbitrary binary data using base64 encoding""" + # For arbitrary binary data, encode to base64 first + binary_data = bytes(range(256)) + base64_encoded = base64.b64encode(binary_data).decode('utf-8') + + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str(base64_encoded) + decrypted = encryptor.decrypt_with_metadata(encrypted) + + # Decode back to binary + decoded_binary = base64.b64decode(decrypted) + assert decoded_binary == binary_data + + def test_binary_data_image_simulation(self): + """Test encryption of simulated binary image data""" + # Simulate image binary data (random bytes) + image_data = os.urandom(1024) # 1KB of random binary data + base64_encoded = base64.b64encode(image_data).decode('utf-8') + + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str(base64_encoded) + decrypted = encryptor.decrypt_with_metadata(encrypted) + + # Verify round-trip + decoded_data = base64.b64decode(decrypted) + assert decoded_data == image_data + + def test_binary_data_with_null_bytes(self): + """Test encryption of data containing null bytes""" + # Create data with null bytes + data_with_nulls = "text\x00with\x00nulls\x00bytes" + + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str(data_with_nulls) + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == data_with_nulls + + def test_binary_data_bytes_input(self): + """Test encryption with bytes input directly""" + # UTF-8 compatible bytes + byte_data = b"Binary data test" + + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str(byte_data) + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == "Binary data test" + + def test_binary_data_large_file_simulation(self): + """Test encryption of large binary data (simulated file)""" + # Simulate a larger binary file (10KB) + large_data = os.urandom(10240) + base64_encoded = base64.b64encode(large_data).decode('utf-8') + + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str(base64_encoded) + decrypted = encryptor.decrypt_with_metadata(encrypted) + + # Verify integrity + decoded_data = base64.b64decode(decrypted) + assert len(decoded_data) == 10240 + assert decoded_data == large_data + + def test_binary_data_json_with_base64(self): + """Test encryption of JSON containing base64 encoded binary data""" + binary_data = os.urandom(256) + json_data = json.dumps({ + "filename": "test.bin", + "data": base64.b64encode(binary_data).decode('utf-8'), + "size": len(binary_data) + }) + + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str(json_data) + decrypted = encryptor.decrypt_with_metadata(encrypted) + + # Parse and verify + parsed = json.loads(decrypted) + assert parsed["filename"] == "test.bin" + assert parsed["size"] == 256 + decoded_binary = base64.b64decode(parsed["data"]) + assert decoded_binary == binary_data + + def test_numeric_password(self): + """Test with numeric string password""" + encryptor = SymmetricEncryption("12345") + encrypted = encryptor.encrypt_with_metadata_return_str("test data") + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == "test data" + + def test_unicode_password(self): + """Test with unicode password""" + encryptor = SymmetricEncryption("パスワード123") + encrypted = encryptor.encrypt_with_metadata_return_str("test data") + decrypted = encryptor.decrypt_with_metadata(encrypted) + + assert decrypted == "test data" + + +class TestIntegration: + """Integration tests""" + + def test_multiple_encrypt_decrypt_cycles(self): + """Test multiple encryption/decryption cycles""" + encryptor = SymmetricEncryption("test_password") + original = "test data" + + # Encrypt and decrypt multiple times + for _ in range(5): + encrypted = encryptor.encrypt_with_metadata_return_str(original) + decrypted = encryptor.decrypt_with_metadata(encrypted) + assert decrypted == original + + def test_different_return_types_interoperability(self): + """Test that different return types can be decrypted""" + encryptor = SymmetricEncryption("test_password") + original = "test data" + + # Encrypt with different return types + encrypted_str = encryptor.encrypt_with_metadata_return_str(original) + encrypted_bytes = encryptor.encrypt_with_metadata_return_bytes(original) + encrypted_dict = encryptor.encrypt_with_metadata_return_dict(original) + + # All should decrypt to the same value + assert encryptor.decrypt_with_metadata(encrypted_str) == original + assert encryptor.decrypt_with_metadata(encrypted_bytes) == original + assert encryptor.decrypt_with_metadata(encrypted_dict) == original + + def test_cross_instance_encryption_decryption(self): + """Test that different instances with same password can decrypt""" + encryptor1 = SymmetricEncryption("test_password") + encryptor2 = SymmetricEncryption("test_password") + + encrypted = encryptor1.encrypt_with_metadata_return_str("test data") + decrypted = encryptor2.decrypt_with_metadata(encrypted) + + assert decrypted == "test data" + + def test_static_and_instance_methods_compatible(self): + """Test that static and instance methods are compatible""" + # Encrypt with static method + encrypted = SymmetricEncryption.encrypt_data("test data", "test_password") + + # Decrypt with instance method + decryptor = SymmetricEncryption("test_password") + decrypted = decryptor.decrypt_with_metadata(encrypted) + + assert decrypted == "test data" + + # And vice versa + encryptor = SymmetricEncryption("test_password") + encrypted2 = encryptor.encrypt_with_metadata_return_str("test data 2") + decrypted2 = SymmetricEncryption.decrypt_data(encrypted2, "test_password") + + assert decrypted2 == "test data 2" + + +# Parametrized tests +@pytest.mark.parametrize("data", [ + "simple text", + "text with spaces and punctuation!", + "123456789", + "unicode: こんにけは", + "emoji: πŸ”πŸ”‘", + "", + "a" * 1000, # Long string +]) +def test_encrypt_decrypt_various_data(data: str): + """Parametrized test for various data types""" + encryptor = SymmetricEncryption("test_password") + encrypted = encryptor.encrypt_with_metadata_return_str(data) + decrypted = encryptor.decrypt_with_metadata(encrypted) + assert decrypted == data + + +@pytest.mark.parametrize("password", [ + "simple", + "with spaces", + "special!@#$%", + "unicodeδΈ–η•Œ", + "123456", + "a" * 100, # Long password +]) +def test_various_passwords(password: str): + """Parametrized test for various passwords""" + encryptor = SymmetricEncryption(password) + encrypted = encryptor.encrypt_with_metadata_return_str("test data") + decrypted = encryptor.decrypt_with_metadata(encrypted) + assert decrypted == "test data" + + +@pytest.mark.parametrize("return_type,expected_type", [ + ("str", str), + ("json", str), + ("bytes", bytes), + ("dict", dict), +]) +def test_return_types_parametrized(return_type: str, expected_type: type): + """Parametrized test for different return types""" + encryptor = SymmetricEncryption("test_password") + result = encryptor.encrypt_with_metadata("test data", return_as=return_type) + assert isinstance(result, expected_type) + + +# Fixtures +@pytest.fixture +def encryptor() -> SymmetricEncryption: + """Fixture providing a basic encryptor instance""" + return SymmetricEncryption("test_password") + + +@pytest.fixture +def sample_encrypted_data(encryptor: SymmetricEncryption) -> str: + """Fixture providing sample encrypted data""" + return encryptor.encrypt_with_metadata_return_str("sample data") + + +def test_with_encryptor_fixture(encryptor: SymmetricEncryption) -> None: + """Test using encryptor fixture""" + encrypted: str = encryptor.encrypt_with_metadata_return_str("test") + decrypted: str = encryptor.decrypt_with_metadata(encrypted) + assert decrypted == "test" + + +def test_with_encrypted_data_fixture(encryptor: SymmetricEncryption, sample_encrypted_data: str) -> None: + """Test using encrypted data fixture""" + decrypted: str = encryptor.decrypt_with_metadata(sample_encrypted_data) + assert decrypted == "sample data" + +# __END__ diff --git a/uv.lock b/uv.lock index 4ac9997..4f0cf30 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.3" @@ -53,9 +98,10 @@ wheels = [ [[package]] name = "corelibs" -version = "0.25.0" +version = "0.25.1" source = { editable = "." } dependencies = [ + { name = "cryptography" }, { name = "jmespath" }, { name = "psutil" }, { name = "requests" }, @@ -69,6 +115,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "cryptography", specifier = ">=46.0.3" }, { name = "jmespath", specifier = ">=1.0.1" }, { name = "psutil", specifier = ">=7.0.0" }, { name = "requests", specifier = ">=2.32.4" }, @@ -133,6 +180,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736, upload-time = "2025-08-23T14:42:43.145Z" }, ] +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -193,6 +296,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, ] +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + [[package]] name = "pygments" version = "2.19.2"