Crypto-random password generator
Generate unbiased random passwords with the Web Crypto API, guaranteeing each chosen character class appears.
React / Next.jsUtilitiespasswordcryptorandomsecurity
Code
export type Options = {
length?: number;
lower?: boolean;
upper?: boolean;
digits?: boolean;
symbols?: boolean;
avoidAmbiguous?: boolean;
};
const SETS = {
lower: 'abcdefghijklmnopqrstuvwxyz',
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
digits: '0123456789',
symbols: '!@#$%^&*()-_=+[]{};:,.?',
} as const;
const AMBIGUOUS = /[O0oIl1|]/g;
// Unbiased integer in [0, max): reject values from the uneven tail.
function randomInt(max: number): number {
const limit = Math.floor(0x100000000 / max) * max;
const buf = new Uint32Array(1);
do {
crypto.getRandomValues(buf);
} while (buf[0]! >= limit);
return buf[0]! % max;
}
function classes(o: Options): string[] {
const { lower = true, upper = true, digits = true, symbols = false, avoidAmbiguous = false } = o;
const picked = Object.entries({ lower, upper, digits, symbols })
.filter(([, on]) => on)
.map(([name]) => SETS[name as keyof typeof SETS]);
return picked.map((s) => (avoidAmbiguous ? s.replace(AMBIGUOUS, '') : s)).filter(Boolean);
}
export function generate(o: Options = {}): string {
const sets = classes(o);
const length = Math.min(128, Math.max(sets.length, o.length ?? 16));
if (!sets.length) throw new Error('Select at least one character type');
const pool = sets.join('');
// One guaranteed character per class, the rest from the combined pool.
const out = sets.map((s) => s[randomInt(s.length)]!);
while (out.length < length) out.push(pool[randomInt(pool.length)]!);
for (let i = out.length - 1; i > 0; i--) {
const j = randomInt(i + 1); // Fisher-Yates so guaranteed characters are not first
[out[i], out[j]] = [out[j]!, out[i]!];
}
return out.join('');
}
export function entropyBits(o: Options = {}): number {
const poolSize = classes(o).join('').length;
return Math.round((o.length ?? 16) * Math.log2(Math.max(2, poolSize)));
}
How it works
What it solves
Math.random() is predictable and must never produce secrets. A secure generator needs a cryptographic source, must avoid the subtle bias introduced by taking a remainder of random bytes, and should guarantee that the requested character classes are actually present.
How it works
randomInt draws 32-bit values from crypto.getRandomValues and uses rejection sampling: any value above the largest multiple of the range is discarded and redrawn. Without this step, using value % n makes some characters slightly more likely whenever n does not divide 2 to the power of 32 evenly.
generate builds a pool from the selected classes (lower, upper, digits, symbols), optionally removing look-alike characters such as O, 0, l and 1. It first picks one character from every selected class, fills the remainder from the combined pool, then shuffles with a Fisher-Yates loop that also uses randomInt. Shuffling matters; otherwise the guaranteed characters would always sit at the start.
entropyBits reports length * log2(pool size), which gives a simple strength figure for the UI.
Adapting it
- Add a passphrase mode by picking random words from a list with
randomInt. - Require a minimum length of the number of classes in
generate. - Exclude a custom character list by filtering the pool.
Pitfalls
- Generate on the client only if the value never leaves the device; do not log it.
- Do not reuse the same generated value for several accounts.
- Entropy estimates assume uniform choice, which this code provides, but the forced classes slightly reduce it.
How to use it
- Save as
lib/generatePassword.ts; it works in browsers and Node 19+ (globalcrypto). generate({ length: 20, symbols: true, avoidAmbiguous: true })returns a string.- Call
entropyBits(options)to show a strength meter. - Never store the output in logs or analytics.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.