# -*- coding: utf-8 -*-
"""
The AESCipher class implements the ICipher interface and
provides AES encryption/decryption functionality supporting
multiple cipher modes.
"""
from binascii import hexlify, unhexlify
from typing import Any, Dict, Optional, Tuple
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from .base import ICipher
[docs]
class AESCipher(ICipher):
"""
Cipher that use AES (Advanced Encryption Standard) method with MODE_GCM
This symmetric/reversible key encryption block clipper is equipped
to handle 128-bit blocks, using keys sized at 128, 192, and 256
bits.
This block chipper is especially recognized for protecting data at rest,
and it's widely regarded as the most secure symmetric key encryption cipher
yet invented.
**AES Cipher Modes**
The cipher modes are required for a usual AES implementation. An incorrect
implementation or application of modes may severely compromise the AES
algorithm security. There are multiple chipper modes are available
in AES, Some highly used AES cipher modes as follows:
- ECB mode: Electronic Code Book mode
- CBC mode: Cipher Block Chaining mode
- CFB mode: Cipher Feedback mode
- OFB mode: Output FeedBack mode
- CTR mode: Counter mode
- GCM mode: Galois/Counter mode
**CBC mode: Cipher Block Chaining mode**
In CBC the mode, every encryption of the same plaintext should result
in a different ciphertext. The CBC mode does this with an initialization
vector. The vector has the same size as the block that is encrypted.
**Problems in (CBC mode)**
One of the major problems an error of one plaintext block will affect all
the following blocks. At the same time, Cipher Block Chaining mode(CBC) is
vulnerable to multiple attack types:
- Chosen Plaintext Attack(CPA)
- Chosen Ciphertext Attack(CCA)
- Padding oracle attacks
**AES-GCM instead of AES-CBC**
Both the AES-CBC and AES-GCM are able to secure your valuable data
with a good implementation. but to prevent complex CBC attacks such
as Chosen Plaintext Attack(CPA) and Chosen Ciphertext Attack(CCA)
it is necessary to use Authenticated Encryption. So the best option
is for that is GCM. AES-GCM is written in parallel which means throughput
is significantly higher than AES-CBC by lowering encryption overheads.
**AES-GCM**
In simple terms, Galois Counter Mode (GCM) block clipper is a combination
of Counter mode (CTR) and Authentication it’s faster and more secure with a
better implementation for table-driven field operations. GCM has two
operations, authenticated encryption and authenticated decryption.
The GCM mode will accept pipelined and parallelized implementations
and have minimal computational latency in order to be useful at high
data rates. As a conclusion, we can choose the Galois Counter Mode (GCM)
block clipper mode to achieve excellent security performance for
data at rest.
"""
[docs]
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
self,
key: Optional[bytes] = None,
mode: int = AES.MODE_GCM,
encoding: str = "UTF-8",
block_size: int = 16,
authenticated_modes: Optional[Tuple[int, ...]] = None,
padding_modes: Optional[Tuple[int, ...]] = None,
) -> None:
"""
Initialize the AES cipher with encryption parameters.
:param key:
Encryption key as bytes. If None, a random key is generated
(32 bytes for MODE_SIV, 16 bytes for other modes).
:param mode: AES cipher mode (default: AES.MODE_GCM).
:param encoding: Character encoding for string operations (default: "UTF-8").
:param block_size: Block size for padding operations (default: 16 bytes).
:param authenticated_modes:
Tuple of AES modes that support authenticated encryption.
Default: (MODE_GCM, MODE_EAX, MODE_CCM, MODE_SIV, MODE_OCB).
:param padding_modes:
Tuple of AES modes that require padding.
Default: (MODE_ECB, MODE_CBC).
Note: Stream cipher modes (CFB, OFB, CTR) and authenticated
modes (GCM, EAX, CCM, SIV) do not require padding.
"""
super().__init__(
key=key,
mode=mode,
encoding=encoding,
)
self.block_size = block_size
# Modes that require encrypt_and_digest() / decrypt_and_verify() due to
# PyCryptodome API constraints, separate encrypt() + digest() is not supported.
self._combined_api_modes = (AES.MODE_SIV, AES.MODE_OCB)
self.authenticated_modes = authenticated_modes or (
AES.MODE_GCM,
AES.MODE_EAX,
AES.MODE_CCM,
AES.MODE_SIV,
AES.MODE_OCB,
)
self.padding_modes = padding_modes or (
AES.MODE_ECB,
AES.MODE_CBC,
)
[docs]
def encrypt(self, data: str, *args, **kwargs) -> Dict:
"""
Encrypts the provided string data using AES encryption.
The method converts the input string to bytes, applies padding if required
for the cipher mode, encrypts the data, and generates an authentication tag
for authenticated encryption modes (GCM, EAX, CCM, SIV, OCB).
:param data: The plaintext string to encrypt.
:return:
A dictionary containing hex-encoded encryption components:
- ciphertext (str): The encrypted data.
- tag (str, optional): Authentication tag for authenticated modes.
- nonce (str, optional): Nonce used for encryption (for nonce-based modes).
- iv (str, optional): Initialization vector (for IV-based modes).
"""
data_bytes: bytes = bytes(data, encoding=self.encoding)
cipher: Any = AES.new(self.key, self.mode) # type: ignore[call-overload]
if self.mode in self.padding_modes:
data_bytes = pad(data_bytes, self.block_size)
# SIV and OCB require encrypt_and_digest(); separate
# encrypt() + digest() is not supported...
if self.mode in self._combined_api_modes:
ciphertext, tag = cipher.encrypt_and_digest(data_bytes)
else:
ciphertext = cipher.encrypt(data_bytes)
# Only generate tag for authenticated encryption modes
tag = cipher.digest() if self.mode in self.authenticated_modes else None
nonce = getattr(cipher, "nonce", None)
iv = getattr(cipher, "iv", None)
res = (
("ciphertext", ciphertext),
("tag", tag),
("nonce", nonce),
("iv", iv),
)
return {
key: hexlify(value).decode(encoding=self.encoding)
for key, value in res if value
}
[docs]
def decrypt(self, data: Dict, *args, **kwargs) -> str:
"""
Decrypts the encrypted data and returns the original plaintext string.
The method processes a dictionary containing hex-encoded encryption components,
converts them back to bytes, decrypts the ciphertext using the appropriate
cipher mode, removes padding if necessary, and verifies the authentication
tag for authenticated encryption modes.
:param data:
Dictionary containing hex-encoded encryption components:
- ciphertext (str): The encrypted data to decrypt.
- tag (str, optional): Authentication tag (required for authenticated modes).
- nonce (str, optional): Nonce used during encryption.
- iv (str, optional): Initialization vector used during encryption.
:return: The decrypted plaintext string.
"""
data_bytes: Dict[Any, Any] = {}
for key, value in data.items():
data_bytes[key] = unhexlify(value.encode(encoding=self.encoding))
tag = data_bytes.get("tag")
ciphertext = data_bytes.get("ciphertext")
nonce, iv = data_bytes.get("nonce"), data_bytes.get("iv")
# AES.new() overloads require Literal mode constants; `self.mode` is int so the
# call is inherently dynamic, declare Any so downstream calls type-check cleanly.
cipher: Any
if self.mode == AES.MODE_CTR:
# CTR mode requires nonce as a keyword argument
cipher = AES.new(self.key, self.mode, nonce=nonce) # type: ignore[call-overload]
elif nonce or iv:
cipher = AES.new(self.key, self.mode, nonce or iv) # type: ignore[call-overload]
else:
# ECB, SIV, and any mode without a nonce/iv take no extra args
cipher = AES.new(self.key, self.mode) # type: ignore[call-overload]
# SIV and OCB require decrypt_and_verify(); separate
# decrypt() + verify() is not supported...
if self.mode in self._combined_api_modes:
res: bytes = cipher.decrypt_and_verify(ciphertext, tag)
else:
res = cipher.decrypt(ciphertext)
if self.mode in self.padding_modes:
res = unpad(res, self.block_size)
# Only verify tag for authenticated encryption modes
if self.mode in self.authenticated_modes:
cipher.verify(tag)
return res.decode(encoding=self.encoding)