AEAD modes are the only way you should be encrypting data today, and it’s not because they’re faster.
Let’s see what happens when we encrypt a simple message using AES-GCM, a common AEAD mode.
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import os
# Generate a random 256-bit AES key
key = os.urandom(32)
# Generate a random 96-bit nonce (GCM requires 96 bits)
nonce = os.urandom(12)
# The plaintext message we want to encrypt
plaintext = b"This is a secret message."
# Associated data (optional, but useful for integrity)
# This data is authenticated but not encrypted.
associated_data = b"recipient: alice"
# Initialize the AES-GCM cipher
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce, associated_data), backend=default_backend())
encryptor = cipher.encryptor()
# Encrypt the plaintext
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
# The GCM mode also produces an authentication tag
tag = encryptor.tag
print(f"Key: {key.hex()}")
print(f"Nonce: {nonce.hex()}")
print(f"Associated Data: {associated_data.decode()}")
print(f"Plaintext: {plaintext.decode()}")
print(f"Ciphertext: {ciphertext.hex()}")
print(f"Authentication Tag: {tag.hex()}")
# --- Decryption ---
decryptor = Cipher(algorithms.AES(key), modes.GCM(nonce, associated_data), backend=default_backend()).decryptor()
decryptor.authenticate_additional_data(associated_data)
decrypted_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
print(f"Decrypted Plaintext: {decrypted_plaintext.decode()}")
# --- Tampering Example ---
# Let's try to tamper with the ciphertext
tampered_ciphertext = bytearray(ciphertext)
tampered_ciphertext[0] ^= 0xFF # Flip a bit in the ciphertext
tampered_ciphertext = bytes(tampered_ciphertext)
try:
decryptor = Cipher(algorithms.AES(key), modes.GCM(nonce, associated_data), backend=default_backend()).decryptor()
decryptor.authenticate_additional_data(associated_data)
decryptor.update(tampered_ciphertext) + decryptor.finalize()
except Exception as e:
print(f"\nTampering failed as expected: {e}")
# Let's try to tamper with the associated data
try:
tampered_associated_data = b"recipient: bob"
decryptor = Cipher(algorithms.AES(key), modes.GCM(nonce, associated_data), backend=default_backend()).decryptor()
decryptor.authenticate_additional_data(tampered_associated_data) # Using tampered AD
decryptor.update(ciphertext) + decryptor.finalize()
except Exception as e:
print(f"Tampering with associated data failed as expected: {e}")
AEAD stands for Authenticated Encryption with Associated Data. The name tells you exactly what it does: it encrypts your data and, crucially, authenticates it all in a single, integrated operation. This isn’t just about confidentiality; it’s about ensuring that the data you receive is exactly what was sent, and hasn’t been tampered with in transit. The "associated data" part is key: it’s data that you want to protect the integrity of, but not necessarily its confidentiality. Think of message headers, timestamps, or sender/recipient information. This data is authenticated but remains in the clear.
The magic happens because the encryption and authentication processes are intertwined. Unlike older methods where you might encrypt first and then generate a separate Message Authentication Code (MAC), AEAD modes perform both concurrently. This tight coupling prevents a whole class of attacks that could exploit the separation. For example, an attacker might try to flip bits in the ciphertext in a way that appears valid to a separate MAC check, but results in a modified plaintext. AEAD modes, by their design, make this practically impossible. The authentication tag generated by an AEAD cipher is intrinsically linked to both the plaintext and the associated data.
The core components of an AEAD system are a symmetric encryption algorithm (like AES), a mode of operation (like GCM or ChaCha20-Poly1305), a secret key, and a nonce (a number used only once). The nonce is critical for security. If you reuse a nonce with the same key, you compromise the confidentiality and authenticity of all messages encrypted with that key-nonce pair. For GCM, a 96-bit nonce is standard and highly recommended. For Poly1305, 128 bits is common. The key is typically 128, 192, or 256 bits.
AEAD modes like AES-GCM and ChaCha20-Poly1305 work by using the underlying block cipher or stream cipher to produce a pseudorandom stream. This stream is then XORed with the plaintext to produce the ciphertext. Simultaneously, the algorithm accumulates information from both the ciphertext and the associated data into an internal state. This state is then used, often with a polynomial hash function (like in GCM) or a universal hash function (like in Poly1305), to generate the authentication tag. During decryption, the process is reversed. The ciphertext is decrypted, and the authentication tag is recomputed based on the decrypted plaintext and the associated data. If the recomputed tag matches the received tag, the data is considered authentic and unaltered.
The "associated data" is not encrypted, but its integrity is guaranteed. This is incredibly useful. Imagine you’re sending an email. The recipient’s address and the subject line are important to authenticate – you don’t want an attacker to change "Meeting tomorrow" to "Meeting yesterday" – but you probably don’t need to encrypt them. AEAD allows you to authenticate these fields without adding the overhead of encrypting them. The GCM mode, for instance, takes this associated data as an argument during initialization.
A subtle but crucial point often overlooked is how the authentication tag is generated. In GCM, for example, it’s derived from a Galois field multiplication. If you tamper with any part of the input – the ciphertext, the associated data, or even the key/nonce (though that’s a different attack vector) – the final tag computation will result in a different value. The decryption process verifies this tag. If it doesn’t match, the decryption fails, and the library will typically raise an authentication error, preventing you from using potentially corrupted or malicious data.
The most common pitfall with AEAD is nonce reuse. When a nonce is reused with the same key, the security guarantees collapse. For GCM, if a nonce is reused, an attacker can potentially recover the authentication key used internally by GCM, allowing them to forge messages. Always ensure your nonce generation strategy guarantees uniqueness for each key. A simple counter that is reset for each new key is a common and effective approach.
The next step beyond basic AEAD is often understanding how to securely manage your keys, especially in distributed systems, and exploring authenticated encryption modes that don’t rely on a single nonce, like those used in TLS 1.3.