Math.random() vs crypto.getRandomValues() — How to Calculate Password Entropy
There are two ways to generate a "random string" in a browser: Math.random() and crypto.getRandomValues(). Both look like they produce the same kind of unpredictable output, but for anything security-sensitive — passwords, API keys — the difference between them matters a great deal.
1. The fundamental difference between the two APIs
Math.random() uses the JS engine's internal pseudorandom number generator (PRNG). It's fast, and it's perfectly fine for game shuffles, UI animation, or random sampling. But this PRNG was never designed to be unpredictable — several security studies have shown that if you know (or can reconstruct) its internal state, or you observe enough of its output, you can in theory work backward to predict future values. That's why official documentation, including MDN, explicitly warns against using Math.random() anywhere security matters.
crypto.getRandomValues(), on the other hand, is part of the Web Crypto API and draws from the operating system's cryptographically secure pseudorandom number generator (CSPRNG). Because it's seeded from real hardware entropy sources (mouse movement, timing jitter, dedicated random-number hardware, and the like), it's designed so that no amount of observed output lets you predict the next value. This is the one you must use when generating tokens, passwords, or encryption keys.
crypto.getRandomValues(arr). It's built the correct (secure) way rather than with Math.random(), so strings it generates are safe to use as passwords or API keys.
2. What entropy is, and how to calculate it
A password's "strength" is commonly expressed as a value called entropy. The formula is simple.
For example, with uppercase + lowercase letters + digits (62 characters: 26 a-z + 26 A-Z + 10 0-9), each character carries log₂(62) ≈ 5.95 bits of entropy. To hit the widely used practical security benchmark of "128-bit entropy" (considered comfortably resistant to brute-force attacks for the foreseeable future), you'd need:
3. Charset size changes how many characters you need
| Charset | Size | Entropy per character | Length needed for 128 bits |
|---|---|---|---|
| Lowercase only (a-z) | 26 | ≈4.7 bits | ≈28 chars |
| Upper + lower + digits | 62 | ≈5.95 bits | ≈22 chars |
| Upper + lower + digits + symbols | ≈94 | ≈6.55 bits | ≈20 chars |
Adding special characters enlarges the charset, which raises the entropy per character, so you can hit the same security level with a shorter string. Conversely, sticking to lowercase-only means you need a much longer string to reach the same 128 bits.
4. Practical guidance
- Passwords: at least 16 characters, ideally combining all four types (uppercase, lowercase, digits, symbols).
- API keys / tokens: 32+ characters is common in practice; 22 characters is the theoretical minimum for 128-bit entropy.
- If you're building your own random generator: always use
crypto.getRandomValues(), and keep Math.random() strictly for games and UI purposes.
Frequently Asked Questions
Q. Has Math.random() actually been broken in practice?
A. Yes. Multiple security studies have reconstructed the internal state of specific JS engines' Math.random() from enough observed output samples and predicted subsequent outputs. The real-world difficulty of such an attack varies by situation, but since the API was never designed to guarantee unpredictability in the first place, it isn't recommended for security purposes.
Q. Does crypto.getRandomValues() work in every browser?
A. Yes — the Web Crypto API is a standard supported by every modern browser (Chrome, Firefox, Safari, Edge). It's available over HTTPS (or on localhost).
Q. Are there cases where you'd want more than 128 bits of entropy?
A. Long-lived master keys or highly sensitive encryption keys sometimes call for 256 bits. But for ordinary passwords, session tokens, and API keys, 128 bits is widely accepted as a practically sufficient standard.
Q. What's the actual entropy of the 32-character strings this tool generates?
A. With the 62-character upper+lower+digit set at 32 characters, that's 32 × 5.95 ≈ 190 bits — comfortably above the 128-bit benchmark.