AES Encryption
AES (Advanced Encryption Standard) is the workhorse symmetric cipher: 128-bit blocks, keys of 128/192/256 bits, and no published practical attack after two decades of scrutiny. On its own a block cipher only encrypts 16 bytes at a time, so real messages need a mode of operation - and the modern default is the authenticated mode AES-GCM, which you can reach directly in the cryptography library.
AES-GCM, the Default
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=256) # 32 random bytes
cipher = AESGCM(key)
nonce = os.urandom(12) # 96-bit nonce, unique per key
aad = b"order:1234" # optional authenticated data
ciphertext = cipher.encrypt(nonce, b"the invoice total is 42.00", aad)
print(ciphertext) # ciphertext || 16-byte tag
plaintext = cipher.decrypt(nonce, ciphertext, aad)
print(plaintext) # b"the invoice total is 42.00"
Three rules make GCM safe: the key is 128-256 random bits from a secure source; the 12-byte nonce is never reused with the same key (generate it fresh per message, or derive it from a counter you store); and any tampering with ciphertext or AAD fails the built-in tag check. Store the nonce next to the ciphertext - it is not secret.
Encrypting Larger Data
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
key = os.urandom(32)
iv = os.urandom(16)
# CBC + HMAC is the classic "encrypt-then-MAC" recipe for streaming data
encryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor()
ciphertext = encryptor.update(chunk) + encryptor.finalize()
# GCM is preferred when available; for very large files,
# chunk with per-chunk nonces or use an authenticated stream cipher
For files, the practical pattern is: generate a random file key, encrypt the bytes in chunks with AES-GCM or ChaCha20-Poly1305 over per-chunk nonces, and wrap the file key (see the hybrid pattern from the symmetric/asymmetric chapter). Chunking keeps memory flat and still authenticates the whole stream.
Key Sizes
| Key | Security level | Notes |
|---|---|---|
| 128-bit | ~128 bits (post-quantum ~64 for some attacks) | plenty for most data; smaller ciphertext tag |
| 256-bit | ~128+ bits, future-proof | default choice for long-lived data |
Avoid in New Code
- ECB - identical blocks encrypt to identical ciphertext; patterns in the data show through (the famous "ECB penguin").
- AES-CTR without a MAC - malleable ciphertext; add GCM or an HMAC.
- Hand-rolled padding or IV derivation - let the library manage it.
Summary
Use AES-256-GCM with a fresh 12-byte nonce per message as the default for encrypting data in Python. Store nonce and AAD with the ciphertext, keep the key in a secret store, and reach for chunked AEAD or a wrapped file key when the payload is a file rather than a field.