AES wrapper

Python AES wrappers can be used to test both encryption and decryption of all supported modes of operation.

To get a template using the CLI, run:

crypto-condor-cli get-wrapper AES --language Python

To get a practical example, run:

crypto-condor-cli get-wrapper AES --language Python --example 1

Encrypt

To test an implementation of AES encryption, the function must:

  • follow the naming convention;

  • implement the Encrypt protocol.

Naming convention

CC_AES_<mode>_encrypt

Where mode is one of: ECB, CBC, CBCPKCS7, CTR, CFB8, CFB128, GCM, CCM.

This tests all key lengths. A specific one can be indicated:

CC_AES_<mode>_<length>_encrypt

Where length is one of 128, 192, or 256.

Protocol

protocol crypto_condor.primitives.AES.Encrypt

Represents a function that encrypts with AES.

Encryption functions must behave like one of the __call__ functions to be tested with this module. Each correspond to one or more modes of operation. In order:

  • ECB

  • CBC or CTR or CFB8 or CFB128

  • CCM or GCM

Classes that implement this protocol must have the following methods / attributes:

__call__(key: bytes, plaintext: bytes) bytes
__call__(key: bytes, plaintext: bytes, *, iv: bytes) bytes
__call__(
key: bytes,
plaintext: bytes,
*,
iv: bytes | None,
aad: bytes | None,
mac_len: int = 0,
) CiphertextAndTag

Encrypts with AES.

Parameters:
  • key – The symmetric key.

  • plaintext – The input to encrypt.

Keyword Arguments:
  • iv – (All modes except ECB) The IV or nonce.

  • aad – (CCM/GCM) The associated data.

  • mac_len – (CCM/GCM) The length of the authentication tag.

Returns:

(ECB/CBC/CTR/CFB) The resulting ciphertext.

(CCM/GCM) A (ciphertext, tag) tuple.

Decrypt

To test an implementation of AES decryption, the function must:

  • follow the naming convention;

  • implement the Decrypt protocol.

Naming convention

CC_AES_<mode>_decrypt

Where mode is one of: ECB, CBC, CBCPKCS7, CTR, CFB8, CFB128, GCM, CCM.

This tests all key lengths. A specific one can be indicated:

CC_AES_<mode>_<key length>_decrypt

Where length is one of 128, 192, or 256.

Protocol

protocol crypto_condor.primitives.AES.Decrypt

Represents a function that decrypts with AES.

Decryption functions must behave like one of the __call__ functions to be tested with this module. Each correspond to one or more modes of operation. In order:

  • ECB

  • CBC or CTR or CFB8 or CFB128

  • CCM or GCM

Classes that implement this protocol must have the following methods / attributes:

__call__(key: bytes, ciphertext: bytes) bytes
__call__(key: bytes, ciphertext: bytes, *, iv: bytes | None) bytes
__call__(
key: bytes,
ciphertext: bytes,
*,
iv: bytes | None,
aad: bytes | None,
mac: bytes | None,
mac_len: int = 0,
) PlaintextAndBool

Decrypts with AES.

Parameters:
  • key – The symmetric key.

  • ciphertext – The input to decrypt.

Keyword Arguments:
  • iv – (All modes except ECB) The IV or nonce.

  • aad – (CCM/GCM) The associated data.

  • mac – (CCM/GCM) The authentication tag.

  • mac_len – (CCM/GCM) The length of the authentication tag in bytes.

Returns:

(ECB/CBC/CTR/CFB) The resulting plaintext.

(CCM/GCM) If the MAC is valid it returns (plaintext, True). Otherwise the plaintext should not be release so it returns (None, False).

Notes

We decided to return None when the MAC verification fails to differentiate from the case where the message is empty, which is a valid case, and is tested by some test vectors. It serves as a clear sign that even in case we don’t test the verification status “this is not the plaintext you’re looking for”.

Example

"""AES wrapper example with PyCryptodome.

Usage:
    crypto-condor-cli test wrapper AES aes_wrapper_example.py
"""

from Crypto.Cipher import AES


def CC_AES_CBC_encrypt(
    key: bytes,
    pt: bytes,
    *,
    iv: bytes | None = None,
) -> bytes:
    """Encrypts with AES-CBC."""
    return AES.new(key, AES.MODE_CBC, iv=iv).encrypt(pt)


def CC_AES_CBC_decrypt(
    key: bytes,
    ct: bytes,
    *,
    iv: bytes | None = None,
) -> bytes:
    """Decrypts with AES-CBC."""
    return AES.new(key, AES.MODE_CBC, iv=iv).decrypt(ct)


def CC_AES_GCM_256_encrypt(
    key: bytes,
    pt: bytes,
    *,
    iv: bytes | None = None,
    aad: bytes | None = None,
    mac_len: int = 0,
) -> tuple[bytes, bytes]:
    """Encrypts with AES-256-GCM.

    Returns:
        A tuple containing the ciphertext and MAC tag.
    """
    cipher = AES.new(key, AES.MODE_GCM, nonce=iv, mac_len=mac_len)
    if aad is not None:
        cipher.update(aad)
    return cipher.encrypt_and_digest(pt)


def CC_AES_GCM_256_decrypt(
    key: bytes,
    ct: bytes,
    *,
    iv: bytes | None = None,
    aad: bytes | None = None,
    mac: bytes | None = None,
    mac_len: int = 0,
) -> tuple[bytes | None, bool]:
    """Decrypts with AES-256-GCM.

    Returns:
        A tuple containing (bytes, True) if the tag verification succeeds, or (None,
        False) if it fails.
    """
    cipher = AES.new(key, AES.MODE_GCM, nonce=iv, mac_len=mac_len)
    if aad is not None:
        cipher.update(aad)
    if mac is None:
        raise ValueError("GCM requires a MAC tag")
    try:
        pt = cipher.decrypt_and_verify(ct, mac)
        return pt, True
    except ValueError:
        return None, False