From 3e9b0dee2d926c9f956a932efbaf58799f77b7bd Mon Sep 17 00:00:00 2001 From: Austin Walker Date: Fri, 27 Jun 2025 12:50:37 -0400 Subject: [PATCH 1/9] Add encrypt_secret helper function --- gen.yaml | 2 +- src/unstructured_client/users.py | 126 +++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/gen.yaml b/gen.yaml index a0125f9a..f2384c37 100644 --- a/gen.yaml +++ b/gen.yaml @@ -39,7 +39,7 @@ python: clientServerStatusCodesAsErrors: true defaultErrorName: SDKError description: Python Client SDK for Unstructured API - enableCustomCodeRegions: false + enableCustomCodeRegions: true enumFormat: enum fixFlags: responseRequiredSep2024: false diff --git a/src/unstructured_client/users.py b/src/unstructured_client/users.py index 1a4c5ab3..153f5108 100644 --- a/src/unstructured_client/users.py +++ b/src/unstructured_client/users.py @@ -7,6 +7,16 @@ from unstructured_client.models import errors, operations, shared from unstructured_client.types import BaseModel, OptionalNullable, UNSET +# region imports +from cryptography import x509 +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.serialization import load_pem_public_key +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend +import os +import base64 +# endregion imports class Users(BaseSDK): def retrieve( @@ -458,3 +468,119 @@ async def store_secret_async( http_res_text, http_res, ) + + # region sdk-class-body + def _encrypt_rsa_aes( + self, + encryption_key_pem: str, + plaintext: str, + ) -> dict: + # Load public RSA key + public_key = serialization.load_pem_public_key( + encryption_key_pem.encode('utf-8'), + backend=default_backend() + ) + + # Generate a random AES key + aes_key = os.urandom(32) # 256-bit AES key + + # Generate a random IV + iv = os.urandom(16) + + # Encrypt using AES-CFB + cipher = Cipher( + algorithms.AES(aes_key), + modes.CFB(iv), + ) + encryptor = cipher.encryptor() + ciphertext = encryptor.update(plaintext.encode('utf-8')) + encryptor.finalize() + + # Encrypt the AES key using the RSA public key + encrypted_key = public_key.encrypt( + aes_key, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + + return { + 'encrypted_aes_key': base64.b64encode(encrypted_key).decode('utf-8'), + 'aes_iv': base64.b64encode(iv).decode('utf-8'), + 'encrypted_value': base64.b64encode(ciphertext).decode('utf-8'), + 'type': 'rsa_aes', + } + + def _encrypt_rsa( + self, + encryption_key_pem: str, + plaintext: str, + ) -> dict: + # Load public RSA key + public_key = serialization.load_pem_public_key( + encryption_key_pem.encode('utf-8'), + backend=default_backend() + ) + + ciphertext = public_key.encrypt( + plaintext.encode(), + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ), + ) + return { + 'encrypted_value': base64.b64encode(ciphertext).decode('utf-8'), + 'type': 'rsa', + 'encrypted_aes_key': "", + 'aes_iv': "", + } + + + def encrypt_secret( + self, + encryption_cert_or_key_pem: str, + plaintext: str, + type: Optional[str] = None, + ) -> dict: + """ + Encrypts a plaintext string for securely sending to the Unstructured API. + + Args: + encryption_cert_or_key_pem (str): A PEM-encoded RSA public key or certificate. + plaintext (str): The string to encrypt. + type (str, optional): Encryption type, either "rsa" or "rsa_aes". + + Returns: + dict: A dictionary with encrypted AES key, iv, and ciphertext (all base64-encoded). + """ + # If a cert is provided, extract the public key + if "BEGIN CERTIFICATE" in encryption_cert_or_key_pem: + cert = x509.load_pem_x509_certificate( + encryption_cert_or_key_pem.encode('utf-8'), + ) + + loaded_key = cert.public_key() + + # Serialize back to PEM format for consistency + public_key_pem = loaded_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + + else: + public_key_pem = encryption_cert_or_key_pem + + # If the plaintext is short, use RSA directly + # Otherwise, use a RSA_AES envelope hybrid + # The length of the public key is a good hueristic + if not type: + type = "rsa" if len(plaintext) <= len(public_key_pem) else "rsa_aes" + + if type == "rsa": + return self._encrypt_rsa(public_key_pem, plaintext) + else: + return self._encrypt_rsa_aes(public_key_pem, plaintext) + # endregion sdk-class-body From 600913431368425c132f23dbf941e7f152b7562a Mon Sep 17 00:00:00 2001 From: Austin Walker Date: Fri, 27 Jun 2025 13:07:01 -0400 Subject: [PATCH 2/9] Add encryption tests --- .../unit/test_encryption.py | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 _test_unstructured_client/unit/test_encryption.py diff --git a/_test_unstructured_client/unit/test_encryption.py b/_test_unstructured_client/unit/test_encryption.py new file mode 100644 index 00000000..8daefa93 --- /dev/null +++ b/_test_unstructured_client/unit/test_encryption.py @@ -0,0 +1,128 @@ +from cryptography import x509 +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend +import os +import base64 +from typing import Optional + +import pytest + +from unstructured_client import UnstructuredClient + +@pytest.fixture +def rsa_key_pair(): + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend() + ) + public_key = private_key.public_key() + + private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ).decode('utf-8') + + public_key_pem = public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + + return private_key_pem, public_key_pem + + +def decrypt_secret( + private_key_pem: str, + encrypted_value: str, + type: str, + encrypted_aes_key: str, + aes_iv: str, +) -> str: + private_key = serialization.load_pem_private_key( + private_key_pem.encode('utf-8'), + password=None, + backend=default_backend() + ) + + if type == 'rsa': + ciphertext = base64.b64decode(encrypted_value) + plaintext = private_key.decrypt( + ciphertext, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + return plaintext.decode('utf-8') + else: + encrypted_aes_key = base64.b64decode(encrypted_aes_key) + iv = base64.b64decode(aes_iv) + ciphertext = base64.b64decode(encrypted_value) + + aes_key = private_key.decrypt( + encrypted_aes_key, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + cipher = Cipher( + algorithms.AES(aes_key), + modes.CFB(iv), + ) + decryptor = cipher.decryptor() + plaintext = decryptor.update(ciphertext) + decryptor.finalize() + return plaintext.decode('utf-8') + + +def test_encrypt_rsa(rsa_key_pair): + private_key_pem, public_key_pem = rsa_key_pair + + client = UnstructuredClient() + + plaintext = "This is a secret message." + + secret_obj = client.users.encrypt_secret(public_key_pem, plaintext) + + # A short payload should use direct RSA encryption + assert secret_obj["type"] == 'rsa' + + decrypted_text = decrypt_secret( + private_key_pem, + secret_obj["encrypted_value"], + secret_obj["type"], + "", + "", + ) + assert decrypted_text == plaintext + + assert True + + +def test_encrypt_rsa_aes(rsa_key_pair): + private_key_pem, public_key_pem = rsa_key_pair + + client = UnstructuredClient() + + plaintext = "This is a secret message." * 100 + + secret_obj = client.users.encrypt_secret(public_key_pem, plaintext) + + # A longer payload uses hybrid RSA-AES encryption + assert secret_obj["type"] == 'rsa_aes' + + decrypted_text = decrypt_secret( + private_key_pem, + secret_obj["encrypted_value"], + secret_obj["type"], + secret_obj["encrypted_aes_key"], + secret_obj["aes_iv"], + ) + assert decrypted_text == plaintext + + assert True \ No newline at end of file From 49eb79d2c0417c35f301a7ca9dbd20fd15c24666 Mon Sep 17 00:00:00 2001 From: Austin Walker Date: Fri, 27 Jun 2025 13:07:13 -0400 Subject: [PATCH 3/9] Throw an error if wrong key type is used --- src/unstructured_client/users.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/unstructured_client/users.py b/src/unstructured_client/users.py index 153f5108..ba4d5cd7 100644 --- a/src/unstructured_client/users.py +++ b/src/unstructured_client/users.py @@ -10,7 +10,6 @@ # region imports from cryptography import x509 from cryptography.hazmat.primitives import serialization, hashes -from cryptography.hazmat.primitives.serialization import load_pem_public_key from cryptography.hazmat.primitives.asymmetric import padding, rsa from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend @@ -481,6 +480,9 @@ def _encrypt_rsa_aes( backend=default_backend() ) + if not isinstance(public_key, rsa.RSAPublicKey): + raise TypeError("Public key must be an RSA public key for envelope encryption.") + # Generate a random AES key aes_key = os.urandom(32) # 256-bit AES key @@ -523,6 +525,9 @@ def _encrypt_rsa( backend=default_backend() ) + if not isinstance(public_key, rsa.RSAPublicKey): + raise TypeError("Public key must be an RSA public key for encryption.") + ciphertext = public_key.encrypt( plaintext.encode(), padding.OAEP( From d9a6b4c610c555319e11815214593a4b25e339b0 Mon Sep 17 00:00:00 2001 From: Austin Walker Date: Fri, 27 Jun 2025 13:09:33 -0400 Subject: [PATCH 4/9] Some linter cleanup --- src/unstructured_client/users.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/unstructured_client/users.py b/src/unstructured_client/users.py index ba4d5cd7..e155029a 100644 --- a/src/unstructured_client/users.py +++ b/src/unstructured_client/users.py @@ -548,7 +548,7 @@ def encrypt_secret( self, encryption_cert_or_key_pem: str, plaintext: str, - type: Optional[str] = None, + encryption_type: Optional[str] = None, ) -> dict: """ Encrypts a plaintext string for securely sending to the Unstructured API. @@ -581,11 +581,11 @@ def encrypt_secret( # If the plaintext is short, use RSA directly # Otherwise, use a RSA_AES envelope hybrid # The length of the public key is a good hueristic - if not type: - type = "rsa" if len(plaintext) <= len(public_key_pem) else "rsa_aes" + if not encryption_type: + encryption_type = "rsa" if len(plaintext) <= len(public_key_pem) else "rsa_aes" - if type == "rsa": + if encryption_type == "rsa": return self._encrypt_rsa(public_key_pem, plaintext) - else: - return self._encrypt_rsa_aes(public_key_pem, plaintext) + + return self._encrypt_rsa_aes(public_key_pem, plaintext) # endregion sdk-class-body From 558c1a5078b742bf5599ac1fa8a9cae05695fe29 Mon Sep 17 00:00:00 2001 From: Austin Walker Date: Fri, 27 Jun 2025 13:12:05 -0400 Subject: [PATCH 5/9] Remove extra lines --- _test_unstructured_client/unit/test_encryption.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/_test_unstructured_client/unit/test_encryption.py b/_test_unstructured_client/unit/test_encryption.py index 8daefa93..c51f3920 100644 --- a/_test_unstructured_client/unit/test_encryption.py +++ b/_test_unstructured_client/unit/test_encryption.py @@ -101,8 +101,6 @@ def test_encrypt_rsa(rsa_key_pair): ) assert decrypted_text == plaintext - assert True - def test_encrypt_rsa_aes(rsa_key_pair): private_key_pem, public_key_pem = rsa_key_pair @@ -123,6 +121,4 @@ def test_encrypt_rsa_aes(rsa_key_pair): secret_obj["encrypted_aes_key"], secret_obj["aes_iv"], ) - assert decrypted_text == plaintext - - assert True \ No newline at end of file + assert decrypted_text == plaintext \ No newline at end of file From b945912436f5bad6a71fa0212f1a90d9c195979a Mon Sep 17 00:00:00 2001 From: Austin Walker Date: Tue, 1 Jul 2025 13:18:40 -0400 Subject: [PATCH 6/9] Use the correct max size for RSA --- .../unit/test_encryption.py | 34 ++++++++++++- src/unstructured_client/users.py | 50 +++++++------------ 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/_test_unstructured_client/unit/test_encryption.py b/_test_unstructured_client/unit/test_encryption.py index c51f3920..2db2ef2c 100644 --- a/_test_unstructured_client/unit/test_encryption.py +++ b/_test_unstructured_client/unit/test_encryption.py @@ -121,4 +121,36 @@ def test_encrypt_rsa_aes(rsa_key_pair): secret_obj["encrypted_aes_key"], secret_obj["aes_iv"], ) - assert decrypted_text == plaintext \ No newline at end of file + assert decrypted_text == plaintext + + +rsa_key_size_bytes = 2048 // 8 +max_payload_size = rsa_key_size_bytes - 66 # OAEP SHA256 overhead + +@pytest.mark.parametrize(("plaintext", "secret_type"), [ + ("Short message", "rsa"), + ("A" * (max_payload_size), "rsa"), # Just at the RSA limit + ("A" * (max_payload_size + 1), "rsa_aes"), # Just over the RSA limit + ("A" * 500, "rsa_aes"), # Well over the RSA limit +]) +def test_encrypt_around_rsa_size_limit(rsa_key_pair, plaintext, secret_type): + """ + Test that payloads around the RSA size limit choose the correct algorithm. + """ + _, public_key_pem = rsa_key_pair + + print(f"Testing plaintext of length {len(plaintext)} with expected type {secret_type}") + + # Load the public key + public_key = serialization.load_pem_public_key( + public_key_pem.encode('utf-8'), + backend=default_backend() + ) + + client = UnstructuredClient() + + secret_obj = client.users.encrypt_secret(public_key_pem, plaintext) + + # Should still use direct RSA encryption + assert secret_obj["type"] == secret_type + assert secret_obj["encrypted_value"] is not None \ No newline at end of file diff --git a/src/unstructured_client/users.py b/src/unstructured_client/users.py index e155029a..f6f52a4f 100644 --- a/src/unstructured_client/users.py +++ b/src/unstructured_client/users.py @@ -471,18 +471,9 @@ async def store_secret_async( # region sdk-class-body def _encrypt_rsa_aes( self, - encryption_key_pem: str, + public_key: rsa.RSAPublicKey, plaintext: str, ) -> dict: - # Load public RSA key - public_key = serialization.load_pem_public_key( - encryption_key_pem.encode('utf-8'), - backend=default_backend() - ) - - if not isinstance(public_key, rsa.RSAPublicKey): - raise TypeError("Public key must be an RSA public key for envelope encryption.") - # Generate a random AES key aes_key = os.urandom(32) # 256-bit AES key @@ -516,18 +507,10 @@ def _encrypt_rsa_aes( def _encrypt_rsa( self, - encryption_key_pem: str, + public_key: rsa.RSAPublicKey, plaintext: str, ) -> dict: # Load public RSA key - public_key = serialization.load_pem_public_key( - encryption_key_pem.encode('utf-8'), - backend=default_backend() - ) - - if not isinstance(public_key, rsa.RSAPublicKey): - raise TypeError("Public key must be an RSA public key for encryption.") - ciphertext = public_key.encrypt( plaintext.encode(), padding.OAEP( @@ -567,25 +550,28 @@ def encrypt_secret( encryption_cert_or_key_pem.encode('utf-8'), ) - loaded_key = cert.public_key() - - # Serialize back to PEM format for consistency - public_key_pem = loaded_key.public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo - ).decode('utf-8') - + public_key = cert.public_key() else: - public_key_pem = encryption_cert_or_key_pem + public_key = serialization.load_pem_public_key( + encryption_cert_or_key_pem.encode('utf-8'), + backend=default_backend() + ) + + if not isinstance(public_key, rsa.RSAPublicKey): + raise TypeError("Public key must be a RSA public key for encryption.") # If the plaintext is short, use RSA directly # Otherwise, use a RSA_AES envelope hybrid - # The length of the public key is a good hueristic + # Use the length of the public key to determine the encryption type + key_size_bytes = public_key.key_size // 8 + max_rsa_length = key_size_bytes - 66 # OAEP SHA256 overhead + print(max_rsa_length) + if not encryption_type: - encryption_type = "rsa" if len(plaintext) <= len(public_key_pem) else "rsa_aes" + encryption_type = "rsa" if len(plaintext) <= max_rsa_length else "rsa_aes" if encryption_type == "rsa": - return self._encrypt_rsa(public_key_pem, plaintext) + return self._encrypt_rsa(public_key, plaintext) - return self._encrypt_rsa_aes(public_key_pem, plaintext) + return self._encrypt_rsa_aes(public_key, plaintext) # endregion sdk-class-body From f7a1625cb448efc8667d8c71b7212ddd598d10d0 Mon Sep 17 00:00:00 2001 From: Austin Walker Date: Tue, 1 Jul 2025 13:34:55 -0400 Subject: [PATCH 7/9] Fix lint error --- src/unstructured_client/users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/unstructured_client/users.py b/src/unstructured_client/users.py index f6f52a4f..6e6880d4 100644 --- a/src/unstructured_client/users.py +++ b/src/unstructured_client/users.py @@ -550,12 +550,12 @@ def encrypt_secret( encryption_cert_or_key_pem.encode('utf-8'), ) - public_key = cert.public_key() + public_key = cert.public_key() # type: ignore[assignment] else: public_key = serialization.load_pem_public_key( encryption_cert_or_key_pem.encode('utf-8'), backend=default_backend() - ) + ) # type: ignore[assignment] if not isinstance(public_key, rsa.RSAPublicKey): raise TypeError("Public key must be a RSA public key for encryption.") From 89b75b5905a450b43326ca09e699919ef699eda3 Mon Sep 17 00:00:00 2001 From: Austin Walker Date: Tue, 1 Jul 2025 13:37:43 -0400 Subject: [PATCH 8/9] Remove a comment --- _test_unstructured_client/unit/test_encryption.py | 1 - 1 file changed, 1 deletion(-) diff --git a/_test_unstructured_client/unit/test_encryption.py b/_test_unstructured_client/unit/test_encryption.py index 2db2ef2c..95508f6d 100644 --- a/_test_unstructured_client/unit/test_encryption.py +++ b/_test_unstructured_client/unit/test_encryption.py @@ -151,6 +151,5 @@ def test_encrypt_around_rsa_size_limit(rsa_key_pair, plaintext, secret_type): secret_obj = client.users.encrypt_secret(public_key_pem, plaintext) - # Should still use direct RSA encryption assert secret_obj["type"] == secret_type assert secret_obj["encrypted_value"] is not None \ No newline at end of file From 68aa5499f6cfd672d6a4748623fe57c0507cd874 Mon Sep 17 00:00:00 2001 From: Austin Walker Date: Tue, 1 Jul 2025 15:57:16 -0400 Subject: [PATCH 9/9] Add decrypt_secret for reference --- .../unit/test_encryption.py | 51 +------------------ src/unstructured_client/users.py | 49 ++++++++++++++++++ 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/_test_unstructured_client/unit/test_encryption.py b/_test_unstructured_client/unit/test_encryption.py index 95508f6d..3ec5d6dc 100644 --- a/_test_unstructured_client/unit/test_encryption.py +++ b/_test_unstructured_client/unit/test_encryption.py @@ -33,53 +33,6 @@ def rsa_key_pair(): return private_key_pem, public_key_pem - -def decrypt_secret( - private_key_pem: str, - encrypted_value: str, - type: str, - encrypted_aes_key: str, - aes_iv: str, -) -> str: - private_key = serialization.load_pem_private_key( - private_key_pem.encode('utf-8'), - password=None, - backend=default_backend() - ) - - if type == 'rsa': - ciphertext = base64.b64decode(encrypted_value) - plaintext = private_key.decrypt( - ciphertext, - padding.OAEP( - mgf=padding.MGF1(algorithm=hashes.SHA256()), - algorithm=hashes.SHA256(), - label=None - ) - ) - return plaintext.decode('utf-8') - else: - encrypted_aes_key = base64.b64decode(encrypted_aes_key) - iv = base64.b64decode(aes_iv) - ciphertext = base64.b64decode(encrypted_value) - - aes_key = private_key.decrypt( - encrypted_aes_key, - padding.OAEP( - mgf=padding.MGF1(algorithm=hashes.SHA256()), - algorithm=hashes.SHA256(), - label=None - ) - ) - cipher = Cipher( - algorithms.AES(aes_key), - modes.CFB(iv), - ) - decryptor = cipher.decryptor() - plaintext = decryptor.update(ciphertext) + decryptor.finalize() - return plaintext.decode('utf-8') - - def test_encrypt_rsa(rsa_key_pair): private_key_pem, public_key_pem = rsa_key_pair @@ -92,7 +45,7 @@ def test_encrypt_rsa(rsa_key_pair): # A short payload should use direct RSA encryption assert secret_obj["type"] == 'rsa' - decrypted_text = decrypt_secret( + decrypted_text = client.users.decrypt_secret( private_key_pem, secret_obj["encrypted_value"], secret_obj["type"], @@ -114,7 +67,7 @@ def test_encrypt_rsa_aes(rsa_key_pair): # A longer payload uses hybrid RSA-AES encryption assert secret_obj["type"] == 'rsa_aes' - decrypted_text = decrypt_secret( + decrypted_text = client.users.decrypt_secret( private_key_pem, secret_obj["encrypted_value"], secret_obj["type"], diff --git a/src/unstructured_client/users.py b/src/unstructured_client/users.py index 6e6880d4..6ce52bf7 100644 --- a/src/unstructured_client/users.py +++ b/src/unstructured_client/users.py @@ -526,6 +526,55 @@ def _encrypt_rsa( 'aes_iv': "", } + def decrypt_secret( + self, + private_key_pem: str, + encrypted_value: str, + secret_type: str, + encrypted_aes_key: str, + aes_iv: str, + ) -> str: + private_key = serialization.load_pem_private_key( + private_key_pem.encode('utf-8'), + password=None, + backend=default_backend() + ) + + if not isinstance(private_key, rsa.RSAPrivateKey): + raise TypeError("Private key must be a RSA private key for decryption.") + + if secret_type == 'rsa': + ciphertext = base64.b64decode(encrypted_value) + plaintext = private_key.decrypt( + ciphertext, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + return plaintext.decode('utf-8') + + # aes_rsa + encrypted_aes_key_decoded = base64.b64decode(encrypted_aes_key) + iv = base64.b64decode(aes_iv) + ciphertext = base64.b64decode(encrypted_value) + + aes_key = private_key.decrypt( + encrypted_aes_key_decoded, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + cipher = Cipher( + algorithms.AES(aes_key), + modes.CFB(iv), + ) + decryptor = cipher.decryptor() + plaintext = decryptor.update(ciphertext) + decryptor.finalize() + return plaintext.decode('utf-8') def encrypt_secret( self,