In the previous module we built the mindset: goals, entropy, Kerckhoffs, and the golden rules. But MediNube's central debt is still open: patient records like Ana Pérez's remain unencrypted on disk. In this module we start paying it off, and we do it where nearly all practical cryptography begins: symmetric encryption, the kind that protects most of the world's data — disks, databases, TLS traffic, messaging. In this lesson you'll meet the two modern protagonists, AES and ChaCha20, understand the difference between block ciphers and stream ciphers, and take your first step with the library chosen in 01-04: pyca/cryptography. We'll close with a first real encryption of bytes at MediNube — and an honest warning: we're still missing one piece (the modes of operation) to do it properly.
Contents
- What symmetric encryption is
- Block cipher vs stream cipher
- AES: the world standard
- ChaCha20: the champion of software
- AES vs ChaCha20: which to choose
- First contact with pyca/cryptography and the
hazmatlayer - MediNube case: first byte encryption with AES (and why it still isn't enough)
What symmetric encryption is
Symmetric encryption is the oldest and most intuitive scheme: the same key is used to encrypt and to decrypt. Whoever has the key can do both; whoever doesn't, can do neither.
Formally: decrypt(K, encrypt(K, message)) == message. Three properties define it:
- A single secret key. All the security is concentrated in it (Kerckhoffs in its purest form: the algorithm is public, the key is not).
- It's very fast. Modern symmetric algorithms encrypt at gigabytes per second. That's why they're the tool for data volumes: entire disks, databases, video streams.
- The key is short and maximally entropic. For our course standard, 256 bits = 32 bytes straight out of the CSPRNG (
os.urandom(32)), as we set in 01-03. It's not a password: it's pure noise.
And the weak point? That both parties need the same key, and getting it to them securely over a hostile network is far from a trivial problem. That problem — key exchange — has elegant solutions we'll see in module 4 (asymmetric cryptography and Diffie-Hellman). In this module we deliberately set it aside: our use case is MediNube encrypting its own data at rest, where the "other end" is ourselves and there's no key to transport.
Block cipher vs stream cipher
Symmetric algorithms split into two families based on how they process data:
- A block cipher transforms fixed-size blocks. AES works with blocks of exactly 16 bytes: you give it 16 bytes, it gives you back 16 encrypted bytes. It's like a stamping press: exact-size pieces, one at a time.
- A stream cipher generates a stream of pseudorandom bytes from the key (the keystream) and combines it with the message via XOR, byte by byte. It can encrypt 1 byte or 1 GB without worrying about sizes: it's like a hose.
| Feature | Block cipher | Stream cipher |
|---|---|---|
| Unit of work | Fixed block (AES: 16 bytes) | Byte by byte (keystream XOR message) |
| Messages of any length? | Not directly: needs a mode of operation (lesson 02-02) and sometimes padding | Yes, naturally |
| Ciphertext length | Multiple of the block size (with padding) | Identical to the message |
| Modern example | AES | ChaCha20 |
| Historical examples | DES, 3DES (obsolete) | RC4 (broken, banned in TLS) |
| Characteristic risk | Misusing the mode of operation | Reusing the nonce (catastrophic, lesson 02-02) |
A nuance we'll see in the next lesson: the boundary is less rigid than it looks, because certain modes of operation (like CTR) turn a block cipher into a stream cipher. Keep the structural idea: AES alone only knows how to encrypt exactly 16 bytes; everything else is contributed by the mode.
AES: the world standard
AES (Advanced Encryption Standard) has been the planet's standard symmetric cipher since 2001, and its history is the best living example of Kerckhoffs's principle we studied in 01-04 — the anti-Mifare, the anti-CSS:
- In 1997, the US NIST launched a public international competition to replace the aging DES (56-bit key, already breakable by brute force).
- 15 candidates were submitted by teams from all over the world, with fully published designs. For three years, the planet's best cryptanalysts — including rival teams — attacked them publicly.
- In 2000, Rijndael, by Belgians Joan Daemen and Vincent Rijmen, won. It was standardized as AES in 2001.
Twenty-five years of worldwide scrutiny later, no practical attack against properly-used AES is known. Compare this with the disaster pattern of the secret algorithms from the previous lesson: this asymmetry in review is exactly what open design buys you.
Technical facts you should know:
- Fixed 128-bit (16-byte) block. Always, regardless of key size. Internally, AES arranges those 16 bytes into a 4×4 matrix and applies several rounds of transformations (byte substitutions, row rotations, column mixing, and combination with subkeys derived from the main key). You don't need to know how to implement them — golden rule #1 — but you do need to understand the consequence: each 16-byte block is transformed completely; changing a single input bit unpredictably changes all 16 output bytes.
- Three key sizes: 128, 192, and 256 bits, with 10, 12, and 14 rounds respectively. All three are secure today (2^128 is already unreachable, as we calculated in 01-03). In this course we use AES-256, the standard we set: the overhead is minimal and it gives extra long-term margin.
- Hardware acceleration: AES-NI. Since ~2010, practically every server and desktop CPU (Intel, AMD, and ARM equivalents) includes native instructions that execute AES rounds directly in silicon. Result: speeds of several GB/s per core and, as a bonus, resistance to the timing attacks we saw in 01-04 (the instruction always takes the same amount of time). It's one of the reasons AES dominates on servers.
ChaCha20: the champion of software
ChaCha20 is the great modern stream cipher. It was designed by Daniel J. Bernstein (djb, one of the most influential figures in cryptography today) in 2008, as an evolution of his Salsa20. Unlike AES, it didn't come out of an agency competition, but from academic scrutiny and industry adoption: today it's an IETF standard used by TLS 1.3, WireGuard, SSH, and Android.
Its distinctive traits:
- 256-bit key, always. There are no smaller variants — it fits directly with our course standard.
- It's a stream cipher: it generates keystream in internal 64-byte blocks using only additions, XOR, and rotations over 32-bit integers — operations any CPU does natively and in constant time. No substitution tables in memory, which is exactly where purely software-implemented ciphers tend to leak information through the cache (side channel, 01-04).
- It shines where there's no AES-NI: low-end phones, embedded/IoT devices, routers, and any software that needs to run fast and safely without depending on hardware. Google pushed it in TLS specifically to speed up Android.
- It needs a nonce (number used once): a public 12-byte (96-bit, in the IETF variant) value that must be different for every encryption with the same key. The nonce is the starting position of the keystream "hose": if you repeat the key+nonce pair, two messages get encrypted with the same keystream, and that's a catastrophe we'll demonstrate with code in the next lesson. For now, lock in the rule: nonce = never repeat with the same key.
AES vs ChaCha20: which to choose
| AES-256 | ChaCha20 | |
|---|---|---|
| Type | Block (16 bytes) | Stream |
| Key | 128/192/256 bits (course: 256) | 256 bits |
| Origin | Public NIST competition (Rijndael, 1998-2001) | D. J. Bernstein (2008), IETF standard |
| Speed with AES-NI | Excellent (GB/s per core) | Good |
| Speed without dedicated hardware | Mediocre, and at risk of side channels if the implementation uses tables | Excellent and constant-time by design |
| Where it shines | Modern servers and desktops, regulatory compliance (FIPS) | Mobile, embedded, pure software, VPNs (WireGuard) |
| Status | World standard, no practical attacks | IETF standard, no practical attacks |
Practical criteria for choosing — and an important preview: in lesson 02-03 you'll see that in the real world neither is used "raw," but in their authenticated constructions (AES-GCM and ChaCha20-Poly1305); still, the choice criterion is the same:
- Modern server with AES-NI (MediNube's case, which runs in the cloud): AES-256. Maximum speed, maximum regulatory compatibility.
- Heterogeneous clients, mobile, embedded, or software that doesn't control its hardware: ChaCha20.
- Do you go badly wrong by choosing "wrong"? No: both are secure. The choice is about performance and ecosystem, not security. What does break security is misusing either one (modes, nonces, integrity) — that's what the next two lessons are about.
For MediNube we therefore set: AES-256 as the algorithm for format v1 promised in 01-04.
First contact with pyca/cryptography and the hazmat layer
In 01-04 we chose pyca/cryptography as the course library. It installs like this:
Quick check that everything is in place:
The library is split into two levels, and understanding that split is understanding its philosophy:
- High-level recipes (
cryptography.fernet): decisions already made by experts — algorithm, mode, integrity, format. Hard to misuse. We'll see them in 02-03. - The
hazmatlayer (cryptography.hazmat): the direct cryptographic primitives. The name is literal — hazardous materials — and it's a warning straight from the authors: here, nothing stops you from combining the pieces in an insecure way. Encrypting withhazmatand getting it right requires knowing exactly what you're doing; that's why this module exists.
Why learn with hazmat instead of sticking to the recipes? Because your work at MediNube demands it: real data formats (the v1 format we'll design, TLS, JWT, third-party encrypted files) are built from these pieces, and reviewing legacy code — your day-to-day in this course — requires recognizing the pieces and their traps. The professional rule is this: use the high-level recipe whenever it fits; drop down to hazmat only when you know what you're doing — which is exactly what this module is going to teach you.
MediNube case: first byte encryption with AES (and why it still isn't enough)
Let's encrypt our first real bytes. To see AES in its purest state — the block stamping press, with nothing wrapped around it — we'll feed it exactly one 16-byte block: a fragment of Ana Pérez's record.
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
# 1. The key: 32 bytes (256 bits) from the CSPRNG, as golden rule #3 demands.
key = os.urandom(32)
# 2. Exactly ONE block: 16 bytes. (Fictional data, like everything at MediNube.)
block = "BLOOD TYPE:0+---".encode("utf-8")
assert len(block) == 16
# 3. Build the AES cipher.
# The API FORCES us to specify a mode; we use ECB, which means
# "apply the block cipher directly, with nothing else."
# ⚠️ This is only acceptable because we're encrypting a SINGLE loose block:
# in the next lesson you'll see why ECB is a disaster for real data.
encryptor = Cipher(algorithms.AES(key), modes.ECB()).encryptor()
ciphertext = encryptor.update(block) + encryptor.finalize()
print(ciphertext.hex()) # 16 random-looking bytes, e.g. '977cd3d431292c9d...'
# 4. Decrypt: same key (symmetric), inverse operation.
decryptor = Cipher(algorithms.AES(key), modes.ECB()).decryptor()
recovered = decryptor.update(ciphertext) + decryptor.finalize()
print(recovered.decode("utf-8")) # BLOOD TYPE:0+---
assert recovered == blockLine-by-line breakdown:
os.urandom(32)— the key is born from the system's CSPRNG (01-03). Never from a hand-typed password, and never fromrandom. (So how do you turn a password into a key when there's no other choice? That's exactly the subject of lesson 02-04.)algorithms.AES(key)— selects AES; the key size (32 bytes) automatically determines the variant (AES-256).modes.ECB()— the "null mode": apply the primitive to each 16-byte block, as is. ThehazmatAPI won't even let you instantiate aCipherwithout declaring a mode: the library is telling you that a block cipher without a mode is not a complete encryption system.encryptor()/update()/finalize()—hazmat's working pattern: you create the operator, feed it data withupdate()(which can be called multiple times, useful for large files), and close withfinalize().- Everything is bytes, never
str— theencode/decodediscipline from 01-02 applies from start to finish. - Notice that the ciphertext is also 16 bytes indistinguishable from noise: without the key, there's nothing to "read" in it.
It works. And yet, this doesn't solve MediNube's debt, for two reasons that define the rest of the module:
- A real record isn't 16 bytes long. How do you encrypt 40 KB with a machine that only processes 16-byte pieces? That's the job of modes of operation, and choosing badly (for example, the ECB we just used) leaks information even if AES itself is perfect. That's the next lesson, 02-02.
- Encrypting doesn't prevent tampering. Nothing in this scheme detects an attacker altering the encrypted bytes. The solution is authenticated encryption (AEAD), lesson 02-03.
As always at MediNube: fictional data, and remember that a real deployment with health data requires security and compliance review (GDPR) by professionals.
Common Mistakes and Tips
- Sticking with this example and encrypting real data with ECB. Today's example is didactic and valid only for a single loose block. In the next lesson you'll see with your own eyes what ECB leaks with real data.
- Generating the key with
randomor "eyeballing" it from a text string. A symmetric key is 32 bytes from the CSPRNG:os.urandom(32)orsecrets.token_bytes(32). A password is not a key (lesson 02-04). - Believing AES-256 "is more secure" than ChaCha20 (or vice versa). Both are secure; the choice is about platform and performance. Insecurity almost always comes from misuse, not from the algorithm.
- Forgetting that the same key decrypts. That's the definition of symmetric: whoever obtains the key reads everything. Where and how to store that key is a serious topic we'll tackle in module 6; in the meantime, never in the source code and never in the repository.
- Confusing the nonce with a secret. ChaCha20's nonce (and the IVs coming in the next lesson) are public; their only obligation is not to repeat with the same key.
Exercises
-
Classify. For each MediNube scenario, reason whether AES-256 or ChaCha20 fits better: (a) encrypting records on MediNube's cloud servers (modern Intel CPUs); (b) a future patient-facing mobile app that encrypts notes locally, including older Android handsets; (c) a medical telemetry IoT device with an ARM microcontroller with no cryptographic acceleration.
-
The stamping press. Modify the lesson's example to try encrypting
"ANA PEREZ MEDICAL NOTE"(22 bytes) with AES in ECB mode. What happens and why? What does the error tell you about what's still missing from what you've learned? -
Round trip with the wrong key. Starting from the lesson's example, decrypt the block with a different key (another
os.urandom(32)). Does it raise an error or return something? What does it return, and what does that tell you about whether the encryption "knows" when the key is correct?
Solutions
-
(a) AES-256: modern servers with AES-NI, maximum performance — it's the choice we set for MediNube's
v1format. (b) ChaCha20: a heterogeneous device fleet where many lack AES acceleration; in pure software ChaCha20 is faster and constant-time by design. (c) ChaCha20: it's the archetypal case — a modest CPU, no cryptographic hardware, and only additions/XOR/rotations that any ARM chip executes well. -
encryptor.finalize()raisesValueError: The length of the provided data is not a multiple of the block length.AES only processes exact 16-byte blocks; 22 bytes don't fit. What's missing is exactly what a complete mode of operation provides: chunking, chaining, and, when needed, padding the last block — precisely the syllabus of lesson 02-02. -
It doesn't raise an error: it returns 16 bytes of pseudorandom garbage (which likely can't even be decoded as UTF-8). Raw block encryption includes no verification whatsoever: decrypting with any key always "works" mechanically and produces something. Detecting that the result is valid — and that no one has tampered with the data — requires authentication, which is exactly what the authenticated encryption in lesson 02-03 adds.
Conclusion
You now have the two protagonists of modern symmetric encryption and their roles: AES-256 (16-byte block cipher, born from a public competition that is Kerckhoffs in action, unbeatable on hardware with AES-NI) and ChaCha20 (Bernstein's stream cipher, 256-bit key, unrepeatable nonce, king of pure software and mobile). You know the choice between them is about platform, not security, and you've written your first real encryption with pyca/cryptography, knowing the boundary between the high-level recipes and the hazmat layer — hazardous materials that this module teaches you to handle. But the final example left two huge loose ends: AES alone, raw, neither encrypts real-sized messages nor detects tampering. We tie up the first loose end right in the next lesson, 02-02: Modes of Operation — where you'll see, with a Python demonstration on a MediNube record, why the ECB mode we used "as a toy" today is a data leak waiting to happen, and what CBC and CTR do right. See you there.
Applied Cryptography Course
Module 1: Cryptography Fundamentals
- What Cryptography Is and What It's For
- Encoding, Obfuscation, and Encryption
- Randomness and Entropy
- Kerckhoffs's Principle and the Golden Rules
Module 2: Symmetric Cryptography
- Symmetric Encryption: AES and ChaCha20
- Modes of Operation
- Authenticated Encryption (AEAD)
- Key Derivation (KDF)
Module 3: Hashes, MACs, and Passwords
Module 4: Asymmetric Cryptography
- Public-Key Fundamentals and RSA
- Elliptic Curve Cryptography
- Digital Signatures
- Key Exchange: Diffie-Hellman
- Hybrid Encryption
