This series goes from the smallest bricks of cryptography to things you use every day without thinking about them: how passwords are stored, what the padlock in the address bar means, how "Sign in with Google" works, why git push doesn't ask for a password. The real examples come from this very site: viet.sarl hashes passwords with Argon2id, signs sign-in sessions with JWT, signs in with Google through OAuth with PKCE, serves HTTPS through Caddy and Cloudflare, and is deployed over SSH.
This first post lays the foundation: what security protects, the three basic tools and the job of each, why safety lies in the key rather than in hiding the algorithm, and the thing every other tool stands on: random numbers.
Roadmap: the CIA triad, hashing, encryption and signatures, Kerckhoffs's principle, XOR and perfect encryption, random numbers (with an experiment that predicts random), then a map of the series. The experiments run with Python 3.12 on a real machine, a few seconds each.
What security protects
The classic answer has three letters, the CIA triad:
| Means | Example when lost | |
|---|---|---|
| Confidentiality | Only those allowed can read it | Leaked emails, passwords, messages |
| Integrity | Data isn't changed without anyone noticing | A man in the middle swaps the account number receiving a payment |
| Availability | The system keeps serving | The site goes down under a denial-of-service attack |
Two more questions usually come along: authentication (who are you?) and authorization (what may you do?). Cryptography handles most of confidentiality, integrity and authentication. Availability belongs to infrastructure, such as Cloudflare in front of this site.
One habit matters more than any algorithm: before picking a tool, ask who you're defending against. Someone on the same café Wi-Fi, someone with a copy of the database, an employee with admin rights, or a nation state. Each answer needs a different layer. The last post of the series does exactly that for this site.
Three tools: hashing, encryption, signatures
Almost everything in the series is assembled from three tools:
| Tool | Input → output | Reversible? | Needs a key? | Used for |
|---|---|---|---|---|
| Hash function | any data → a fixed-size "fingerprint" (e.g. 32 bytes) | No | No (HMAC does) | Checking integrity, storing passwords, naming content |
| Encryption | plaintext + key → ciphertext | Yes, with the key | Yes | Keeping secrets |
| Digital signature | data + private key → signature; anyone checks it with the public key | Not needed | A key pair | Proving who wrote it, and that it wasn't changed |
Encryption comes in two families:
- Symmetric: the same key locks and unlocks (AES, ChaCha20). Very fast, but both sides must already share the key.
- Asymmetric (public key): a key pair. The public key goes to the whole world, the private key stays put (RSA, elliptic curves). Much slower, but it solves the problem of "how do I share a key with a server I've never met".
In practice the two families always go together: asymmetric to agree on a key and verify identity, then symmetric to encrypt the actual data. TLS, SSH and Signal all do this. Post 5 takes apart exactly how HTTPS puts them together.
A common confusion: encryption doesn't guarantee integrity. A ciphertext can be altered so that it decrypts to a different plaintext without the recipient noticing. That's why modern algorithms always combine encryption with authentication (AEAD).
Kerckhoffs's principle: hide the key, not the algorithm
In 1883, Auguste Kerckhoffs wrote that a cryptographic system must be safe even if the enemy knows everything about it except the key. The reasons are practical:
- The algorithm lives in software, hardware and documentation. Sooner or later it leaks (reverse engineering, departing employees, leaked source). A key can be changed in minutes; an algorithm can't.
- Public algorithms are examined by thousands of experts over years. AES won a public competition that lasted 3 years. A secret algorithm is only examined by the people who wrote it.
The consequence for anyone writing software: don't invent your own cryptographic algorithms, and don't combine cryptographic pieces in new ways either. Most real-world vulnerabilities don't come from AES being broken but from how things are put together: reusing a nonce, comparing signatures with ==, gluing a key to data and hashing it. This series breaks a few such things on purpose to show why. Use vetted libraries (cryptography, pwdlib, pyjwt in Python) with their default parameters.
XOR and perfect encryption
The most basic operation in cryptography is XOR (⊕): two equal bits give 0, different bits give 1. It has a magical property: (p ⊕ k) ⊕ k = p. XOR with a key to encrypt, XOR again with the same key to decrypt.
If the key is truly random, as long as the message, and used only once, you get the one-time pad, the only encryption proven unbreakable, even with infinitely powerful computers. The reason: for a given ciphertext, every plaintext of the same length is equally likely, because there's always a key that turns that ciphertext into that plaintext.
But those three conditions are hard to keep. The key must be as long as the data (encrypting 1 GB takes 1 GB of key, exchanged in advance somehow). And if the key is reused even once:
k = os.urandom(len(p1)) # a truly random key
c1 = xor(p1, k) # "chuyen 500 trieu cho An luc 9h"
c2 = xor(p2, k) # same key, different message
xor(c1, c2) == xor(p1, p2) # True: the key cancels out
xor(xor(c1, c2), p1) # b'hop luc 10h tai phong so 3, ok'
The eavesdropper only has two ciphertexts, but c1 ⊕ c2 equals exactly p1 ⊕ p2: the key disappears. Guess one message (often easy: an email opening, an HTTP header, a fixed form) and you can read the other. Soviet intelligence reused parts of one-time pads in the 1940s, and the American Venona project read thousands of messages thanks to exactly that mistake.
Modern algorithms such as AES-CTR and ChaCha20 are essentially "a one-time pad whose key is generated from a short key": from a 32-byte key and a number used once (a nonce), they generate an arbitrarily long stream of random-looking bytes and XOR it with the data. So they inherit exactly this weakness: reusing a nonce leaks the data.
Random numbers: the forgotten foundation
Keys, salts, nonces, password-reset tokens, session ids, OAuth's state, PKCE's verifier: all of them are random numbers. If an attacker can guess them, every algorithm above them is meaningless, even AES-256.
Python has two sources of random numbers, and they're fundamentally different:
random |
secrets / os.urandom |
|
|---|---|---|
| Algorithm | Mersenne Twister (MT19937) | The operating system's generator (CSPRNG) |
| Designed for | Simulation, games, shuffling | Keys, tokens, passwords |
| Predictable? | Yes, after seeing enough output | No, as far as anyone knows |
Mersenne Twister's internal state is 624 32-bit numbers. Each number it returns is one of those state numbers passed through a "tempering" step, and that step can be reversed. So after seeing 624 consecutive outputs, you can rebuild the whole state and compute every number that follows.
Experiment: a "server" creates a random.Random seeded from os.urandom (unguessable). The attacker sees 624 numbers the server leaks (say, an API that returns random ids), reverses the tempering, builds a clone, and compares the next 10,000 numbers:
victim = random.Random(os.urandom(16)) # unguessable seed
seen = [victim.getrandbits(32) for _ in range(624)] # what the attacker observes
state = [untemper(v) for v in seen] # reverse the tempering (~15 lines)
clone = random.Random()
clone.setstate((3, tuple(state + [624]), None))
predicted next 10000 outputs: 10000
victim token d3b8e33596bf3516f782843061294435
clone token d3b8e33596bf3516f782843061294435 True
10,000 out of 10,000 predicted correctly, along with the 128-bit "password-reset token" the server made afterwards. A strong seed doesn't help: the problem is the algorithm, not the seed. The whole experiment takes under a second.
secrets (a wrapper around os.urandom) takes numbers from the operating system's secure random generator. The kernel gathers entropy from hardware, such as interrupt timing, circuit noise and the CPU's random-number instruction, then mixes it through a cryptographic algorithm. No amount of output lets you derive the next output. This site creates OAuth's state and verifier with it:
def new_state() -> tuple[str, str]:
"""(state, PKCE code verifier)."""
return secrets.token_urlsafe(24), secrets.token_urlsafe(48)
secrets.token_urlsafe(32) gives 32 random bytes, base64-encoded into 43 characters, i.e. 256 bits: guess blindly and the universe runs out of time first. The rule is simple: anything security-related uses secrets, never random. It's also a mistake checkers like Bandit or Ruff (rule S311) catch automatically.
Map of the series
| Post | Content | Underlying tool |
|---|---|---|
| 1 | Three tools, random numbers (this post) | |
| 2 | Hash functions: SHA-256, collisions, HMAC | Hashing |
| 3 | Storing passwords: why not SHA-256, Argon2id | Slow hashing |
| 4 | Public key: RSA, Diffie-Hellman, elliptic curves, post-quantum | Asymmetric |
| 5 | TLS/HTTPS and certificates | All three |
| 6 | Encrypting data at rest: disk, database, single columns | Symmetric + key management |
| 7 | OAuth 2.0, OpenID Connect, PKCE | Hashing, signatures, TLS |
| 8 | SSH | Key exchange, signatures |
| 9 | Putting it together: this site's own security | All of them |
One convention runs throughout: the posts print keys and tokens generated just for the experiments. No real secret, key or server address of this site appears here. That's also the first lesson in operations: secrets never go into git, logs or blog posts.
Summary
- Security protects confidentiality, integrity, availability, plus authentication and authorization. Always ask first: who are you defending against.
- Three tools: hashing (one-way, checks integrity), encryption (two-way, needs a key; symmetric is fast, asymmetric solves key exchange), signatures (prove who wrote it).
- Encryption alone doesn't stop data being altered. Modern algorithms combine encryption with authentication.
- Kerckhoffs: safety lies in the key. Don't invent crypto, don't combine it in new ways.
- The one-time pad is perfect, but reusing the key leaks:
c1 ⊕ c2 = p1 ⊕ p2. Modern stream ciphers inherit exactly this weakness through the nonce. randombecomes fully predictable after 624 numbers (10,000/10,000 correct in the experiment). Anything security-related usessecrets.
The next post goes into the first tool: hash functions. How many output bits change when you flip 1 bit, why MD5 and SHA-1 were retired, how much sooner collisions arrive than you'd think, and an attack that forges a sha256(key + data) "signature" without knowing the key.