Why Math.random() Can't Make Passwords: CSPRNGs Explained

Two functions in your browser both hand back “random” numbers, and only one of them is safe to build a password on. Math.random() returns a float that looks scrambled to a human. crypto.getRandomValues() returns bytes that no observer can predict, even after watching thousands of earlier outputs. The gap between those two guarantees is the whole subject of this guide, and getting it wrong has shipped guessable session tokens, reset links and vault keys into production.

What “random” has to mean for a secret

Randomness for a card-shuffle animation and randomness for a password are different requirements wearing the same word. An animation needs values that look varied. A secret needs values that stay unpredictable to an attacker who has already seen related outputs. That second property has a name: cryptographic security.

A pseudo-random number generator (PRNG) is a deterministic function. It holds an internal state, applies a fixed transformation, and emits a number derived from that state. Feed it the same starting state and it replays the identical sequence forever. A PRNG is judged on speed and statistical evenness — does the output spread flatly across the range, pass the usual distribution tests, avoid obvious cycles.

A cryptographically secure PRNG (CSPRNG) adds a harder promise on top: given any run of outputs, an attacker still cannot compute the next output or recover the state with less work than brute force. That promise is what a secret leans on, and an ordinary PRNG was never designed to keep it.

Why Math.random() fails the test

Every major JavaScript engine backs Math.random() with a variant of xorshift128+. Its state is two 64-bit integers. Each call shifts and XORs them together, updates the pair, and returns a double built from the top bits. The algorithm is excellent at what it is for: it is fast, has a long period, and passes standard randomness test suites.

It is also completely reversible. The output leaks the state bits rather than hiding them, so an attacker who collects a modest run of consecutive Math.random() values can solve for the internal 128-bit state — this has been demonstrated with off-the-shelf constraint solvers that recover the state from a handful of outputs. Once the state is known, every past and future value in that sequence falls out by running the same arithmetic forward. There is no secret key involved and nothing to break; the design never promised unpredictability in the first place. That is why a token minted from Math.random() can be replayed by anyone who watched enough of its siblings.

Where the browser gets real entropy

crypto.getRandomValues() does not run in JavaScript arithmetic at all. The browser asks the operating system’s CSPRNG — getrandom() on Linux, BCryptGenRandom on Windows, the arc4random family on macOS. That pool is seeded from hardware and system noise: interrupt timings, the CPU’s RDRAND instruction, device jitter. It is periodically reseeded, and its whole job is to resist state recovery. This is the same source OpenSSL and your password manager draw on, which is why generating a key in a tab is not the weak link people assume it is.

The trap that catches careful code: modulo bias

Having good bytes is necessary but not sufficient. The moment you map random bytes onto a character set, a subtle flaw can creep back in.

Say you want to pick one of 6 symbols and you take a random byte, which is a value from 0 to 255, then reduce it with byte % 6. A byte has 256 possible values. Divide 256 by 6: you get 42 remainder 4. So four of the six results land in 43 of the 256 bytes, and the other two land in only 42.

ResultBytes mapping to itProbability
04316.80%
14316.80%
24316.80%
34316.80%
44216.41%
54216.41%

The first four outcomes are about 2.4% likelier than the last two. On a 6-symbol toy that is small, but scale it to a real alphabet and the skew becomes structural knowledge an attacker can exploit. A password generator drawing from the 94 printable ASCII characters has the same problem: 256 ÷ 94 is 2 remainder 68, so the first 68 characters of the pool appear with probability 3/256 while the rest appear with 2/256 — the low-index characters are 50% more common. That is real entropy quietly removed from every character.

The fix: rejection sampling

The cure is to throw away the bytes that cause the imbalance. Find the largest multiple of your range that fits under 256, and reject any byte at or above it. For the 6-symbol case, the largest multiple of 6 below 256 is 252. So:

function pick(n) {                    // n = size of the character set
  const limit = 256 - (256 % n);      // 252 when n = 6
  const buf = new Uint8Array(1);
  let b;
  do {
    crypto.getRandomValues(buf);
    b = buf[0];
  } while (b >= limit);               // discard 252–255, ~1.6% of draws
  return b % n;                       // now perfectly uniform over 0..n-1
}

Bytes 252 through 255 are discarded and redrawn. You waste roughly 1.6% of draws, and in exchange every one of the six results is exactly equally likely. No character is cheaper to guess than any other.

How these generators apply it

Every generator on this site uses crypto.getRandomValues() and rejection sampling, never Math.random(). The password generator draws each character uniformly from the pool you select, so the entropy it reports — for a 16-character mixed-charset password, about 104 bits, or log₂(94¹⁶) — is honest rather than optimistic. The encryption key generator requests raw bytes straight from the OS pool and only encodes them as hex or Base64, so no reduction step exists to bias. The UUID generator fills a v4 UUID’s 122 random bits the same way, which is what makes such an identifier safe to use as an unguessable reset token.

What to do next

Three rules carry almost all of this:

Randomness is only half of a strong secret; the other half is having enough of it. If you want to see how bit counts translate into real crack times, read how password entropy actually works, and if you would rather never handle these strings by hand, how password managers work covers the tools that generate and store them for you.