Learning Goals
3 minBy the end of this lesson you can:
- Encrypt and decrypt data with
Fernet(symmetric, authenticated). - Generate, store, and protect a symmetric key safely.
- Derive a key from a password with a KDF (so users can "unlock" with a passphrase).
- Explain what Fernet guarantees — and the key-distribution problem it doesn't solve.
Warm-Up · One Key, Two Directions
5 minHASHING data ──► fingerprint (one-way, can't reverse) ENCRYPTION data ──► ciphertext ──► data (two-way, with a key) SYMMETRIC same key encrypts AND decrypts ← today (Fernet) ASYMMETRIC public key encrypts, private decrypts ← Lesson 16
Symmetric encryption uses one secret key for both directions — fast and simple, ideal for "encrypt my files / my config / my backup." The danger is doing it by hand: choosing a mode, an IV, padding, and a MAC, any of which is easy to get catastrophically wrong. Fernet is a safe, opinionated recipe that bundles all of that correctly — you just encrypt and decrypt.
New Concept · Fernet, Keys & KDFs
14 minEncrypt & decrypt
from cryptography.fernet import Fernet # pip install cryptography key = Fernet.generate_key() # 32 random bytes, url-safe base64 encoded f = Fernet(key) token = f.encrypt(b"secret message") # bytes in → ciphertext token out print(token) # b'gAAAAAB...' (includes timestamp + MAC) plain = f.decrypt(token) # ciphertext → original bytes print(plain.decode()) # "secret message"
Fernet works on bytes. The token isn't just ciphertext — it bundles a version, a timestamp, the IV, the ciphertext, and an authentication tag (HMAC), all base64-encoded into one safe blob.
What Fernet guarantees: authenticated encryption
Fernet is authenticated encryption: it doesn't just hide the data, it detects tampering. If even one byte of the token is altered, decrypt raises InvalidToken rather than returning garbage. This protects integrity (Lesson 1) automatically — a property naive encryption lacks, and a reason never to hand-roll it.
from cryptography.fernet import InvalidToken tampered = token[:-1] + (b"X" if token[-1:] != b"X" else b"Y") try: f.decrypt(tampered) except InvalidToken: print("tampering detected — refused to decrypt")
Key handling — the hard part
Encryption only moves the secret: now you must protect the key. If the key leaks, the encryption is worthless.
- Never hard-code the key or commit it (Lesson 8 / Lesson 44).
- Store it in an environment variable, a secrets manager, or a key file with tight permissions.
- Lose the key → the data is gone forever (that's the point — but back the key up securely).
Deriving a key from a password (KDF)
Often you want a person to unlock data with a passphrase. You can't use the password directly as a key (too short, low entropy). Use a Key Derivation Function — the same slow + salted idea as password hashing:
import base64, os from cryptography.fernet import Fernet from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives import hashes def key_from_password(password: str, salt: bytes) -> bytes: kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=480_000) # slow on purpose return base64.urlsafe_b64encode(kdf.derive(password.encode())) salt = os.urandom(16) # store the salt alongside the data key = key_from_password("my passphrase", salt) f = Fernet(key) # same passphrase + same salt → same key → can decrypt later
This is the engine behind the Encrypted Message Vault project (Lesson 18): the file stores the salt and the ciphertext; the right passphrase regenerates the key to unlock it.
Rotation with MultiFernet
from cryptography.fernet import MultiFernet # rotate keys: decrypt with old OR new, but always encrypt with the newest mf = MultiFernet([Fernet(new_key), Fernet(old_key)]) mf.encrypt(b"data") # uses new_key mf.decrypt(old_token) # still reads old_key tokens
MultiFernet supports key rotation (Lesson 44) — you can introduce a new key without breaking data encrypted under the old one.
Worked Example · Encrypt a Config File
12 minGoal: encrypt a sensitive file (say an API-key config) at rest with a password-derived key, and decrypt it back — proving tampering is detected. Key never stored; passphrase from the environment.
import base64, os, json from pathlib import Path from cryptography.fernet import Fernet, InvalidToken from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives import hashes def _key(password: str, salt: bytes) -> bytes: kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=480_000) return base64.urlsafe_b64encode(kdf.derive(password.encode())) def encrypt_file(src: str, dst: str, password: str) -> None: salt = os.urandom(16) f = Fernet(_key(password, salt)) data = Path(src).read_bytes() token = f.encrypt(data) # store salt + ciphertext together (salt is not secret) Path(dst).write_bytes(salt + token) print(f"encrypted {src} → {dst} ({len(token)} bytes ciphertext)") def decrypt_file(src: str, dst: str, password: str) -> bool: blob = Path(src).read_bytes() salt, token = blob[:16], blob[16:] f = Fernet(_key(password, salt)) try: Path(dst).write_bytes(f.decrypt(token)) print(f"decrypted {src} → {dst}") return True except InvalidToken: print("WRONG PASSWORD or file tampered — refused to decrypt") return False # passphrase comes from the environment, never hard-coded: pw = os.environ.get("VAULT_PASSPHRASE", "demo-passphrase") encrypt_file("secrets.json", "secrets.enc", pw) decrypt_file("secrets.enc", "secrets.out.json", pw) # works decrypt_file("secrets.enc", "x", "wrong-passphrase") # refused
encrypted secrets.json → secrets.enc (178 bytes ciphertext) decrypted secrets.enc → secrets.out.json WRONG PASSWORD or file tampered — refused to decrypt
Read the code
The passphrase is turned into a strong key by a slow, salted KDF; the salt is stored with the ciphertext (it isn't secret), so the right passphrase regenerates the key to decrypt. Because Fernet is authenticated, a wrong passphrase or any tampering yields InvalidToken — never silently-wrong data. The passphrase comes from the environment, and the key itself is never written to disk. This is exactly the Encrypted Message Vault (Lesson 18) in miniature, and the pattern for encrypting any data at rest.
Try It Yourself
13 minGenerate a Fernet key, encrypt a message, and decrypt it back. Print the token and confirm it changes each time you encrypt the same message (Fernet includes a random IV).
Encrypt a message, flip one byte of the token, and confirm decrypt raises InvalidToken. Explain why this proves Fernet protects integrity, not just confidentiality.
Hint
from cryptography.fernet import Fernet, InvalidToken f = Fernet(Fernet.generate_key()) t = bytearray(f.encrypt(b"hello")) t[20] ^= 1 # flip a bit try: f.decrypt(bytes(t)) except InvalidToken: print("integrity check caught it ✓")
Build save_note(text, passphrase) and read_note(passphrase) that store an encrypted note to disk (salt + token) and only decrypt with the correct passphrase. Confirm a wrong passphrase fails cleanly.
Hint
Reuse the _key + encrypt_file/decrypt_file pattern from the worked example, but encrypting a string the user types rather than a file.
Mini-Challenge · The Key-Distribution Problem
8 minSymmetric encryption has a famous weakness: to send Bob an encrypted message, you both need the same key — but how do you get the key to Bob securely without an attacker intercepting it? Write a short analysis with a concrete scenario showing why this is hard, and explain how asymmetric encryption (Lesson 16) solves it. (No code required — this is the conceptual bridge to public-key crypto.)
Show a model answer
The key-distribution problem: Aisha wants to send Ben an encrypted file. With Fernet they need the SAME key. So Aisha has to GET the key to Ben — but any channel she uses (email, chat, SMS) could be intercepted by an eavesdropper, who then has the key and reads everything. She can't encrypt the key itself... with what key? It's a chicken-and-egg problem. This is fine when ONE party holds the key (encrypting your OWN files). It breaks down for COMMUNICATION between parties who've never met. Asymmetric encryption (L8-16) solves it: Ben publishes a PUBLIC key anyone can use to encrypt TO him, but only his PRIVATE key (never shared) can decrypt. Aisha encrypts with Ben's public key over an insecure channel — no shared secret needs to travel. In practice, TLS uses asymmetric crypto just to exchange a symmetric key, then switches to fast symmetric encryption (best of both).
Non-negotiables: a concrete interception scenario, why symmetric breaks for strangers, and how public/private keys fix it.
Recap
3 minSymmetric encryption uses one key to encrypt and decrypt — fast, perfect for protecting data at rest. Fernet is the safe, opinionated tool: generate_key, encrypt, decrypt, all authenticated so tampering raises InvalidToken instead of returning garbage (confidentiality + integrity). Protect the key like the secret it is — env vars or a key file, never hard-coded — and derive a key from a passphrase with a slow, salted KDF when a human must unlock data. Use MultiFernet for rotation. Symmetric's limit is the key-distribution problem, which asymmetric encryption (next lessons) solves.
Vocabulary Card
- symmetric encryption
- One shared key both encrypts and decrypts.
- Fernet
- A safe authenticated-encryption recipe in the cryptography library.
- authenticated encryption
- Encryption that also detects tampering (integrity).
- KDF
- Key Derivation Function — turns a passphrase into a strong key (slow, salted).
Homework
4 minBuild a small filecrypt.py CLI: encrypt <file> and decrypt <file> using a passphrase-derived Fernet key (salt stored with the ciphertext, passphrase from env or prompt). Confirm tampering and wrong passphrases are rejected. Write a note: where the key lives, why you never hard-code it, and what happens (correctly) if the key/passphrase is lost.
Sample · filecrypt notes
Where the key lives: nowhere on disk. The passphrase comes from $VAULT_PASSPHRASE (or a prompt); the KDF re-derives the key in memory each run, using the salt stored in the file header. Why never hard-code: a key in source is a key in git history and in every copy of the repo — one leak and all ciphertext is readable. Same rule as API keys (L8-44). If the passphrase is lost: the data is UNRECOVERABLE — by design. That's the security property, so I keep a secure backup of the passphrase (a password manager), not of the key. Tampering / wrong passphrase → InvalidToken → tool prints "refused to decrypt" and exits non-zero. Never returns garbage.
Non-negotiables: working encrypt/decrypt CLI, salt-with-ciphertext, passphrase from env/prompt, and the "lost key = lost data" understanding.