DEV Community

ein-monarch
ein-monarch

Posted on

How Can A Randomly Generated Number Be "Insecure"? (a beginner's guide to PRNGs)

Let's play a little game...

I've generated a sequence of 16 bits using 3 different random number generators from different languages and libraries.

All you have to do is guess which one is cryptographically insecure.
Got it?

Alright, here they are:
A. 1010100000101001
B. 1100111001101101
C. 1101100010000111

Lock in your guess, and then click me!
The correct answer is... B.
A. Generated by Java's SecureRandom.nextBytes,
B. With Python's random.getrandbits (which is insecure),
C. Through JS's window.crypto.getRandomValues.

Congrats if you guessed it right!


The truth is, it's not possible to tell.
A single short stream of bits doesn't tell you anything about the cryptographic security of the source. With only 16 bits, any sequence is equally likely from a secure or insecure generator.

So, then, what makes Python's random.getrandbits different from JS's crypto.getRandomValues?
If you can't distinguish their outputs, then how can one of them be a vulnerability?

To understand, we need to break down a Random Number Generator, or RNG, down to its components.

What is an RNG in the first place?

Computers are always deterministic: the same input always results in the same output.

So, when we say RNG, we mean something that usually takes a starting value (a seed) and uses math to generate outputs that look random. The same seed leads to the same sequence of outputs, so this is technically called a Pseudo-Random Number Generator (PRNG), but I'll refer to it as RNG for simplicity.

Now, we can finally understand what an insecure RNG really means:
A weak Random Number Generator allows the seed value to be easily determined, typically by looking at enough of the RNG's outputs.
All RNGs are deterministic, so if you can figure out either its seed or its internal state, you now know every single future output.

When these predictable RNGs are used in security-critical portions of applications and websites, like session tokens, password resets, or key generation, it becomes a major vulnerability; for example, Wordpress, Docker, and, most recently, online Crypto wallets were all compromised due to flaws that made it easy to determine RNG seeds.

In truth, using an insecure RNG is a very simple mistake but can lead to disastrous consequences.

Thankfully, it's easy to prevent; if you're generating random numbers for any security-related reason, NEVER use Math.random() or your preferred language's general purpose equivalent. Most languages offer cryptographically secure alternatives, like Python's secrets module or JS's window.crypto.getRandomValues().

Okay; that's a simple change to make.
But why?

What really makes window.crypto.getRandomValues() actually secure and unguessable?

Let's take a deeper dive and actually see the difference between Math.Random() and window.crypto.getRandomValues().

The Two Parts To A Secure RNG

1. A Secure Algorithm

A secure RNG requires an algorithm that not only produces statistically random numbers, but also prevents attackers from determining its internal state from its outputs.

Math.random(), for example, typically uses xorshift128+ across modern JavaScript engines (V8, SpiderMonkey, JavaScriptCore). It was designed for speed, not security. Determining its internal state can be done from only a few outputs; just take a look at this repo.

Python's random.getrandbits(), on the other hand, uses the Mersenne Twister algorithm; while it has excellent statistical properties and a huge period, it is also not cryptographically secure. Observing 624 consecutive 32-bit outputs is enough to reconstruct its entire internal state and predict every future value.

window.crypto.getRandomValues(), though, works differently. Browsers do not implement their own cryptographic algorithm here. Instead, they delegate to the operating system's CSPRNG (Cryptographically Secure Pseudo-Random Number Generator); /dev/urandom on Linux, arc4random on macOS, or BCryptGenRandom on Windows. The OS uses a PRNG algorithm designed so that even with billions of outputs, determining its internal state requires solving problems believed to be computationally infeasible.

Even with an unbreakable RNG algorithm, if someone figures out the seed then they'll know all the future outputs; a secure algorithm alone isn't enough for a truly secure RNG.

An Unpredictable Seed

Remember: computers are deterministic.
Every input always results in an unchanging output.

So a good RNG needs to be initialised with an unpredictable seed.

The first strategy that comes to mind is to use time as a seed.
For example, we may seed the RNG with the number of nanoseconds that have elapsed since the start of the program.

However, this method is unreliable. It's not possible for an attacker to guess the exact nanosecond count, but it allows them to narrow it down to a range of values.

With just a few RNG outputs, the attacker can simply test every single seed within said range, figuring out which one perfectly matches the outputs.

Time based seeding simply isn't random enough; so, then, how do we produce unpredictable seeds?

The answer is Entropy Pools.

Essentially, your operating system converts events like mouse movements, key presses, disk I/O, network packet arrivals, and hardware interrupts into randomness.

When a system event occurs, the computer logs a high-resolution timestamp. Because raw timestamps are predictable, the system extracts entropy from the jitter (the microscopic, physically random variation) in the least significant bits of those timestamps, then blends that unpredictability into the pool via cryptographic mixing.

An image of Cloudflare's lava lamp wall.

Cloudflare also extracts randomness from physical events: they convert chaotic lava flow to unpredictable randomness by hashing photos of a lava lamp wall.

If you're on Linux, you can check your kernel's entropy estimator with:

cat /proc/sys/kernel/random/entropy_avail
Enter fullscreen mode Exit fullscreen mode

This used to tell you how many bits of entropy, or randomness, is left in your system's entropy pool. On older devices, seeding an CSPRNG would reduce the bits of entropy in the pool, requiring more events to occur to regenerate its size.

On modern kernels (version 5.17+), this will typically read 256. That number reflects that the kernel's own CSPRNG (which typically uses the ChaCha20 algorithm) has been successfully seeded with 256 bits of quality entropy. In other words, we use random events to seed a CSPRNG, which is used to generate seeds for other CSPRNGs.

On a typical desktop, this seeding happens during boot; on servers or VMs with few entropy sources, it can sometimes take longer.

To give some context to what 256 bits of security really means:

There are around 10^68 atoms in our galaxy, the Milky Way.
The probability of picking one specific atom out of all the matter in the entire Milky Way is a billion times more likely than guessing a 256 bit value correctly.

The Future of RNGs?

Standard computers are deterministic, but quantum mechanics is fundamentally probabilistic. Devices called Quantum Random Number Generators (QRNGs) exploit this by measuring quantum phenomena such as radioactive decay; these phenomena are by definition impossible to predict, no matter how many outputs you have.

You can actually view or even listen to real outputs from an actual QRNG here.

Unlike PRNGs, QRNGs do not rely on a seed or an internal algorithmic state. However, they are still physical devices: they can suffer from hardware biases, environmental noise, or manufacturing flaws, so their outputs still require post-processing and health monitoring to ensure quality.

So, for now, I suppose window.crypto.getRandomValues, backed by your system's robust CSPRNG and seeded using data generated (in part) from each keypress and mouse movement you make, will have to do.

Leave a reaction if you learned something new, and I'd love to hear tell any security disasters caused by Math.Random() you've heard of!

Til next time,
ein-monarch

Top comments (0)