The most common mistake in homemade password generators is also the most invisible: using Math.random(). It looks random. It feels random. It is not random — at least not in any sense a security auditor or attacker cares about. This article explains why, walks through the right primitive (Web Crypto's getRandomValues), and builds a complete password generator like the one powering the YoBox Password Generator.
The problem with Math.random
Math.random() returns a double in [0, 1). Under the hood it's a pseudo-random number generator — a deterministic algorithm seeded once per page load. Given enough output, the internal state can be reconstructed, and from there every future "random" number is predictable. There are published papers on doing exactly that against V8 and SpiderMonkey.
For animations, particle effects, or shuffling a list of cat photos, that's fine. For passwords, encryption keys, session tokens, or anything an attacker cares about, it's malpractice.
The right primitive
Every modern browser ships Web Crypto. The relevant function is crypto.getRandomValues:
const buf = new Uint8Array(20);
crypto.getRandomValues(buf);
It fills the typed array with cryptographically secure random bytes drawn from the OS entropy source. On Linux that's /dev/urandom; on macOS, SecRandomCopyBytes; on Windows, BCryptGenRandom. All three are fit for keying material.
Mapping bytes to a charset
The naive approach has a subtle bias bug:
// Slight modulo bias — avoid in production
const c = charset[buf[i] % charset.length];
If charset.length doesn't evenly divide 256, the early characters in the charset are slightly more likely. For a 62-char alphanumeric set the bias is negligible; for a 70-char set with symbols it's measurable. The unbiased version rejects bytes above the largest multiple of charset.length:
function pickChar(charset: string): string {
const max = Math.floor(256 / charset.length) * charset.length;
while (true) {
const b = crypto.getRandomValues(new Uint8Array(1))[0];
if (b < max) return charset[b % charset.length];
}
}
For most production purposes, fetching a larger buffer and rejecting biased bytes in a single pass is more efficient:
export function generatePassword(length = 20, charset = DEFAULT_CHARSET) {
const max = Math.floor(256 / charset.length) * charset.length;
const out: string[] = [];
while (out.length < length) {
const buf = crypto.getRandomValues(new Uint8Array(length * 2));
for (const b of buf) {
if (out.length >= length) break;
if (b < max) out.push(charset[b % charset.length]);
}
}
return out.join("");
}
Choosing a charset
A pragmatic default:
const DEFAULT_CHARSET =
"ABCDEFGHJKMNPQRSTUVWXYZ" + // no I, L, O
"abcdefghjkmnpqrstuvwxyz" + // no i, l, o
"23456789" + // no 0, 1
"!@#$%^&*-_=+";
Excluding visually ambiguous characters (0/O, 1/l/I) is the difference between "this works in a screenshot handoff" and "support ticket every Tuesday."
Enforcing complexity rules
Some systems require at least one of each character class. The cleanest pattern is to draw one from each required class, then fill the rest from the union, then shuffle:
export function generateWithClasses(length = 20) {
const classes = [
"ABCDEFGHJKMNPQRSTUVWXYZ",
"abcdefghjkmnpqrstuvwxyz",
"23456789",
"!@#$%^&*-_=+",
];
const required = classes.map((c) => pickChar(c));
const rest = Array.from({ length: length - required.length }, () =>
pickChar(classes.join(""))
);
return shuffle([...required, ...rest]).join("");
}
function shuffle<T>(arr: T[]): T[] {
// Fisher–Yates with crypto-grade randomness
for (let i = arr.length - 1; i > 0; i--) {
const buf = crypto.getRandomValues(new Uint32Array(1));
const j = buf[0] % (i + 1);
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
Entropy: how strong is "strong"?
...............
Continue Reading
This article is part of the YoBox Developer Blog.
Read the complete guide here:
https://yobox.dev/blog/generating-cryptographically-secure-passwords-in-the-browser
⭐ More developer tools:
https://yobox.dev
⭐ GitHub examples:
https://github.com/hocineman4/yobox-examples
About YoBox
YoBox is a collection of free developer tools for API testing, disposable email, webhook inspection, Docker utilities, regex testing, password generation, and QA workflows.
⭐ GitHub Examples
Top comments (0)