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 →