Skip to content

AES cipher

AES is a symmetric encryption algorithm that is widely used for securing data. It operates on fixed-size blocks of data and uses a secret key for both encryption and decryption. AES is known for its speed and security, making it a popular choice for various applications.

PKCS7AESCipher

Bases: CipherProtocol

Source code in topsecret/infra/cipher/aes.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
class PKCS7AESCipher(CipherProtocol):
    class MethodType(StrEnum):
        PASSPHRASE = "passphrase"  # noqa: S105
        AUTO = "auto"

    def __init__(self, iterations: int = 100000, algorithm: hashes.HashAlgorithm | None = None):
        self.iterations = iterations
        self.algorithm = algorithm or hashes.SHA256()

    AUTO_KEY_SEP = b"|"

    def encrypt(self, data: bytes, passphrase: str | None = None) -> tuple[Ciphertext, Metadata]:
        """Encrypts the provided data using either a passphrase or an auto-generated key.

        This method supports two modes of encryption:

        1.  **Passphrase-based**: If a `passphrase` is provided, a key is derived
            from it using PBKDF2HMAC. A salt is generated (or used if provided
            internally, though this implementation always generates a new one for
            passphrase encryption via `_get_key`) and prepended to the ciphertext.
            The metadata will indicate `method: "passphrase"`.

        2.  **Auto-generated key**: If no `passphrase` is provided, a new Fernet key
            is generated. This key is then prepended to the ciphertext, separated
            by a `b"|"` delimiter. The metadata will indicate `method: "auto"`.

        The Fernet symmetric encryption algorithm is used for the actual encryption
        of the data.

        Args:
            data: The bytes to be encrypted.
            passphrase: An optional secret phrase. If provided, it's used to
                derive the encryption key. If None, a new key is generated
                and stored with the ciphertext.

        Returns:
            A tuple containing:
                - Ciphertext: The encrypted data.
                  If passphrase-based, this is `salt + encrypted_content`.
                  If auto-key based, this is `key + b"|" + encrypted_content`.
                - Metadata: A dictionary containing information about the
                  encryption process, specifically the `method` used
                  ("passphrase" or "auto").
        """
        key, salt = _get_key(passphrase, algorithm=self.algorithm, iterations=self.iterations)
        cipher = Fernet(key)
        ciphertext = cipher.encrypt(data)

        metadata: Metadata = {}
        if passphrase:
            # For passphrase-based encryption, salt must exist as _get_key generates it.
            encrypted_data = salt + ciphertext if salt else ciphertext
            metadata["method"] = self.MethodType.PASSPHRASE
        else:
            # For auto-generated key encryption, prepend the key itself to the ciphertext.
            # The key is separated from the actual ciphertext by AUTO_KEY_SEP.
            # This allows decryption without needing external key management for this mode.
            encrypted_data = key + self.AUTO_KEY_SEP + ciphertext
            metadata["method"] = self.MethodType.AUTO

        return encrypted_data, metadata

    def decrypt(self, ciphertext: Ciphertext, metadata: Metadata, passphrase: str | None = None) -> str:
        """Decrypts the provided ciphertext using the method specified in metadata.

        This method supports two modes of decryption, corresponding to the encryption
        methods:
        1.  **Passphrase-based**: If `metadata["method"]` is "passphrase", this
            method expects a `passphrase` to be provided. It extracts the salt
            from the beginning of the `ciphertext`, derives the decryption key
            using the passphrase and salt (via `_get_key`), and then decrypts
            the remaining part of the `ciphertext`.
        2.  **Auto-generated key**: If `metadata["method"]` is "auto" (or if no
            method is specified, "auto" is assumed), this method expects the
            decryption key to be prepended to the `ciphertext`, separated by
            `AUTO_KEY_SEP`. It splits the `ciphertext` to retrieve the key and
            the actual encrypted data, then uses this key for decryption.

        The Fernet symmetric decryption algorithm is used for the actual decryption.

        Args:
            ciphertext: The encrypted data.
                If passphrase-based, this is `salt + encrypted_content`.
                If auto-key based, this is `key + b"|" + encrypted_content`.
            metadata: A dictionary containing information about the
                encryption process, crucially the `method` ("passphrase" or "auto").
            passphrase: The secret phrase used for encryption if the method was
                "passphrase". Required for passphrase-based decryption.

        Returns:
            The decrypted data as a UTF-8 string.

        Raises:
            DecryptionError: If decryption fails due to various reasons,
                such as a missing passphrase for passphrase-based decryption,
                an invalid passphrase, an invalid key, corrupted ciphertext,
                or an unknown encryption method.
        """
        method = metadata.get("method", self.MethodType.AUTO)
        match method:
            case self.MethodType.PASSPHRASE:
                if passphrase is None:
                    raise DecryptionError("Passphrase required for decryption.")  # noqa: TRY003
                # The salt is assumed to be the first 16 bytes, as generated by os.urandom(16) in _get_key.
                salt = ciphertext[:16]
                actual_ciphertext = ciphertext[16:]
                key, _ = _get_key(passphrase, iterations=self.iterations, algorithm=self.algorithm, salt=salt)
                cipher = Fernet(key)
                try:
                    decrypted_data = cipher.decrypt(actual_ciphertext)
                except InvalidToken as e:
                    raise DecryptionError("Invalid passphrase or corrupted data.") from e  # noqa: TRY003
                except KeyError as e:
                    raise DecryptionError("Invalid key.") from e  # noqa: TRY003

            case self.MethodType.AUTO:
                # In "auto" mode, the key is prepended to the ciphertext, separated by AUTO_KEY_SEP.
                key_parts = ciphertext.split(self.AUTO_KEY_SEP, 1)
                if len(key_parts) != 2:
                    # This indicates the ciphertext is not in the expected format (key|data).
                    raise DecryptionError(
                        "Invalid ciphertext format for auto mode. Expected key and data separated by '|'."
                    )  # noqa: TRY003
                key, actual_ciphertext = key_parts
                cipher = Fernet(key)
                try:
                    decrypted_data = cipher.decrypt(actual_ciphertext)
                except InvalidToken as e:
                    # This can happen if the embedded key is incorrect or the ciphertext is corrupted.
                    raise DecryptionError(
                        "Failed to decrypt the data with the auto-embedded key. The ciphertext may be corrupted."
                    ) from e  # noqa: TRY003
            case _:
                # If the method specified in metadata is not recognized.
                raise DecryptionError(f"Unknown encryption method: {method}")  # noqa: TRY003

        return decrypted_data.decode("utf-8")

decrypt(ciphertext, metadata, passphrase=None)

Decrypts the provided ciphertext using the method specified in metadata.

This method supports two modes of decryption, corresponding to the encryption methods: 1. Passphrase-based: If metadata["method"] is "passphrase", this method expects a passphrase to be provided. It extracts the salt from the beginning of the ciphertext, derives the decryption key using the passphrase and salt (via _get_key), and then decrypts the remaining part of the ciphertext. 2. Auto-generated key: If metadata["method"] is "auto" (or if no method is specified, "auto" is assumed), this method expects the decryption key to be prepended to the ciphertext, separated by AUTO_KEY_SEP. It splits the ciphertext to retrieve the key and the actual encrypted data, then uses this key for decryption.

The Fernet symmetric decryption algorithm is used for the actual decryption.

Parameters:

Name Type Description Default
ciphertext Ciphertext

The encrypted data. If passphrase-based, this is salt + encrypted_content. If auto-key based, this is key + b"|" + encrypted_content.

required
metadata Metadata

A dictionary containing information about the encryption process, crucially the method ("passphrase" or "auto").

required
passphrase str | None

The secret phrase used for encryption if the method was "passphrase". Required for passphrase-based decryption.

None

Returns:

Type Description
str

The decrypted data as a UTF-8 string.

Raises:

Type Description
DecryptionError

If decryption fails due to various reasons, such as a missing passphrase for passphrase-based decryption, an invalid passphrase, an invalid key, corrupted ciphertext, or an unknown encryption method.

Source code in topsecret/infra/cipher/aes.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def decrypt(self, ciphertext: Ciphertext, metadata: Metadata, passphrase: str | None = None) -> str:
    """Decrypts the provided ciphertext using the method specified in metadata.

    This method supports two modes of decryption, corresponding to the encryption
    methods:
    1.  **Passphrase-based**: If `metadata["method"]` is "passphrase", this
        method expects a `passphrase` to be provided. It extracts the salt
        from the beginning of the `ciphertext`, derives the decryption key
        using the passphrase and salt (via `_get_key`), and then decrypts
        the remaining part of the `ciphertext`.
    2.  **Auto-generated key**: If `metadata["method"]` is "auto" (or if no
        method is specified, "auto" is assumed), this method expects the
        decryption key to be prepended to the `ciphertext`, separated by
        `AUTO_KEY_SEP`. It splits the `ciphertext` to retrieve the key and
        the actual encrypted data, then uses this key for decryption.

    The Fernet symmetric decryption algorithm is used for the actual decryption.

    Args:
        ciphertext: The encrypted data.
            If passphrase-based, this is `salt + encrypted_content`.
            If auto-key based, this is `key + b"|" + encrypted_content`.
        metadata: A dictionary containing information about the
            encryption process, crucially the `method` ("passphrase" or "auto").
        passphrase: The secret phrase used for encryption if the method was
            "passphrase". Required for passphrase-based decryption.

    Returns:
        The decrypted data as a UTF-8 string.

    Raises:
        DecryptionError: If decryption fails due to various reasons,
            such as a missing passphrase for passphrase-based decryption,
            an invalid passphrase, an invalid key, corrupted ciphertext,
            or an unknown encryption method.
    """
    method = metadata.get("method", self.MethodType.AUTO)
    match method:
        case self.MethodType.PASSPHRASE:
            if passphrase is None:
                raise DecryptionError("Passphrase required for decryption.")  # noqa: TRY003
            # The salt is assumed to be the first 16 bytes, as generated by os.urandom(16) in _get_key.
            salt = ciphertext[:16]
            actual_ciphertext = ciphertext[16:]
            key, _ = _get_key(passphrase, iterations=self.iterations, algorithm=self.algorithm, salt=salt)
            cipher = Fernet(key)
            try:
                decrypted_data = cipher.decrypt(actual_ciphertext)
            except InvalidToken as e:
                raise DecryptionError("Invalid passphrase or corrupted data.") from e  # noqa: TRY003
            except KeyError as e:
                raise DecryptionError("Invalid key.") from e  # noqa: TRY003

        case self.MethodType.AUTO:
            # In "auto" mode, the key is prepended to the ciphertext, separated by AUTO_KEY_SEP.
            key_parts = ciphertext.split(self.AUTO_KEY_SEP, 1)
            if len(key_parts) != 2:
                # This indicates the ciphertext is not in the expected format (key|data).
                raise DecryptionError(
                    "Invalid ciphertext format for auto mode. Expected key and data separated by '|'."
                )  # noqa: TRY003
            key, actual_ciphertext = key_parts
            cipher = Fernet(key)
            try:
                decrypted_data = cipher.decrypt(actual_ciphertext)
            except InvalidToken as e:
                # This can happen if the embedded key is incorrect or the ciphertext is corrupted.
                raise DecryptionError(
                    "Failed to decrypt the data with the auto-embedded key. The ciphertext may be corrupted."
                ) from e  # noqa: TRY003
        case _:
            # If the method specified in metadata is not recognized.
            raise DecryptionError(f"Unknown encryption method: {method}")  # noqa: TRY003

    return decrypted_data.decode("utf-8")

encrypt(data, passphrase=None)

Encrypts the provided data using either a passphrase or an auto-generated key.

This method supports two modes of encryption:

  1. Passphrase-based: If a passphrase is provided, a key is derived from it using PBKDF2HMAC. A salt is generated (or used if provided internally, though this implementation always generates a new one for passphrase encryption via _get_key) and prepended to the ciphertext. The metadata will indicate method: "passphrase".

  2. Auto-generated key: If no passphrase is provided, a new Fernet key is generated. This key is then prepended to the ciphertext, separated by a b"|" delimiter. The metadata will indicate method: "auto".

The Fernet symmetric encryption algorithm is used for the actual encryption of the data.

Parameters:

Name Type Description Default
data bytes

The bytes to be encrypted.

required
passphrase str | None

An optional secret phrase. If provided, it's used to derive the encryption key. If None, a new key is generated and stored with the ciphertext.

None

Returns:

Type Description
tuple[Ciphertext, Metadata]

A tuple containing: - Ciphertext: The encrypted data. If passphrase-based, this is salt + encrypted_content. If auto-key based, this is key + b"|" + encrypted_content. - Metadata: A dictionary containing information about the encryption process, specifically the method used ("passphrase" or "auto").

Source code in topsecret/infra/cipher/aes.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def encrypt(self, data: bytes, passphrase: str | None = None) -> tuple[Ciphertext, Metadata]:
    """Encrypts the provided data using either a passphrase or an auto-generated key.

    This method supports two modes of encryption:

    1.  **Passphrase-based**: If a `passphrase` is provided, a key is derived
        from it using PBKDF2HMAC. A salt is generated (or used if provided
        internally, though this implementation always generates a new one for
        passphrase encryption via `_get_key`) and prepended to the ciphertext.
        The metadata will indicate `method: "passphrase"`.

    2.  **Auto-generated key**: If no `passphrase` is provided, a new Fernet key
        is generated. This key is then prepended to the ciphertext, separated
        by a `b"|"` delimiter. The metadata will indicate `method: "auto"`.

    The Fernet symmetric encryption algorithm is used for the actual encryption
    of the data.

    Args:
        data: The bytes to be encrypted.
        passphrase: An optional secret phrase. If provided, it's used to
            derive the encryption key. If None, a new key is generated
            and stored with the ciphertext.

    Returns:
        A tuple containing:
            - Ciphertext: The encrypted data.
              If passphrase-based, this is `salt + encrypted_content`.
              If auto-key based, this is `key + b"|" + encrypted_content`.
            - Metadata: A dictionary containing information about the
              encryption process, specifically the `method` used
              ("passphrase" or "auto").
    """
    key, salt = _get_key(passphrase, algorithm=self.algorithm, iterations=self.iterations)
    cipher = Fernet(key)
    ciphertext = cipher.encrypt(data)

    metadata: Metadata = {}
    if passphrase:
        # For passphrase-based encryption, salt must exist as _get_key generates it.
        encrypted_data = salt + ciphertext if salt else ciphertext
        metadata["method"] = self.MethodType.PASSPHRASE
    else:
        # For auto-generated key encryption, prepend the key itself to the ciphertext.
        # The key is separated from the actual ciphertext by AUTO_KEY_SEP.
        # This allows decryption without needing external key management for this mode.
        encrypted_data = key + self.AUTO_KEY_SEP + ciphertext
        metadata["method"] = self.MethodType.AUTO

    return encrypted_data, metadata

get_hash(data)

Generate a SHA-256 hash of the provided data.

Parameters:

Name Type Description Default
data bytes

The bytes to hash

required

Returns:

Type Description
str

Hexadecimal string representation of the hash

Source code in topsecret/infra/cipher/aes.py
170
171
172
173
174
175
176
177
178
179
180
def get_hash(data: bytes) -> str:
    """
    Generate a SHA-256 hash of the provided data.

    Args:
        data: The bytes to hash

    Returns:
        Hexadecimal string representation of the hash
    """
    return hashlib.sha256(data).hexdigest()