Password Generator — TypeScript 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 TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
/**
* Password generation + entropy scoring - pure logic extracted from
* PasswordGenerator.tsx so it is unit-testable independent of React/crypto I/O.
* Generation still uses the Web Crypto CSPRNG (`crypto.getRandomValues`).
*/
export interface PasswordOptions {
length: number;
upper: boolean;
lower: boolean;
numbers: boolean;
symbols: boolean;
excludeAmbiguous: boolean;
}
export type StrengthVariant = 'danger' | 'accent' | 'success';
export interface StrengthTier {
label: string;
variant: StrengthVariant;
segments: 1 | 2 | 3 | 4 | 5;
}
const SETS = {
lower: 'abcdefghijklmnopqrstuvwxyz',
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
numbers: '0123456789',
symbols: '!@#$%^&*()-_=+[]{};:,.<>?/',
};
// Visually ambiguous characters removed when `excludeAmbiguous` is set.
const AMBIGUOUS = /[O0Il1|]/g;
/** Build the candidate charset from the selected option flags. */
export function buildCharset(o: PasswordOptions): string {
let cs = '';
if (o.lower) cs += SETS.lower;
if (o.upper) cs += SETS.upper;
if (o.numbers) cs += SETS.numbers;
if (o.symbols) cs += SETS.symbols;
if (o.excludeAmbiguous) cs = cs.replace(AMBIGUOUS, '');
return cs;
}
/**
* Map a stream of random uint32 draws to a uniform index in [0, n) via
* rejection sampling. Pure: inject `draw` for testability (production passes
* a CSPRNG draw). Removes the modulo bias of a plain `draw() % n`.
*/
export function unbiasedIndex(n: number, draw: () => number): number {
if (n <= 0) return 0;
const MAX = 0x100000000; // 2^32 (Uint32 range, exclusive)
const limit = MAX - (MAX % n); // largest multiple of n ≤ 2^32
let r: number;
do {
r = draw() >>> 0;
} while (r >= limit);
return r % n;
}
const csprngDraw = (): number => {
const buf = new Uint32Array(1);
crypto.getRandomValues(buf);
return buf[0];
};
/** Generate a cryptographically-random, unbiased password of `o.length` chars. */
export function generatePassword(o: PasswordOptions): string {
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, csprngDraw)];
return out;
}
/** Theoretical entropy in bits for a uniform-random password. */
export function entropyBits(length: number, charsetSize: number): number {
if (length <= 0 || charsetSize <= 1) return 0;
return length * Math.log2(charsetSize);
}
/** Classify bits into one of 5 tiers (1:1 with the 5 meter segments). */
export function strengthTier(bits: number): StrengthTier {
if (bits >= 100) return { label: 'very strong', variant: 'success', segments: 5 };
if (bits >= 70) return { label: 'strong', variant: 'success', segments: 4 };
if (bits >= 45) return { label: 'fair', variant: 'accent', segments: 3 };
if (bits >= 28) return { label: 'weak', variant: 'danger', segments: 2 };
return { label: 'very weak', variant: 'danger', segments: 1 };
}
export interface AttackScenario {
id: string;
label: string;
guessesPerSecond: number;
}
export const ATTACK_SCENARIOS: AttackScenario[] = [
{ 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) = 2^(bits-1) / guessesPerSecond. */
export function crackTimeSeconds(bits: number, guessesPerSecond: number): number {
return Math.pow(2, bits - 1) / guessesPerSecond;
}
// Each row: [factor, unit-name-AFTER-dividing]. Start from 'second'; dividing
// seconds by 60 yields minutes, by 60 again yields hours, etc. The unit label
// is the bucket you land in AFTER the division (NOT the one you leave).
// Misaligning labels (e.g. [60,'second']) is the classic off-by-one.
const CRACK_UNITS: [number, string][] = [
[60, 'minute'],
[60, 'hour'],
[24, 'day'],
[365, 'year'],
];
/** Human-readable span; collapses to order-of-magnitude beyond 10⁶ years. */
export function formatCrackTime(seconds: number): string {
if (!Number.isFinite(seconds) || seconds < 0) return '-';
if (seconds < 1) return '< 1 second';
let val = seconds;
let unit = 'second';
for (const [factor, name] of CRACK_UNITS) {
if (val < factor) break;
val /= factor;
unit = name;
}
if (unit === 'year' && val >= 1e6) return `10^${Math.round(Math.log10(val))} years`;
const n = Math.round(val);
return `${n.toLocaleString()} ${unit}${n === 1 ? '' : 's'}`;
}
/** Entropy bits per length across a range - the data for the crack-time-vs-length graph. */
export function entropyCurve(
charsetSize: number,
fromLength: number,
toLength: number,
step = 1,
): { length: number; bits: number }[] {
const out: { length: number; bits: number }[] = [];
for (let l = fromLength; l <= toLength; l += step) out.push({ length: l, bits: entropyBits(l, charsetSize) });
return out;
}
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 →