Engineer's toolkit
Secure randomness and tokens
Where keys, nonces, salts and session tokens must come from, why Math.random is unsafe, and how many bits a token needs.
Every key, token, nonce, salt and session ID must be hard to guess. "Hard to guess" means it comes from a CSPRNG, a cryptographically secure pseudo-random number generator. The operating system seeds it from hardware randomness and exposes it through getrandom() or /dev/urandom.
Concept
Entropy is how unpredictable a value is, measured in bits. A 256-bit random token needs about 2^256 guesses to find by brute force. The length you pick sets the entropy.
Why Math.random is unsafe
General-purpose generators are built for speed and statistics, not for secrecy. JavaScript's Math.random() (V8 uses xorshift128+), Python's random (Mersenne Twister) and Go's math/rand all keep a small internal state. After watching a handful of outputs, an attacker can recover that state and predict every later value. Password-reset tokens built on Math.random() have been exploited this way.
Use the CSPRNG in your language:
| Language | Use | Not |
|---|---|---|
| Node.js | crypto.randomBytes(32), crypto.randomUUID() | Math.random() |
| Browser | crypto.getRandomValues(new Uint8Array(32)) | Math.random() |
| Python | secrets.token_urlsafe(32) | random.random() |
| Go | crypto/rand.Read(b) | math/rand |
| Java | SecureRandom | java.util.Random |
How many bits
For anything that grants access, such as session IDs, reset tokens and API keys, use at least 128 bits. 256 bits, or 32 bytes, costs nothing extra.
Work the numbers for 128 bits. Say an attacker makes a billion guesses per second against a system with a million live tokens:
2^128 / (10^6 tokens × 10^9 guesses per second) ≈ 3.4 × 10^23 seconds ≈ 10^16 yearsGuessing is hopeless at that size. The weak point is never the length. It's a predictable generator.
Walking through it
import { randomBytes, createHash } from "node:crypto"
const token = randomBytes(32).toString("base64url") // send to the user once
const tokenHash = createHash("sha256").update(token).digest("hex") // store only this
console.log(token.length, tokenHash.length)
// 43 64Store the hash, not the token. A database leak then doesn't leak usable tokens. A fast hash is fine here, unlike for passwords, because a 256-bit random value can't be brute-forced. See password hashing for why passwords are different.
A note on IDs. UUIDv4 holds 122 random bits from a CSPRNG, so it's unguessable in practice. UUIDv7 starts with a timestamp, so it's partly predictable. Use IDs as identifiers, never as secrets, and never use an unguessable ID in place of an authorization check.
Common mistakes
- Using
Math.random(),random.random()ormath/randfor anything that grants access. - Storing the raw token, so a database read gives an attacker working tokens.
- Treating a sequential or time-based ID as a secret.
Check yourself
- Why isn't a 256-bit token made with
Math.random()safe? Answer: the generator's state is small, so an attacker can recover it from a few outputs and predict the rest. - Why store the SHA-256 of a reset token instead of the token? Answer: a database leak then exposes only hashes, and a 256-bit random token can't be brute-forced from its hash.
Key takeaways
- Secrets come from a CSPRNG, such as
crypto.randomBytes, Python'ssecretsor Go'scrypto/rand. - Use at least 128 bits for access tokens. 256 bits costs nothing extra.
- Store a hash of each token, so a leaked table is useless.
- IDs identify things. They don't authorize anything.
Further learning
Next, see password hashing, the case where random salts matter and fast hashes don't work.
Related
Cryptography for engineers
The few building blocks you need for everyday security decisions, which question each one answers, and where teams go wrong. Start here for the map.
Hash functions
A hash turns any input into a fixed-size fingerprint. A secure hash is hard to reverse and hard to collide, and MD5 and SHA-1 no longer meet that bar.
Authentication and authorization
Authentication proves who you are, authorization decides what you can do, and service-to-service calls need their own identity. The map for the rest of this topic.