Symmetric Encryption with Fernet
Fernet is the "batteries included" symmetric encryption recipe from the cryptography library: AES-128-CBC for confidentiality, HMAC-SHA256 for authenticity, a random 128-bit IV per message, a timestamp, and URL-safe base64 packaging - all in two method calls. It is the right default for encrypting small to medium values (tokens, config entries, JSON documents) without thinking about IVs, padding, or MAC lengths.
Encrypt and Decrypt
from cryptography.fernet import Fernet, InvalidToken
key = Fernet.generate_key() # 32 bytes, url-safe base64
f = Fernet(key)
token = f.encrypt(b"top secret payload")
print(token)
# gAAAAABl...= (self-contained: version, timestamp, ciphertext, HMAC)
print(f.decrypt(token)) # b"top secret payload"
The token is opaque and self-verifying: decrypting a token that was altered by even one byte raises InvalidToken instead of returning garbage. The only secret you must protect is the 32-byte key - store it outside the application (env var, secret manager, key file with tight permissions).
Timestamps and TTL
Fernet embeds the creation time and can reject old tokens, which is perfect for session tokens and API grants:
token = f.encrypt(b"session-grant")
# decrypt, but fail if the token is older than 1 hour
try:
f.decrypt(token, ttl=3600)
except InvalidToken:
print("expired or invalid")
# ignore the timestamp entirely when you need stateless tokens
f.decrypt(token, ttl=None)
Multiple Keys (Rotation)
A MultiFernet tries keys in order for decryption and uses the first key for encryption - the standard shape for key rotation: prepend the new key, and old tokens stay readable until they expire:
from cryptography.fernet import MultiFernet
old = Fernet.generate_key()
new = Fernet.generate_key()
mf = MultiFernet([Fernet(new), Fernet(old)]) # new key encrypts; both decrypt
token = mf.encrypt(b"data") # encrypted with `new`
print(mf.decrypt(token))
Know the Limits
- Fernet tokens grow by roughly 50% over the plaintext and are meant for small-to-medium values; for large files use AES directly (next chapters) or Fernet over chunks.
- The key is a single random value - for keys derived from a passphrase, run it through a KDF first.
- There is no built-in per-record association tag in the base Fernet; if you need to bind a token to its context (user ID, table row), include that context in the plaintext before encrypting, or move to AES-GCM with an AAD.
Summary
Use Fernet when you want correct authenticated encryption in two lines: generate a key, encrypt, decrypt. Add ttl for expiring tokens, MultiFernet for rotation, and drop down to raw AES only when you outgrow its size and binding limits.