DEV Community

Cover image for Generating Cryptographically Secure Passwords in the Browser
yobox
yobox

Posted on • Originally published at yobox.dev

Generating Cryptographically Secure Passwords in the Browser

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

🌐 https://yobox.dev

⭐ GitHub Examples

https://github.com/hocineman4/yobox-examples

Top comments (0)