Cracking the Factory: When 'Random' Passwords Aren't

Every password your computer has ever generated is the output of a deterministic algorithm. When that algorithm is weak, fingerprinted, or seeded from a timestamp, 'random' collapses to 'predictable with effort.' Three real-world cases prove this isn't theoretical.

Cracking the Factory: When 'Random' Passwords Aren't — Audio

Listen to the audio version of this investigation.

0:00 0:00

Nothing Is Random

Here is an uncomfortable truth about every password your computer has ever generated: none of them are random.

Not in the way you think. Not in the way the marketing copy implies. Every “random” character your machine produces is the output of a deterministic algorithm, a mathematical function that, given the same starting state, will produce the same sequence every time. We call it pseudo-random because it looks random to anyone who doesn’t know the initial state. But “looking random” and “being random” are not the same thing.

Your operating system tries hard to make this work. Linux’s /dev/urandom collects entropy from interrupt timing, disk I/O jitter, keyboard and mouse events, the accumulated chaos of a machine interacting with the physical world. Intel’s RDRAND instruction samples thermal noise in silicon. These sources are chaotic, unpredictable in practice, and sufficient for most purposes. But they’re not random in the philosophical sense. They’re deterministic physical processes we can’t practically measure with enough precision to predict.

True randomness, if it exists at all, lives at the quantum level. Radioactive decay. Photon polarization. Phenomena that, according to our best physics, are genuinely indeterministic. But your password manager doesn’t have a Geiger counter. It has a CPU, a system clock, and an algorithm.

So the real question isn’t whether your password is random. It’s how far from random it actually is, and whether that gap is large enough for an attacker to exploit.

The answer, it turns out, is yes. Repeatedly.

The Chain of Trust

The concept of a “random” password rests on a chain of assumptions:

  1. The entropy source is unpredictable, maybe, depending on what your OS collects
  2. The PRNG preserves that unpredictability, often it doesn’t
  3. The mapping to characters doesn’t introduce bias, modulo bias says otherwise
  4. The implementation doesn’t leak state, JavaScript’s Math.random() does

Break any single link and “random” collapses to “predictable with effort.” Break two and you’re looking at passwords that can be brute-forced in minutes.

This isn’t theoretical. It’s been done. Repeatedly.


Case 1: Kaspersky, Every Password in the World, at the Same Second

In June 2019, Jean-Baptiste Bédrune at Ledger Donjon reported a devastating flaw in Kaspersky Password Manager (KPM). The vulnerability was so fundamental it sounds like a textbook exercise: KPM seeded its password generator with the current system time.

Not a hash of the system time combined with other entropy. Not the time plus mouse movements plus process IDs. Just the time. In seconds.

The consequence: every instance of Kaspersky Password Manager in the world generated the exact same password at any given second.

The Math

The desktop application used Mersenne Twister, a well-known PRNG that passes statistical randomness tests but is not cryptographically secure. The web version used JavaScript’s Math.random(). Both were seeded with time().

If you know roughly when a password was generated (which you often can, account creation dates are frequently visible or inferable), you know the seed. If you know the seed, you know the password.

Bédrune demonstrated that all passwords generated during the vulnerable period for a given character set could be brute-forced in minutes. Not hours. Not days. Minutes.

The vulnerability was assigned CVE-2020-27020. An initial code fix arrived in October 2019, four months after disclosure, but the full remediation including user notifications to regenerate affected passwords wasn’t deployed until October 2020. Kaspersky’s public advisory followed in April 2021.

Sources: Ledger Donjon blog, CVE-2020-27020, Hackread


Case 2: RoboForm, Hacking Time to Steal $3 Million in Bitcoin

In 2024, hardware hacker Joe Grand (known as “Kingpin”) and software researcher Bruno Krauss reverse-engineered an older version of RoboForm’s password generator. Their target: a Bitcoin wallet locked behind a lost RoboForm-generated password from 2013.

The flaw was identical in principle to Kaspersky’s: RoboForm versions before 7.9.14 (June 2015) used the system clock as the primary entropy source. Passwords were deterministic. If you could set the system clock to the right time, you could regenerate the exact same password.

Grand and Krauss reverse-engineered the password generation routine, then automated mass generation of passwords across a timeframe window. They recovered the password. The wallet contained approximately $3 million in Bitcoin.

The work was presented at DEF CON 32 in 2024.

Key insight: The passwords looked random. They contained uppercase, lowercase, numbers, symbols. No human would suspect they were generated from a timestamp seed. But the algorithm knew, and the algorithm could be reversed.

Sources: Tom’s Hardware, El País, Grand Idea Studio, Technical Notes (PDF)


Case 3: Math.random(), The Web’s Dirty Secret

JavaScript’s Math.random() is not random. The specification says so. MDN says so. Every security researcher says so. And yet, password generators across the web still use it.

Most modern browsers implement Math.random() using XorShift128+, a fast PRNG with good statistical properties but zero cryptographic security. The internal state is 128 bits. If an attacker can observe as few as three outputs, they can reconstruct the full internal state using tools like the Z3 SMT solver. From there, they can predict every future output, and reconstruct every past one.

What This Means for Password Generators

Any web-based password generator that uses Math.random() instead of the Web Crypto API (crypto.getRandomValues()) is vulnerable. The password it generates is deterministic. If you can observe or infer a few outputs from the same generator instance (which can happen through timing attacks, shared browser contexts, or observable side effects), you can predict the password.

In 2014, a Firefox for Android vulnerability (CVE-2014-1516) used a weak PRNG to generate profile directory names, enabling a malicious app to predict file paths and access sensitive user data. In 2015, the CSGO Jackpot gambling site was broken because it used Math.random() for determining winners, a researcher predicted outcomes by observing publicly displayed results.

In 2025, CVE-2025-7783 (CVSS 9.4 Critical) demonstrated that the popular Node.js form-data library used Math.random() for multipart boundary generation, making applications vulnerable to parameter injection via PRNG state recovery.

The pattern is always the same: What looks random is actually predictable. The generator’s fingerprint is hiding in the output.

Sources: V8 blog on Math.random, DeepSource, Z3 state recovery, CSGO Jackpot exploit


The Fingerprinting Attack

Here’s where it gets interesting, and where the original intuition proves out.

If you have a leaked password hash and you suspect the password was generated by a specific tool, you don’t need to brute-force the full character space. You need to brute-force only the outputs that tool could produce.

Three Fingerprinting Vectors

1. Modulo Bias

When a generator converts a random number to a character from a set, it typically uses the modulo operator: charset[random_number % charset_length]. If the random number’s range isn’t an exact multiple of the charset length, some characters appear more frequently than others.

This bias is measurable. Given enough samples, you can determine the charset size, the approximate RNG range, and potentially the algorithm. Even a 0.1% frequency deviation across millions of characters is a detectable signal.

2. Structural Constraints

Many generators enforce rules that shrink the keyspace:

  • Always include at least one uppercase, one lowercase, one digit, one symbol
  • Never start with a symbol
  • Never use ambiguous characters (0/O, 1/l/I)
  • Never repeat consecutive characters
  • Fixed first-character capitalization

Each constraint is a filter. Stack enough filters and the “random” password lives in a much smaller space than its character length suggests.

A 16-character password from a 95-character set theoretically has ~105 bits of entropy. Add three structural constraints and you might be down to 70. Add a weak PRNG and you might be at 30. Add a timestamp seed and you’re at effectively zero.

3. Character Distribution Signatures

Different generators produce different statistical fingerprints:

  • Some generators use a limited set of special characters for longer passwords
  • Some generators weight character classes differently (e.g. 60% alpha, 20% numeric, 20% symbol)
  • “Memorable” password generators (word+number+symbol) have characteristic morphological patterns

These signatures can be extracted from leaked password databases. If a breach reveals plaintext or weakly hashed passwords, statistical analysis can cluster them by likely generator, then apply targeted attacks to each cluster.


The Real Keyspace

Here’s what the attack looks like in practice:

Scenario Theoretical Keyspace Actual Keyspace Time to Crack
16-char, 95 chars, true CSPRNG 95^16 ≈ 2^105 2^105 Heat death of universe
16-char, KPM (timestamp seed) 95^16 ≈ 2^105 ~315,000 per second window Minutes
16-char, RoboForm pre-2015 95^16 ≈ 2^105 Days × seconds in window Hours
16-char, Math.random() (state known) 95^16 ≈ 2^105 1 (deterministic) Instant
12-char, “memorable” generator Looks like 2^78 ~50,000 word combos × variants Seconds

The gap between theoretical and actual keyspace is the attack surface.


How to Protect Yourself

For Users:

  1. Check your generator’s source code. Open-source generators (KeePass, Bitwarden) can be audited. If you can’t see the code, you can’t trust the randomness.
  2. Look for crypto.getRandomValues() or equivalent. If a web-based generator uses Math.random(), close the tab.
  3. Regenerate old passwords. If you used Kaspersky, RoboForm (pre-2015), or any generator you’re unsure about, regenerate everything. Now.
  4. Use a passphrase with real entropy. Diceware with physical dice is still among the most secure methods, no algorithm to reverse, no state to recover.

For Developers:

  1. Never use Math.random() for security-critical values. Use crypto.getRandomValues() (browser) or crypto.randomBytes() (Node.js).
  2. Seed your PRNG from OS entropy, not timestamps. This is a solved problem. /dev/urandom, CryptGenRandom(), or equivalent.
  3. Handle modulo bias. Use rejection sampling. KeePass does it right, the code is public and well-commented.
  4. Don’t enforce structural rules that shrink keyspace. If you require “at least one uppercase,” you’re reducing entropy, not increasing it. A truly random 20-character password from a 95-character set is strong enough without training wheels.

The Uncomfortable Truth

Password generators exist because humans are bad at randomness. But the generators themselves are written by humans, and humans make predictable mistakes. Timestamp seeds. Mersenne Twister instead of a CSPRNG. Modulo bias instead of rejection sampling. Structural rules that feel secure but shrink the space.

The question isn’t whether your password is long enough. It’s whether the algorithm that built it left a fingerprint.


FTRCRP, Future Trust & Responsible Computing Practice Investigation by mr0 · Research by HAL March 2026