How to Generate a Cryptographically Secure Random Number in Your Browser
Math.random() is not secure. Learn why, what "secure" means for random number generation, the right browser API to use (crypto.getRandomValues), and how to generate a random number in a range without bias.
For a cryptographically secure random number in the browser, use <code>crypto.getRandomValues()</code> — the Web Crypto API, available in every modern browser. <code>Math.random()</code> is not secure: it is a PRNG, not a CSPRNG, and it is seeded with a value that is often predictable (timestamp, in some implementations). The <a href="/random-number-generator">Uttir Random Number Generator</a> uses crypto.getRandomValues() in your browser, with options for cryptographically-secure mode, multiple numbers, and unique numbers (no duplicates).
You need a random number. You reach for Math.random(). It works. It returns a number between 0 and 1. You scale it to your range. You ship it.
The problem: Math.random() is not cryptographically secure. It is a pseudo-random number generator, not a cryptographically-secure pseudo-random number generator (CSPRNG). For most uses (a game, a UI effect, a sample dataset), the difference does not matter. For some uses (a password, a token, a security identifier, anything where predictability is a vulnerability), the difference is the bug that gets you breached.
This post is the short version of when it matters, why Math.random() is not enough, what to use instead, and how to do it right in the browser. The Uttir Random Number Generator uses the right API and exposes a "cryptographically secure" option; the Password Generator uses the same API for the same reason.
Why Math.random() is not secure
Math.random() is a pseudo-random number generator (PRNG). It takes a seed value, runs a deterministic algorithm on it, and produces a sequence of numbers that look random. The numbers are not random; they are the output of a function. If you know the seed and the algorithm, you can predict every number the function will ever produce.
The seed in most JavaScript implementations is derived from the current time, in milliseconds. If the attacker knows roughly when the random number was generated (often, they do — server logs, request timestamps, observable side effects), they can narrow the seed to a small range, run the algorithm with each candidate seed, and predict the output.
For a CSPRNG, the seed is derived from a source of true entropy (hardware noise, OS-level randomness, user input) and the algorithm is designed so that even with full knowledge of the output, the attacker cannot predict the next number. The seed is not recoverable from the output; the output does not reveal the internal state.
The distinction is not academic. In 2012, a group of researchers hacked a large number of Bitcoin wallets by exploiting a similar weakness in the Java SecureRandom implementation on Android. The wallets generated random numbers using a PRNG that was not cryptographically secure, and the researchers could predict the private keys from a small number of outputs. The same class of bug, in any system, has the same class of consequence.
What to use instead
In the browser, the right API is crypto.getRandomValues(). It is part of the Web Crypto API, available in every modern browser, and it is a CSPRNG. The output is unpredictable, the seed is not recoverable, and the implementation is audited by the browser vendors.
// Generate a 32-byte random buffer.
const buffer = new Uint8Array(32);
crypto.getRandomValues(buffer);
// Or, to get a single random number in a range:
function secureRandomInt(max) {
const buffer = new Uint32Array(1);
crypto.getRandomValues(buffer);
return buffer[0] % max;
}
This is what the Web Crypto API is for. The implementation is in the browser, not in user-space code, so it is not vulnerable to the JavaScript-level attacks that Math.random() is. The output is the same kind of randomness that /dev/urandom on a Unix system or BCryptGenRandom on Windows provides — the kind that is suitable for cryptographic keys, session tokens, password salts, and any other security-sensitive use.
The modulo bias problem
The naive buffer[0] % max in the example above is biased when the range of the random number is not a power-of-two divisor of the buffer's max. If the buffer is 32 bits (max value 2^32 = 4,294,967,296) and the desired max is 100, then 4,294,967,296 is not evenly divisible by 100. The first 96 values (0, 100, 200, ..., 429,496,700) get one extra hit each, and the bias is small but measurable.
For most uses (a game, a UI effect, a sample), the bias is so small it does not matter. For a security-sensitive use (a session token, a password salt, a cryptographic nonce), the bias is a vulnerability. The fix is to use a rejection-sampling loop: keep generating random numbers until you get one in the right range.
function secureRandomInt(max) {
// Compute the largest multiple of max that fits in 32 bits.
const limit = Math.floor(0xFFFFFFFF / max) * max;
const buffer = new Uint32Array(1);
let value;
do {
crypto.getRandomValues(buffer);
value = buffer[0];
} while (value >= limit);
return value % max;
}
This eliminates the bias. The loop runs at most a few iterations (the expected number of iterations is 1 for small max, more for max close to 2^32). The performance cost is negligible.
The Uttir Random Number Generator does this for you — it accepts a min, max, and count, and uses the rejection-sampling approach to produce unbiased random numbers.
When Math.random() is fine
For most uses, Math.random() is fine. The list of fine uses is long; the list of not-fine uses is short but important.
Fine:
- A game (a coin flip for fun, a random number in a board game, a random enemy spawn)
- A UI effect (a random particle direction, a random colour, a random delay)
- A sample dataset (a random subset of users for an A/B test, a random shuffle for a demo)
- A random ordering for display (a random sort of search results, a random order of gallery images)
Not fine:
- A password (use the Password Generator)
- A session token (use crypto.getRandomValues to fill a buffer, base64-encode the result)
- A password reset token (same as session token)
- A CSRF token (same as session token)
- A cryptographic nonce (use crypto.getRandomValues, never Math.random)
- A UUID (use crypto.randomUUID, which is built on crypto.getRandomValues)
- A cryptographic key (use crypto.subtle.generateKey, which is built on crypto.getRandomValues)
The rule of thumb: if the random value is shown to the user, it is probably fine to use Math.random(). If the random value is sent to the server, used as a security token, or used to derive a cryptographic key, use crypto.getRandomValues() instead.
Other browser APIs
Beyond crypto.getRandomValues, the Web Crypto API has a few other useful primitives.
crypto.randomUUID() returns a random UUID v4. It is the right way to generate a UUID in the browser; the implementation is built on crypto.getRandomValues, and the output is RFC 4122 compliant. The Uttir UUID Generator uses this internally.
crypto.subtle.generateKey() generates a cryptographic key for use with the Web Crypto API's encryption, decryption, signing, and verification functions. The output is a CryptoKey object that you can export, store, or use directly.
crypto.subtle.digest() computes a hash (SHA-256, SHA-384, SHA-512) of a buffer. Useful for fingerprinting, integrity verification, and content addressing. The Password Strength Checker can use this to check if a password has appeared in known breach databases (by hashing the password and comparing against a list of hashes).
All four are part of the same Web Crypto API, available in every modern browser, and free to use. There is no excuse for shipping Math.random() in a security-sensitive code path.
How to do it in your browser
The Uttir Random Number Generator accepts a min, max, and count, with a "cryptographically secure" option that uses crypto.getRandomValues with the rejection-sampling approach. The output is unbiased, fast, and never predictable from the previous output. The Password Generator uses the same API for the same reason. The UUID Generator uses crypto.randomUUID(), which is built on the same primitive.
The Coin Flip tool is a good example of the same API used for a simpler case: a single bit of random, biased neither toward heads nor tails, with no way to predict the next flip from the previous one. The Password Strength Checker is the right tool to verify that the password you generated (or the one you already have) is strong enough to survive a guessing attack.