Coverage for confy_addons/encryption/aes.py: 100%
69 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-20 18:19 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-20 18:19 +0000
1"""AES encryption implementation using the cryptography library.
3This module provides an AES encryption handler that implements the
4AESEncryptionABC abstract base class. It supports AES encryption and
5decryption in CFB mode with 256-bit keys.
6"""
8import base64
9import binascii
10import logging
11import secrets
12from typing import Optional
14from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
16from confy_addons.core.abstract import AESEncryptionABC
17from confy_addons.core.constants import AES_IV_SIZE, AES_KEY_SIZE, LOGGER_LEVEL
18from confy_addons.core.exceptions import DecryptionError, EncryptionError
19from confy_addons.core.mixins import EncryptionMixin
21logging.basicConfig(level=LOGGER_LEVEL)
22logger = logging.getLogger(__name__)
25class AESEncryption(EncryptionMixin, AESEncryptionABC):
26 """AES symmetric encryption handler.
28 This class provides AES encryption and decryption operations in CFB mode
29 with 256-bit keys. It can generate a random key or use a provided key.
31 Attributes:
32 key: The AES encryption key (32 bytes).
33 key_size: The size of the AES key in bytes (always 32 for AES-256).
35 """
37 def __init__(self, key: Optional[bytes] = None):
38 """Initialize AESEncryption with a key.
40 Creates an AES encryption handler with either a provided key or a newly
41 generated random key.
43 Args:
44 key: An optional 32-byte AES key. If None, a random key is generated.
46 Raises:
47 ValueError: If the provided key is not 32 bytes long.
48 TypeError: If the provided key is not bytes or bytearray.
50 """
51 self._key_size = AES_KEY_SIZE
53 if key is None:
54 self._key = secrets.token_bytes(self._key_size)
55 else:
56 if not isinstance(key, (bytes, bytearray)):
57 logger.error(f'Invalid key type: {type(key)}')
58 raise TypeError('AES key must be bytes or bytearray')
60 key_bytes = bytes(key)
62 if len(key_bytes) != self._key_size:
63 logger.error(f'Invalid key length: {len(key_bytes)}')
64 raise ValueError(
65 f'AES key must be {self._key_size} bytes long ({self._key_size * 8} bits)'
66 )
67 self._key = key_bytes
68 logger.debug('AES encryption initialized with provided key')
70 def __repr__(self):
71 """Return a string representation of the AESEncryption instance.
73 Returns:
74 str: A detailed string representation including module, class name,
75 key, and memory address.
77 """
78 class_name = type(self).__name__
79 return f"""{self.__module__}.{class_name}(key=<hidden>) object at {hex(id(self))}"""
81 def encrypt(self, plaintext: str) -> str:
82 """Encrypts text using AES in CFB mode.
84 Encrypts the provided plaintext using AES-256 in CFB mode with a
85 randomly generated initialization vector. Returns the result as a
86 base64-encoded string.
88 Args:
89 plaintext: The text string to encrypt.
91 Returns:
92 str: The base64-encoded encrypted data (IV + ciphertext).
94 Raises:
95 EncryptionError: If an error occurs during encryption.
96 TypeError: If the plaintext is not a string.
98 """
99 if not isinstance(plaintext, str):
100 logger.error(f'Invalid plaintext type: {type(plaintext)}')
101 raise TypeError('plaintext must be a str')
103 try:
104 iv = secrets.token_bytes(AES_IV_SIZE)
105 cipher = Cipher(algorithms.AES(self._key), modes.CFB(iv))
106 encryptor = cipher.encryptor()
107 ciphertext = (
108 encryptor.update(plaintext.encode(encoding='utf-8')) + encryptor.finalize()
109 )
110 return base64.b64encode(iv + ciphertext).decode(encoding='ascii')
111 except Exception as e:
112 logger.error(f'Error occurred during encryption: {e}')
113 raise EncryptionError('Error occurred during encryption') from e
115 def decrypt(self, b64_ciphertext: str) -> str:
116 """Decrypts base64-encoded AES encrypted data.
118 Decrypts the provided base64-encoded encrypted data using AES-256
119 in CFB mode. The encrypted data must be in the format produced by
120 the encrypt method (IV + ciphertext).
122 Args:
123 b64_ciphertext: The base64-encoded encrypted data.
125 Returns:
126 str: The decrypted plaintext as a string.
128 Raises:
129 TypeError: If the b64_ciphertext is not a string.
130 ValueError: If the base64 data is invalid or too short.
131 DecryptionError: If an error occurs during decryption.
133 """
134 if not isinstance(b64_ciphertext, str):
135 logger.error(f'Invalid b64_ciphertext type: {type(b64_ciphertext)}')
136 raise TypeError('b64_ciphertext must be a base64-encoded str')
138 try:
139 data = base64.b64decode(b64_ciphertext)
140 except (binascii.Error, ValueError, TypeError) as e:
141 logger.error(f'Error occurred during base64 decoding: {e}')
142 raise ValueError('Invalid base64 encrypted data') from e
144 if len(data) < AES_IV_SIZE:
145 logger.error(f'Invalid encrypted data length: {len(data)}')
146 raise ValueError('Encrypted data is too short to contain an IV and ciphertext')
148 iv, ciphertext = data[:AES_IV_SIZE], data[AES_IV_SIZE:]
150 try:
151 cipher = Cipher(algorithms.AES(self._key), modes.CFB(iv))
152 decryptor = cipher.decryptor()
153 plaintext_bytes = decryptor.update(ciphertext) + decryptor.finalize()
154 return plaintext_bytes.decode('utf-8')
155 except Exception as e:
156 logger.error(f'Error occurred during decryption: {e}')
157 raise DecryptionError('Decryption failed') from e
159 @property
160 def key(self) -> bytes:
161 """Returns the AES encryption key.
163 Returns:
164 bytes: The 32-byte AES key.
166 """
167 return self._key
169 @property
170 def key_size(self) -> int:
171 """Returns the size of the AES key in bytes.
173 Returns:
174 int: The key size in bytes (always 32 for AES-256).
176 """
177 return self._key_size