Every key, nonce, and IV in cryptography starts with random bytes. If the randomness is weak, nothing built on top of it can be strong. This chapter covers where secrets come from and what can go wrong.
Every cryptographic system we have covered so far, hashes, MACs, and every one we will cover next, depends on secret values that an attacker cannot predict. If an attacker can guess your key, the cipher does not matter.
In Chapter 3, we assumed Alice and Bob already shared a secret key. In the next chapter on symmetric encryption, we will assume the same. But where does that key come from? It cannot be a password someone typed. It cannot be a timestamp. It cannot be a counter. It has to be generated in a way that no attacker can predict or reproduce.
Randomness is the answer. But not all randomness is created equal. A coin flip is random. A shuffled deck is random. Math.random() is random enough for a dice game. None of them are random enough for cryptography.
In 2008, a Debian maintainer accidentally removed two lines of code from OpenSSL. This reduced the entropy pool to roughly 32,000 possible values. For two years, every SSH and TLS key generated on Debian and Ubuntu systems was trivially guessable. An attacker could try all 32,000 keys in seconds.
In 2010, researchers extracted Sony's PlayStation 3 ECDSA master signing key. The cause: Sony used a fixed value where the ECDSA algorithm requires a fresh random nonce for every signature. With two signatures sharing the same nonce, the private key falls out of the math. These are not exotic academic attacks. They are full system compromises caused by bad randomness.
Entropy is the amount of uncertainty in a random source. A fair coin flip has 1 bit of entropy. A six-sided die has about 2.6 bits. A 256-bit cryptographic key needs 256 bits of entropy to be unguessable.
Operating systems collect entropy from hardware events: mouse movements, keyboard timings, disk seek times, interrupt timings, thermal noise from hardware sensors. Each event contributes a small, unpredictable signal.
The OS continuously feeds hardware events into an entropy pool. When you request random bytes, the system draws from this pool. On modern systems the pool is continuously reseeded by a CSPRNG, so running out of entropy is effectively a solved problem.
Raw entropy is messy and biased. A cryptographically secure PRNG takes that entropy as a seed and stretches it into a uniform, unpredictable stream of bytes. This is what crypto.getRandomValues() gives you in the browser.
On modern systems, you almost never need to worry about running out of entropy. The kernel CSPRNG (ChaCha20 on Linux, Fortuna on macOS) continuously reseeds from hardware sources. The important thing is to use the right API, not to add your own "extra randomness" on top.
Generate random bytes using the browser's crypto.getRandomValues() API. Visualize the distribution and compare it with Math.random(), which is fast but not cryptographically secure.
Generate random bytes and see their distribution. A good source produces a flat, uniform histogram.
Generate at least 10,000 bytes to see meaningful distribution differences. A truly uniform source keeps all 256 bars at roughly the same height.
True Random Number Generators (TRNGs) measure physical phenomena. Cryptographically Secure Pseudo-Random Number Generators (CSPRNGs) use deterministic algorithms seeded with true randomness. In practice, CSPRNGs are what you actually use.
TRNGs measure genuinely unpredictable physical events: radioactive decay, thermal noise in resistors, photon arrival times. Intel's RDRAND instruction uses thermal noise from a transistor circuit. These sources are slow and their raw output is biased. They need conditioning (hashing, Von Neumann debiasing) before the bytes are usable.
You rarely interact with TRNGs directly. The operating system collects their output, mixes it into an entropy pool, and feeds it to a CSPRNG that does the heavy lifting.
A CSPRNG starts from a truly random seed and uses a deterministic algorithm (ChaCha20, AES-CTR, or HMAC-DRBG) to produce an arbitrarily long stream of bytes. The output is computationally indistinguishable from true randomness. Even if you observe gigabytes of output, you cannot predict the next byte without knowing the seed.
This is the engine behind every crypto.getRandomValues() call, every os.urandom(), and every /dev/urandom read. Fast (gigabytes per second), uniform, and secure.
Three properties separate a CSPRNG from a regular PRNG. Next-bit unpredictability: given the first k bits of output, no algorithm can predict bit k+1 with probability better than 50%. State compromise recovery: even if an attacker learns the internal state at time T, they cannot recover outputs generated before T once the PRNG reseeds. Backward security: past outputs remain unpredictable even after a state leak.
Math.random() in JavaScript (V8 engine) uses xorshift128+. It fails all three properties. Given just a few outputs, the full internal state can be recovered and every future (and past) value predicted. It was never designed for security.
A linear congruential generator (LCG) is one of the simplest PRNGs. Given a few outputs, you can predict everything that comes next. Try it yourself and see why cryptography demands better.
A weak PRNG leaks its future outputs once you know the algorithm. A CSPRNG does not.
Think you can type random digits? Enter a sequence and see how your choices compare to a truly random source. Humans are predictably bad at being unpredictable.
Type at least 40 random digits (0-9) as fast as you can. Try to be as unpredictable as possible.
Keys, nonces, and IVs all come from randomness. A nonce (number used once) must never repeat with the same key. Reuse one, and XOR-based encryption completely falls apart.
If you XOR two different plaintexts with the same keystream (because the nonce was reused), the keystreams cancel out: C1 XOR C2 = P1 XOR P2. The attacker now has the XOR of two plaintexts, which leaks enormous amounts of information. With frequency analysis and some guessing, both messages can often be recovered completely.
This is not theoretical. WEP (the original Wi-Fi encryption protocol) was broken exactly this way. Its 24-bit IV space was too small, causing inevitable nonce collisions within hours of normal traffic. The result: any WEP network could be cracked in minutes.
Two messages encrypted with the same key. Toggle between unique and reused nonces to see what leaks.
With unique nonces, each message is encrypted with a different keystream. XORing the ciphertexts produces random-looking output that leaks no information about either plaintext.
The rules are simple. Use the right API. Never roll your own. Never add extra steps that reduce entropy.
Use crypto.getRandomValues() in browsers, os.urandom() in Python, crypto/rand in Go, SecureRandom in Java. Never use Math.random(), rand(), or any non-cryptographic PRNG for key material.
For AES-GCM, generate a random 96-bit (12-byte) nonce per encryption. For AES-CBC, generate a random 128-bit (16-byte) IV. Never repeat a (key, nonce) pair.
Web session tokens, CSRF tokens, and password reset tokens must be cryptographically random. At least 128 bits of entropy. If an attacker can predict a token, they can hijack sessions or reset passwords.
Each password gets a unique random salt (16+ bytes). The salt does not need to be secret, but it must be unique per user. This prevents precomputed rainbow tables from working.
The most common mistake is not generating bad randomness. It is using good randomness incorrectly: reusing nonces, truncating random bytes, or XORing together multiple random sources (which can reduce entropy if done wrong). Keep it simple: generate, use, done.