How Password Generators Work: Randomness, Entropy & Security
Introduction
When you click "Generate" in a password tool, you receive a string of characters that looks random — but not all randomness is equal. The difference between a password generated with Math.random() and one generated with crypto.getRandomValues() is the difference between security theater and genuine protection. Understanding how a password generator works under the hood helps you evaluate whether a tool you use can actually be trusted.
This article explains the mechanics of secure password generation: where the randomness comes from, how character sets affect the number of possible passwords, why modulo bias is a subtle but real problem, and how a well-built generator eliminates every source of predictability.
The Problem with Math.random()
JavaScript's Math.random() is a pseudorandom number generator (PRNG). It produces a sequence of numbers that appear random but are entirely deterministic: given the same internal seed value, the function will produce the same output. More importantly, the browser's JavaScript engine chooses that seed — often from low-entropy sources like the current time in milliseconds.
An attacker who can observe a few outputs from Math.random() can often reconstruct the internal state and predict future outputs. A 2010 academic paper demonstrated this attack against browser implementations. If a password generator uses Math.random(), an attacker who knows which tool you used, approximately when, and sees the first few generated characters can potentially predict the rest of your password.
Cryptographic Randomness: crypto.getRandomValues()
The Web Cryptography API's crypto.getRandomValues() uses the operating system's cryptographically secure pseudorandom number generator (CSPRNG). On Linux this is /dev/urandom, on Windows it is CryptGenRandom, and on macOS it uses the kernel's built-in entropy pool. These CSPRNGs are seeded from hardware sources: interrupt timing, keyboard events, disk activity, network traffic — unpredictable physical phenomena that are extraordinarily difficult to predict or reproduce.
The fundamental guarantee is that no computationally feasible algorithm can distinguish the output of a CSPRNG from truly random noise. This is a cryptographic security property, not just a statistical one.
// Insecure: do not use for passwords
function weakRandom(max) {
return Math.floor(Math.random() * max);
}
// Secure: cryptographically random index
function secureRandom(max) {
const array = new Uint32Array(1);
crypto.getRandomValues(array);
return array[0] % max; // Note: still has modulo bias — see below
}
How Character Sets Determine the Search Space
Once you have a secure random number, you use it to pick a character from your character set. The size of that set determines how many possible values each position can hold, which directly determines how hard brute-force attacks become.
The total number of possible passwords of a given length is:
possible_passwords = charset_size ^ password_length
The common character sets and their sizes:
| Character Set | Characters Included | Size |
|---|---|---|
| Lowercase only | a–z | 26 |
| Lowercase + uppercase | a–z, A–Z | 52 |
| Alphanumeric | a–z, A–Z, 0–9 | 62 |
| Full printable ASCII | a–z, A–Z, 0–9, symbols | 94 |
The difference is substantial. A 16-character lowercase password has 26^16 ≈ 43 quadrillion combinations. The same length with a full 94-character set has 94^16 ≈ 30 septillion — roughly 700,000 times more. Adding symbols is not just cosmetic: it meaningfully expands the search space an attacker must exhaust.
Rejection Sampling: Eliminating Modulo Bias
There is a subtle flaw in the naive implementation above. When you use array[0] % charset_size, you introduce modulo bias. A Uint32Array holds values from 0 to 4,294,967,295 (2^32 - 1). If your charset has 94 characters, the 4.3 billion values do not divide evenly into 94 groups — some characters will appear slightly more often than others.
While the bias is small (less than 0.01%), it is a measurable statistical deviation from true uniformity. In high-security contexts, or when generating many passwords, it compounds. The correct solution is rejection sampling:
function secureRandomIndex(max) {
// Calculate the largest multiple of max that fits in Uint32
const limit = Math.floor(0x100000000 / max) * max;
let value;
const buf = new Uint32Array(1);
do {
crypto.getRandomValues(buf);
value = buf[0];
} while (value >= limit); // Reject values in the biased tail
return value % max;
}
This loop re-draws until the value falls within a uniformly distributed range. In practice it almost never iterates more than once, but it guarantees mathematically unbiased output. This is the same approach used in the standard library implementations of many compiled languages.
Assembling the Password
With a bias-free random index function and a character set, generating a password is straightforward:
function generatePassword(length, charset) {
let password = '';
for (let i = 0; i < length; i++) {
password += charset[secureRandomIndex(charset.length)];
}
return password;
}
const charset =
'abcdefghijklmnopqrstuvwxyz' +
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'0123456789' +
'\!@#$%^&*()-_=+[]{}|;:,.<>?';
const password = generatePassword(16, charset);
// Example: "k#T9mQr2\!Bx&Lv5n"
Each character is chosen independently and uniformly. No character is more likely than any other. No pattern from one position influences the next. The result has the full entropy of the charset and length combination.
Why You Should Never Create Passwords Manually
Humans are terrible random number generators. When asked to produce a "random" string, people unconsciously avoid long runs of the same character type, prefer certain characters over others, and create patterns that reflect their cognitive biases. Studies have shown that human-generated "random" sequences are predictable at rates far above chance.
Even when you try hard to be random, the effort itself introduces bias: you avoid characters you just typed, you alternate between types in ways that feel "balanced," and you rely on working memory in ways that constrain what you produce. A computer generating a password with a CSPRNG has none of these limitations. It is indifferent, uniform, and fast.
How Our Tool Uses crypto.getRandomValues()
The SnapUtils password generator runs entirely in your browser. When you click Generate:
- Your selected options (length, character types) define the charset string.
- For each character position,
crypto.getRandomValues()fills aUint32Array. - Rejection sampling removes modulo bias before mapping to a charset index.
- The password is assembled locally — it is never sent to any server.
Because all computation happens in your browser, no network request can intercept the generated password. You can verify this by opening your browser's network inspector before clicking Generate: no requests will appear.
See It in Action
Try our password generator — cryptographic randomness, rejection sampling, and zero server-side processing, all in your browser.
Open Password Generator