DEV3 Technologies home

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

  1. Save as lib/generatePassword.ts; it works in browsers and Node 19+ (global crypto).
  2. generate({ length: 20, symbols: true, avoidAmbiguous: true }) returns a string.
  3. Call entropyBits(options) to show a strength meter.
  4. 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.

Related snippets