Skip to content

Password Generator — JavaScript source

Generate cryptographically-random passwords with a CSPRNG using rejection sampling (no modulo bias). Shows live entropy in bits, a 5-tier strength meter, average offline-GPU crack time, and a Pro mode with the entropy formula, a crack-time-vs-length curve, and a 4-scenario attack table. Everything runs locally - nothing is sent anywhere.

This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

/**
 * password-generator - JavaScript polyglot showcase port.
 *
 * Cryptographically-secure password generation with entropy scoring and an
 * average crack-time model. Pure logic only - no DOM, no I/O - so it can be
 * lifted into any runtime that exposes the Web Crypto API (browsers, Deno,
 * Node >= 19, Cloudflare Workers).
 *
 * Ported from the canonical TypeScript implementation at
 *   src/lib/password.ts
 * to keep CosmoDev's "code is the hero" tool pages in lock-step across the
 * supported languages.
 *
 * This file is display source - part of CosmoDev's polyglot tool pages
 * (dev.cosmolabs.org). License: MIT.
 */

// Character classes that make up a candidate alphabet. Kept as plain string
// pools (not arrays) so a charset is built by cheap concatenation.
const LOWER = 'abcdefghijklmnopqrstuvwxyz';
const UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const NUMBERS = '0123456789';
const SYMBOLS = '!@#$%^&*()-_=+[]{};:,.<>?/';

// Glyphs that are easy to confuse by eye (O vs 0, I vs l vs 1, and a stray
// pipe). Removed from the pool when the caller opts into the
// "exclude ambiguous" hardening. Mirrors the TS regex /[O0Il1|]/g - note the
// set is {O, 0, I, l, 1, |}; lowercase 'o' and uppercase 'L' are NOT removed.
const AMBIGUOUS = new Set(['O', '0', 'I', 'l', '1', '|']);

/**
 * @typedef {Object} PasswordOptions
 * @property {number}  length           Requested password length (chars).
 * @property {boolean} upper            Include A-Z.
 * @property {boolean} lower            Include a-z.
 * @property {boolean} numbers          Include 0-9.
 * @property {boolean} symbols          Include the punctuation set above.
 * @property {boolean} excludeAmbiguous Drop O/0/I/l/1/| when true.
 */

/**
 * Build the candidate alphabet from the selected option flags.
 *
 * Order (lower -> upper -> digits -> symbols) is cosmetic: every draw picks a
 * uniform index over the whole pool, so ordering affects only *which*
 * characters are available, never their relative frequency.
 *
 * @param {PasswordOptions} o
 * @returns {string}
 */
export function buildCharset(o) {
  let cs = '';
  if (o.lower) cs += LOWER;
  if (o.upper) cs += UPPER;
  if (o.numbers) cs += NUMBERS;
  if (o.symbols) cs += SYMBOLS;
  // Filter rather than regex-replace so the ambiguous set is one named
  // declaration the reader can audit at a glance.
  if (o.excludeAmbiguous) cs = [...cs].filter((c) => !AMBIGUOUS.has(c)).join('');
  return cs;
}

/**
 * Map the CSPRNG to a uniform index in [0, n) via rejection sampling.
 *
 * `limit` is the largest multiple of n that fits in the 2^32 Uint32 range; any
 * draw at or above it is thrown away and redrawn, so the survivors map evenly
 * onto [0, n). This removes the modulo bias of a plain `draw() % n`, which
 * over-represents the low buckets when 2^32 is not an exact multiple of n.
 * Mirrors `unbiasedIndex` in the TS lib.
 *
 * @param {number} n   Pool size (must be > 0).
 * @returns {number}   Uniform index in [0, n).
 */
function unbiasedIndex(n) {
  const MAX = 0x100000000; // 2^32 (Uint32 range, exclusive)
  const limit = MAX - (MAX % n);
  const buf = new Uint32Array(1);
  let r;
  do {
    crypto.getRandomValues(buf); // Web Crypto CSPRNG (OS entropy source).
    r = buf[0];
  } while (r >= limit);
  return r % n;
}

/**
 * Generate a cryptographically-random, unbiased password.
 *
 * One CSPRNG draw per character, reduced via rejection sampling so every index
 * is uniformly distributed over the charset. Returns '' when no class is
 * enabled or length < 1.
 *
 * @param {PasswordOptions} o
 * @returns {string}
 */
export function generatePassword(o) {
  const cs = buildCharset(o);
  if (!cs || o.length < 1) return '';
  let out = '';
  for (let i = 0; i < o.length; i++) out += cs[unbiasedIndex(cs.length)];
  return out;
}

/**
 * Theoretical entropy (bits) of a uniform-random password - the Shannon
 * formula: length * log2(|alphabet|). Returns 0 for a non-positive length or a
 * charset size <= 1.
 *
 * @param {number} length
 * @param {number} charsetSize
 * @returns {number}
 */
export function entropyBits(length, charsetSize) {
  if (length <= 0 || charsetSize <= 1) return 0;
  return length * Math.log2(charsetSize);
}

// Entropy tier thresholds (bits), 1:1 with the 5 strength-meter segments.
const TIER_VERY_STRONG = 100;
const TIER_STRONG = 70;
const TIER_FAIR = 45;
const TIER_WEAK = 28;

/**
 * @typedef {Object} PasswordStrength
 * @property {string} label                          Tier label for display.
 * @property {'danger'|'accent'|'success'} variant   UI colour bucket.
 * @property {1|2|3|4|5} segments                     Meter segment count.
 */

/**
 * Classify an entropy value into one of five tiers, 1:1 with the meter.
 *
 * @param {number} bits
 * @returns {PasswordStrength}
 */
export function strengthTier(bits) {
  if (bits >= TIER_VERY_STRONG) return { label: 'very strong', variant: 'success', segments: 5 };
  if (bits >= TIER_STRONG)      return { label: 'strong',       variant: 'success', segments: 4 };
  if (bits >= TIER_FAIR)        return { label: 'fair',         variant: 'accent',  segments: 3 };
  if (bits >= TIER_WEAK)        return { label: 'weak',         variant: 'danger',  segments: 2 };
  return { label: 'very weak', variant: 'danger', segments: 1 };
}

/**
 * @typedef {Object} AttackScenario
 * @property {string} id
 * @property {string} label
 * @property {number} guessesPerSecond
 */

/**
 * The four documented attack models, from a throttled online attacker to a
 * fast offline GPU rig. Guess rates match the TS ATTACK_SCENARIOS constant.
 */
export const ATTACK_SCENARIOS = [
  { id: 'online-throttled', label: 'online, throttled (100/h)', guessesPerSecond: 100 / 3600 },
  { id: 'online',           label: 'online, no throttle (10/s)', guessesPerSecond: 10 },
  { id: 'offline-slow',     label: 'offline, slow hash (10⁴/s)', guessesPerSecond: 1e4 },
  { id: 'offline-fast',     label: 'offline, fast GPU (10¹⁰/s)', guessesPerSecond: 1e10 },
];

/**
 * Average time to crack (seconds). The `2^(bits-1)` averages over the keyspace
 * - on average half the space is searched before the secret is found - so this
 * is the EXPECTED time, not the worst-case full-keyspace search (`2^bits / rate`).
 *
 * @param {number} bits
 * @param {number} guessesPerSecond
 * @returns {number}
 */
export function crackTimeSeconds(bits, guessesPerSecond) {
  return Math.pow(2, bits - 1) / guessesPerSecond;
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →