CSPRNG vs PRNG
Two properties separate the two, neither of them is statistical, and no test suite can tell you which one you have.
Updated 2026-09-05
Two properties, and neither is about uniformity
A pseudorandom generator is a deterministic function that expands a small seed into a long output stream. That description covers both a fast PRNG such as xorshift128+ and a cryptographic one such as ChaCha20 or an AES-CTR DRBG. The difference is not the shape of the output. Both produce output that looks uniform under every statistical test you are likely to run.
The first property that matters is next-bit unpredictability. Given the first k bits of the output, no efficient algorithm should predict bit k plus one with probability meaningfully better than one half. This is a stronger requirement than it sounds, because Yao showed that a generator passing the next-bit test passes every efficient statistical test. Cryptographic security implies statistical quality. The reverse does not hold, which is the entire point.
The second property is state compromise resistance, and it comes in two directions. Backtracking resistance means that an attacker who learns the internal state today cannot reconstruct output the generator produced yesterday. Forward secrecy, sometimes called prediction resistance when it comes from reseeding, means that the same attacker loses the ability to predict future output once fresh entropy is mixed in. A fast PRNG has neither. Its state is small enough to recover and it never mixes anything new in, so one compromise is total and permanent in both directions.
Why a test suite cannot answer the question
Take a 128-bit counter, encrypt it with AES under a fixed key, and emit the ciphertext. Increment and repeat. That stream passes every statistical battery ever written, because AES output is indistinguishable from random to anyone without the key. Now hand the key to the attacker. The stream is a counter. Every value, past and future, falls out immediately.
The same construction with a known key is statistically perfect and cryptographically worthless. That single example settles the question of whether uniformity testing can establish security. It cannot, in either direction. A generator can be statistically flawless and fully predictable, and a generator with a small measurable bias can still be cryptographically strong.
This is why we publish chi-square results for our own output and then say explicitly what they prove. They validate our mapping from random bytes to characters, which is a real thing that can be wrong and often is. They say nothing about the browser CSPRNG underneath, and they are not a security argument. Two separate claims, and neither is allowed to borrow the other's credibility.
A statistical test suite detects bias. It cannot detect predictability. Any page that offers a chi-square result as evidence that its output is secure has confused the two.
The chain from your browser to the kernel
crypto.getRandomValues is not itself a generator. It is a request that travels down through the browser to the operating system, which maintains a CSPRNG seeded from hardware sources and interrupt timing and reseeded continuously. The relevant point is that the browser does not roll its own entropy, and neither should you.
We read the Chromium source rather than assuming. Chromium routes through BoringSSL RAND_bytes, which calls ProcessPrng from bcryptprimitives.dll on Windows, getrandom on Linux, getentropy on macOS, and zx_cprng_draw on Fuchsia. The Windows choice is documented in the source as avoiding a device handle in the renderer process. We have not read the Gecko or WebKit sources, so we make no claim about which call Firefox and Safari use, only that both expose the same specified interface.
If you are writing outside the browser, the table below is the set of calls you want. Every one of them is a wrapper over the kernel CSPRNG. None of them requires you to open a file, check a short read and close a descriptor correctly, which is the class of mistake that file-based interfaces invite.
| Platform | Call | Behaviour |
|---|---|---|
| Linux | getrandom(buf, len, 0) | Available since kernel 3.17. Blocks only until the kernel CSPRNG is initially seeded, then never again. |
| Windows | BCryptGenRandom with BCRYPT_USE_SYSTEM_PREFERRED_RNG | The documented replacement for the deprecated CryptGenRandom. Chromium reaches the same generator through ProcessPrng. |
| macOS, iOS | SecRandomCopyBytes(kSecRandomDefault, len, buf) | The Security framework wrapper. getentropy is the lower-level equivalent and is what Chromium uses. |
| Any POSIX | getentropy(buf, len) | Capped at 256 bytes per call. Loop for larger requests. |
crypto.getRandomValues and its quota
The Web Crypto function fills an integer typed array in place and returns the same array. It accepts Int8Array through BigUint64Array and refuses floating-point arrays with a TypeMismatchError, which is a reasonable design decision that catches a real class of mistake.
The limit that trips people up is that the per-call quota is 65,536 bytes, not 65,536 elements. A Uint8Array of 65,536 is the largest single call. A Uint32Array is capped at 16,384, and asking for 16,385 throws QuotaExceededError. The fix is to loop over subarray slices, which costs nothing, and the failure mode of not looping is an exception in production on exactly the large batch you did not test.
One more practical note. Drawing four bytes per character is wasteful when you are generating ten thousand strings. Fill a pool once and serve draws from it, and a batch of 10,000 strings of 32 characters becomes roughly 78 calls into the operating system rather than 320,000. Several audited competitors make one getRandomValues call per character, which is correct and slow.
The /dev/random argument is over
The folklore says that /dev/random draws from a finite pool of gathered entropy, that reading it depletes that pool, and that /dev/urandom is the unsafe shortcut that keeps returning bytes after the entropy has run out. Serious-sounding advice built on this model still circulates, and it is out of date.
The entropy-depletion model was wrong about how a modern CSPRNG works. Once the kernel generator has been seeded with enough entropy to be unpredictable, it stays unpredictable, and continuing to draw from it does not wear it down any more than encrypting more plaintext wears out AES. Linux removed the blocking pool in kernel 5.6, and since then /dev/random blocks only until the generator is first seeded and behaves identically to /dev/urandom afterwards.
That leaves one genuine problem, and it is the one the folklore was pointing at from the wrong direction. Very early in boot, before the kernel has gathered enough entropy, /dev/urandom will return bytes anyway. On a general-purpose machine this window is microscopic. On an embedded device or a freshly created virtual machine with no disk timing, no interrupt noise and no hardware RNG, it can be long enough to matter. getrandom with no flags is the correct answer to this: it blocks until seeding is complete and never blocks again, which is exactly the behaviour you want and neither device file gives you by itself.
Where CSPRNGs actually fail in practice
Real-world randomness failures are almost never breaks of the algorithm. ChaCha20 and AES-CTR do not fall over. What falls over is the seed.
The recurring pattern is state duplication. A virtual machine image or container built with a seeded userspace pool, then started a thousand times, produces a thousand identical streams. A process that forks after seeding gives both halves the same state until something reseeds. A device that generates its host key on first boot before the kernel pool is ready produces a key an attacker can enumerate. In each case the generator worked exactly as designed and the entropy was never there.
The defence is to draw from the kernel rather than from a userspace pool you manage, and to draw at the moment of use rather than seeding something at startup and carrying it around. crypto.getRandomValues does this for you. So does secrets in Python, crypto/rand in Go, random_bytes in PHP and RandomNumberGenerator in .NET. The advice is boring on purpose: use the platform call, do not cache its output, and do not build an abstraction over it that could be configured to use something else.
Doing this in your own code
Chunking around the quota
// getRandomValues() throws QuotaExceededError above 65,536 BYTES.
// That is 16,384 Uint32 slots, not 65,536 of them.
const QUOTA_BYTES = 65536;
const QUOTA_WORDS = QUOTA_BYTES >>> 2;
function fillRandomWords(words) {
for (let off = 0; off < words.length; off += QUOTA_WORDS) {
const end = Math.min(off + QUOTA_WORDS, words.length);
crypto.getRandomValues(words.subarray(off, end));
}
return words;
}
new Uint32Array(16384); // fine
// crypto.getRandomValues(new Uint32Array(16385)) -> QuotaExceededError
// crypto.getRandomValues(new Float32Array(4)) -> TypeMismatchError The quota is per call, not per page. Loop, do not fall back to anything weaker.
The bottom of the stack
/* Linux, since 3.17. Blocks only until the kernel CSPRNG is seeded,
then never blocks again. This is the call you want. */
ssize_t n = getrandom(buf, len, 0);
/* Windows. */
NTSTATUS s = BCryptGenRandom(NULL, buf, len,
BCRYPT_USE_SYSTEM_PREFERRED_RNG);
/* Apple platforms. */
int r = SecRandomCopyBytes(kSecRandomDefault, len, buf); Every one of these is a wrapper over a kernel CSPRNG seeded from hardware and interrupt entropy. None of them reads a file you have to open, check and close correctly.
Questions
Is xorshift128+ a bad algorithm?
No, it is a good algorithm for the job it was designed to do. It is fast, it has a long period and it passes standard statistical batteries. It was never designed to resist an attacker who sees its output, and it does not. Calling it bad conflates the two purposes.
Does crypto.getRandomValues ever block or fail?
In a browser it does not block, because the operating system generator is long since seeded by the time a page loads. It throws QuotaExceededError above 65,536 bytes in one call and TypeMismatchError for a floating-point array. It can also be absent in a non-secure context in some older runtimes, in which case the right response is to throw, never to fall back to Math.random.
Do I need to add my own entropy, such as mouse movements or timestamps?
No. Mixing your own inputs into a kernel-seeded CSPRNG cannot improve it and can only introduce a bug in the mixing. The kernel already collects interrupt timing and hardware sources, and it does so continuously.
Should I use /dev/random or /dev/urandom on Linux?
Use neither directly. Call getrandom with no flags, or a language wrapper that does. On a kernel from 5.6 onward the two device files behave the same once the generator is seeded, and the syscall removes the file descriptor handling that the device files make you get right.
Is a hardware random number generator better than the kernel CSPRNG?
It is a good entropy source, and the kernel already uses it when present. Reading a hardware generator directly and using its raw output is usually worse, because raw physical sources are biased and need conditioning, which is precisely what the kernel does before handing you bytes.