Math.random vs Entropy Sources: Minting Secure JS Tokens
Why Does Math.random() Keep Failing My Security Audit?
You're three Red Bulls deep into building a JavaScript password generator. The UI is slick. The entropy meter looks impressive. Then your security auditor drops the hammer: "Replace Math.random() with a proper entropy source or we ship nothing." Brutal. But fair.
Here's the short answer: Math.random() is a pseudo-random number generator (PRNG) optimized for speed, not unpredictability. It uses an internal algorithm—typically xorshift128+ in V8—that produces numbers appearing random but following a deterministic sequence. If an attacker learns the internal state, they can predict every future output. Every single one.
Entropy sources, by contrast, pull randomness from physical system phenomena—CPU thermal noise, keystroke timing, disk seek times—that are fundamentally unpredictable. In JavaScript security tools, that's the difference between a lock and a painted-on lock.
What Exactly Counts as an Entropy Source in JavaScript?
Let's get concrete. An entropy source is any input that introduces genuine unpredictability into your system. In a browser environment, the Web Crypto API exposes this through crypto.getRandomValues(), which taps into the operating system's cryptographically secure random number generator (CSPRNG).
Checklist of what qualifies:
- OS-level entropy pools: /dev/urandom on Linux, CryptGenRandom on Windows, arc4random on macOS. These aggregate hardware interrupts, scheduler timing, and other noisy signals.
- Hardware random number generators: RDRAND instructions on Intel chips, though these have their own controversy.
- User-derived entropy: Mouse movement patterns, keystroke intervals, accelerometer data. Useful as supplementary input, risky as a sole source.
What doesn't count? Date.now() seeded into a custom PRNG. I've seen this in production code. The auditor's face was memorable.
How Predictable Is Math.random() in Practice?
More than you'd hope. V8's xorshift128+ uses a 128-bit internal state. If you can observe enough consecutive outputs—researchers have demonstrated this with as few as 5-10 values—you can reconstruct the state and predict all future numbers.
Consider a token generator that produces 8-character session IDs using Math.random(). With roughly 2.8 trillion possible combinations (62^8), you'd think brute force is impractical. But if the PRNG state is recoverable, the attacker doesn't brute force—they just calculate. The search space collapses from 2.8 trillion to 2^128 possible states, and observation narrows it further.
For a gambling application, a lottery selector, or anything where money or security rides on unpredictability, this is catastrophic.
When Is Math.random() Actually Acceptable?
Not never. Let's be honest about where it belongs:
- UI animations and particle effects where visual variety matters, not cryptographic strength
- Shuffling non-sensitive display data, like rotating featured products
- Game mechanics with no real-world stakes
- Generating test data for development environments
The rule I use: if losing predictability would cost someone money, expose private data, or break authentication, Math.random() is the wrong tool. Full stop.
How Do You Actually Use crypto.getRandomValues() Correctly?
Here's where most tutorials get vague. Let's fix that with a concrete implementation checklist:
Step 1: Check API Availability
The Web Crypto API is available in all modern browsers and requires HTTPS in most contexts. In Node.js, use crypto.randomBytes() or crypto.webcrypto.getRandomValues() from Node 15+.
Step 2: Request the Right Number of Bytes
For a 256-bit key, request 32 bytes. For a 16-byte UUID, request 16 bytes. The API caps at 65,536 bytes per call—if you need more, make multiple requests.
Step 3: Map to Your Output Format
Raw bytes aren't directly useful. Convert them appropriately: hex strings for tokens, Base64 for compact transmission, or integers within a specific range using rejection sampling to avoid modulo bias.
That last point matters. Using randomValue % range introduces bias when range doesn't divide evenly into 256. For range = 10, values 0-5 get selected 26 times out of 256 while 6-9 get 25 times. That's a 4% skew. Use rejection sampling: discard values above the largest multiple of range below 256, then apply modulo.
What About Node.js Security Tools?
Node.js developers get a slightly different toolkit. crypto.randomBytes(size) returns a Buffer filled with cryptographically strong bytes. For synchronous needs, crypto.randomFillSync() fills an existing buffer. The async version, crypto.randomFill(), won't block the event loop during entropy collection.
One gotcha: crypto.randomBytes() is synchronous and can block if the entropy pool is depleted. In high-throughput server scenarios, this creates latency spikes. The async variants let the event loop breathe while waiting for entropy.
Can You Mix Entropy Sources for Extra Security?
You can, and sometimes should. Combining multiple entropy sources through XOR operations or a hash function reduces reliance on any single source. If one is compromised or biased, the others maintain unpredictability.
A practical pattern: collect OS-provided randomness via crypto.getRandomValues(), XOR it with user-derived entropy (mouse movements hashed with SHA-256), and use the result. This is how some password managers achieve high-confidence randomness.
But don't overcomplicate it. For 95% of security tools, crypto.getRandomValues() alone is sufficient. Layering entropy sources makes sense for high-stakes applications—cryptocurrency key generation, certificate authorities, election systems—where the threat model justifies the complexity.
How Do You Verify Your Random Numbers Are Actually Random?
Visual inspection doesn't count. Run statistical tests. The NIST test suite and DieHarder battery evaluate sequences for uniformity, independence, and patterns. A single test passing means little; you want multiple tests across large sample sizes.
For practical development, a simpler check: generate 10,000 values and verify the distribution. If you're generating integers 0-9, each should appear roughly 1,000 times. Deviations beyond 3-4% suggest bias.
Remember: statistical randomness ≠ cryptographic security. A generator can pass every statistical test and still be predictable. The distinction matters. Math.random() passes most statistical tests. It still fails security audits because its predictability, not its distribution, is the problem.
What's the Bottom Line for Your Security Tool?
Swap Math.random() for crypto.getRandomValues() in any security-sensitive code path. Audit your dependencies—some libraries use Math.random() internally. Test your output for bias. Document your entropy sources for the next auditor who asks.
Your security tool's randomness is only as strong as its weakest source. Make sure that source isn't a PRNG designed for animation frame rates.
Frequently Asked Questions
Is Math.random secure for cryptography in JavaScript?
No, Math.random() is not secure for cryptographic purposes because it uses a predictable pseudo-random number generator (PRNG). JavaScript security tools require cryptographically secure pseudo-random number generators (CSPRNG) to prevent attackers from predicting future outputs.
What is the difference between Math.random and crypto.getRandomValues?
Math.random() generates predictable numbers based on an internal seed, while crypto.getRandomValues() uses system-level entropy sources to produce unpredictable results. The latter is designed for security-critical applications like generating tokens, whereas the former is meant for basic UI effects.
What is an entropy source in web security?
An entropy source is a physical or system-level process that provides unpredictable data, such as mouse movements, keyboard timings, or hardware noise. JavaScript security tools use these high-quality entropy sources to seed cryptographically secure random number generators, ensuring true unpredictability.
Why should I never use Math.random for password generation?
Using Math.random() for password generation makes the output vulnerable to reverse-engineering if an attacker can guess the internal state. Secure password generators rely on entropy-backed functions like crypto.getRandomValues() to ensure that generated passwords cannot be predicted.
How does JavaScript access system entropy?
JavaScript accesses system entropy through the Web Crypto API, specifically via window.crypto.getRandomValues(). This interface communicates directly with the operating system's secure random number generator, which continuously collects environmental noise and hardware entropy.
Are crypto.getRandomValues and window.crypto secure?
Yes, window.crypto and its getRandomValues() method are designed to be cryptographically secure and are supported in all modern browsers. They draw from the OS-level entropy pool, making them suitable for security tools, token generation, and cryptographic key creation.
What makes a random number generator cryptographically secure (CSPRNG)?
A CSPRNG uses high-entropy seeds and complex algorithms to ensure that the output is statistically random and completely unpredictable to attackers. Unlike standard PRNGs used in Math.random(), a CSPRNG guarantees that previous outputs cannot be used to calculate future or past numbers.
Can an attacker predict Math.random output in Node.js or browsers?
Yes, if an attacker observes enough consecutive outputs from Math.random(), they can deduce the internal state and predict future numbers. This is why security tools explicitly avoid Math.random() in favor of entropy-driven alternatives like Node.js's crypto.randomBytes().
Is Math.random a true random number generator (TRNG)?
No, Math.random() is a pseudo-random number generator (PRNG), meaning it relies on a deterministic mathematical formula rather than true physical entropy. True random number generators or CSPRNGs rely on unpredictable hardware or environmental events to produce secure randomness.
When is it appropriate to use Math.random in JavaScript?
It is appropriate to use Math.random() for non-security tasks like shuffling UI elements, game mechanics, or generating dummy data. If the random number will be used for authentication, encryption, or security tokens, you must use an entropy-backed cryptographic API instead.