Math.random() looks random and is not safe; crypto.getRandomValues() is drawn from your operating system's entropy and is. This article explains the difference in plain language, the rejection-sampling trick good generators use, and how to sanity-check any tool that claims to be random.
Every password generator, passphrase tool and UUID function lives or dies on one ingredient: the quality of its randomness. If the numbers underneath are predictable, the outputs are predictable, and no amount of length or symbols saves them. Browsers offer two very different wells to draw from. Math.random() is a fast pseudo-random generator designed for games and shuffles — deterministic under the hood, seeded invisibly, and explicitly not safe for security. crypto.getRandomValues() asks the operating system for cryptographically secure random bytes, the same source the browser itself trusts for TLS keys.
This article explains what "cryptographically secure" actually promises, why distribution matters as much as unpredictability (with the modulo-bias trap shown in numbers), and the checks you can apply to any random tool — including ours.
Key takeaways
- Math.random() is predictable in principle: given enough outputs, its internal state can be inferred and future values forecast. Never use it for passwords, tokens or IDs with security meaning.
- crypto.getRandomValues() fills a typed array with bytes from the OS CSPRNG — the correct source for anything on this site.
- Getting random integers in a range needs rejection sampling; the naive % operator biases results toward low values.
- Verifiable honesty: a trustworthy tool names its source, works offline in view-source plain sight, and sends nothing anywhere.
Two kinds of 'random'
A pseudo-random number generator (PRNG) is an algorithm: start from a seed, stir deterministically, emit numbers that pass statistical tests for patternlessness. Math.random() uses a fast PRNG (modern engines use xorshift128+ or similar). Its outputs look random and, for simulations and games, are. But determinism cuts both ways: the sequence is a function of a compact internal state, and researchers have shown that observing a run of outputs lets an attacker reconstruct that state and predict values before and after — including values another part of the page used for something sensitive.
A cryptographically secure PRNG (CSPRNG) makes a stronger promise: even seeing many outputs, predicting the next is computationally infeasible, because the generator continually reseeds itself from physical entropy the operating system gathers — timing jitter, hardware noise, device interrupts. crypto.getRandomValues() exposes exactly that source to web pages, limited to 65,536 bytes per call, which is enough for a lifetime of passwords per click.
Modulo bias: the subtle flaw in 'secure' generators
Suppose you have random bytes (0–255) and want a character from a 94-symbol alphabet. The lazy mapping, byte % 94, is biased: 256 is not a multiple of 94 — 94×2 = 188, leaving 68 symbols reachable from three byte values while 26 symbols get only two. Those 68 symbols appear 50% more often than they should. The skew per character is small, but it is systematic, attackers model it, and across millions of generated passwords it measurably shrinks the real search space below the advertised entropy.
The correct technique is rejection sampling: work out the largest multiple of your pool size that fits in the range (here, 188), accept bytes below it, and discard — reject — bytes from 188 to 255, drawing replacements. Every symbol then has exactly equal probability, and the entropy figure a tool quotes is the entropy it delivers. When a generator's documentation brags about anything, this is the detail worth bragging about; when it is silent, you cannot tell — one more reason open, inspectable client-side code matters.
UUIDs and passphrases ride on the same well
crypto.randomUUID() is the CSPRNG wearing a standardised format: 122 bits from the secure source, arranged with version and variant markers. A passphrase generator does the same draw at word granularity — to choose uniformly from a 7,776-word list it needs unbiased integers up to 7,776, which means rejection sampling over 13-bit draws (8,192 values, of which 416 must be rejected when they land at or above 7,776). Different outputs, identical foundation: if the draw underneath is honest, the entropy maths quoted for each tool follows by simple multiplication; if it is not, nothing downstream can repair it.
Checking a tool you did not write
- Read its claim. A serious tool states its source: "crypto.getRandomValues" or your platform equivalent. Vague words like "military-grade randomness" state nothing.
- Check the network tab. Generation should involve zero requests — randomness shipped from a server would mean the server could know your password, which defeats the purpose.
- Look for viewable source. Client-side tools run code your browser already has. You (or anyone) can read it and confirm the API used and the absence of exfiltration.
- Test statistically, lightly. Generate a large batch and count symbol frequencies: honest rejection sampling gives a flat distribution within noise. You cannot prove security this way, but obvious bias and obvious repetition stand out.
- Never enter a generated password anywhere to 'check' it on the same page that made it unless that page is the tool itself, offline-capable and inspectable.
Mistakes and pitfalls
Shuffling Math.random() output 'really well' to make it secure
Shuffling permutes values; it cannot add unpredictability that was not in the source. A thoroughly shuffled predictable sequence is still a predictable sequence in a fancy order.
Seeding a generator with the current time or the mouse position
Timestamps have very few plausible values (an attacker tries the seconds around when a password was made), and mouse entropy collected by a web page is thin and often patterned. The operating system's pool already aggregates far better sources — use it.
Assuming longer output fixes a weak source
A 100-character password drawn from a compromised generator can have less real entropy than 12 characters from an honest one, because the attacker searches the generator's state space, not the character space.
Common questions
If getRandomValues is just an API, where does the randomness physically come from?
From your operating system's entropy pool, which mixes hardware sources — interrupt timings, device noise, in many chips a dedicated hardware random generator (such as Intel's RDRAND) — and is stirred continuously. The browser asks the OS; the OS is the same authority your banking apps trust.
Is there any legitimate use of Math.random() left?
Plenty: games, visual effects, sampling for simulations, A/B bucketing where prediction harms no one. The rule is about consequences: if someone profiting from predicting the output is in your threat model, Math.random is disqualified.
Can I just call getRandomValues for a whole array of passwords?
Yes — fill a buffer once and consume it with rejection sampling; the API happily fills up to 64 KiB per call. Performance is a non-issue at password scale.
To wrap up
Randomness is the one ingredient in security tools that cannot be inspected in the output — a biased password looks exactly like an honest one. So the discipline is procedural: draw from the operating system's CSPRNG, map to your alphabet without bias, and use tools whose code and silence on the network you can verify. Everything else — length, entropy figures, strength bars — is arithmetic built on that foundation.
Try it yourself
Every generator on this site — passwords, passphrases, UUIDs — draws from crypto.getRandomValues() with rejection sampling, entirely in your browser. Open the network tab while you generate and watch nothing leave.