Password Generator — Rust 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 Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! # password-generator — Rust polyglot showcase port.
//!
//! Cryptographically-secure password generation with entropy scoring and an
//! average crack-time model. Mirrors the canonical TypeScript at
//! `src/lib/password.ts` so the CosmoDev tool pages render equivalent logic
//! across every supported language.
//!
//! This file is display source — part of CosmoDev's polyglot tool pages
//! (dev.cosmolabs.org). License: MIT.
//!
//! ## CSPRNG dependency
//! Rust ships randomness out of the stdlib only via host interfaces; the
//! idiomatic, audited source is the [`rand`](https://crates.io/crates/rand)
//! crate. [`OsRng`] draws directly from the operating system's entropy
//! source (getrandom / SecRandomCopyBytes) — the correct primitive for
//! secrets. Add to `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! rand = "0.8"
//! ```
use rand::rngs::OsRng;
use rand::RngCore;
/// Generator configuration. Field-for-field compatible with the TS
/// `PasswordOptions` interface (snake_cased to match Rust convention).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PasswordOptions {
pub length: usize,
pub upper: bool,
pub lower: bool,
pub numbers: bool,
pub symbols: bool,
pub exclude_ambiguous: bool,
}
/// Tier assessment returned to the UI. `variant` selects the colour bucket and
/// is a `&'static str` because the label set is closed and tiny.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PasswordStrength {
pub label: &'static str,
pub variant: &'static str, // "danger" | "accent" | "success"
pub segments: u8, // 1..=5
}
/// One attack model's guess rate.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AttackScenario {
pub id: &'static str,
pub label: &'static str,
pub guesses_per_second: f64,
}
/// Character pools, kept as `&'static str` so a charset is concatenation.
const LOWER: &str = "abcdefghijklmnopqrstuvwxyz";
const UPPER: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const NUMBERS: &str = "0123456789";
const SYMBOLS: &str = "!@#$%^&*()-_=+[]{};:,.<>?/";
/// 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 does not change the
/// distribution — only which characters are *available*.
///
/// `String::retain` is used for the ambiguous-filter because it mutates in
/// place and expresses intent more directly than a `chars().filter().collect()`.
pub fn build_charset(o: &PasswordOptions) -> String {
let mut cs = String::new();
if o.lower {
cs.push_str(LOWER);
}
if o.upper {
cs.push_str(UPPER);
}
if o.numbers {
cs.push_str(NUMBERS);
}
if o.symbols {
cs.push_str(SYMBOLS);
}
if o.exclude_ambiguous {
// The exact set behind the TS regex /[O0Il1|]/g — {O,0,I,l,1,|}.
cs.retain(|c| !matches!(c, 'O' | '0' | 'I' | 'l' | '1' | '|'));
}
cs
}
/// Draw a uniform index in `[0, n)` from the OS CSPRNG via rejection sampling.
///
/// `limit` is the largest multiple of `n` that fits in the 2^32 `u32` range;
/// any draw at or above it is thrown away and redrawn, so the survivors reduce
/// evenly onto `[0, n)`. This removes the modulo bias of a plain `draw % n`
/// (which over-weights the low buckets when 2^32 is not a multiple of `n`).
/// Mirrors `unbiasedIndex` in the TS lib.
fn unbiased_index(n: usize) -> usize {
let max = 1u64 << 32; // 2^32 (u32 range, exclusive)
let limit = max - (max % n as u64);
loop {
let r = OsRng.next_u32() as u64; // uniform in [0, 2^32)
if r < limit {
return (r % n as u64) as usize;
}
}
}
/// Generate a cryptographically-random, unbiased password of `o.length` chars.
///
/// Each index is drawn with rejection sampling over [`OsRng`] (the OS CSPRNG),
/// so every position is uniformly distributed over the charset. Returns an
/// empty [`String`] when no class is enabled or `length` is zero.
pub fn generate_password(o: &PasswordOptions) -> String {
let cs = build_charset(o);
if cs.is_empty() || o.length == 0 {
return String::new();
}
// Collect once into a Vec<char> so indexing is O(1) per draw; charset
// strings are ASCII so char boundaries never split a glyph.
let pool: Vec<char> = cs.chars().collect();
let mut out = String::with_capacity(o.length);
for _ in 0..o.length {
out.push(pool[unbiased_index(pool.len())]);
}
out
}
/// Theoretical entropy (bits) of a uniform-random password — the Shannon
/// formula: `length * log2(|alphabet|)`. Returns `0.0` for a zero length or a
/// charset size <= 1.
pub fn entropy_bits(length: usize, charset_size: usize) -> f64 {
if length == 0 || charset_size <= 1 {
return 0.0;
}
length as f64 * (charset_size as f64).log2()
}
// Entropy tier thresholds (bits), 1:1 with the 5 strength-meter segments.
const TIER_VERY_STRONG: f64 = 100.0;
const TIER_STRONG: f64 = 70.0;
const TIER_FAIR: f64 = 45.0;
const TIER_WEAK: f64 = 28.0;
/// Classify an entropy value into one of five tiers, 1:1 with the meter.
pub fn strength_tier(bits: f64) -> PasswordStrength {
if bits >= TIER_VERY_STRONG {
PasswordStrength { label: "very strong", variant: "success", segments: 5 }
} else if bits >= TIER_STRONG {
PasswordStrength { label: "strong", variant: "success", segments: 4 }
} else if bits >= TIER_FAIR {
PasswordStrength { label: "fair", variant: "accent", segments: 3 }
} else if bits >= TIER_WEAK {
PasswordStrength { label: "weak", variant: "danger", segments: 2 }
} else {
PasswordStrength { label: "very weak", variant: "danger", segments: 1 }
}
}
/// The four documented attack models, from a throttled online attacker to a
/// fast offline GPU rig. Guess rates match the TS `ATTACK_SCENARIOS` constant.
pub const ATTACK_SCENARIOS: &[AttackScenario] = &[
AttackScenario { id: "online-throttled", label: "online, throttled (100/h)", guesses_per_second: 100.0 / 3600.0 },
AttackScenario { id: "online", label: "online, no throttle (10/s)", guesses_per_second: 10.0 },
AttackScenario { id: "offline-slow", label: "offline, slow hash (10⁴/s)", guesses_per_second: 1e4 },
AttackScenario { id: "offline-fast", label: "offline, fast GPU (10¹⁰/s)", guesses_per_second: 1e10 },
];
/// Average time to crack (seconds). `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`).
pub fn crack_time_seconds(bits: f64, guesses_per_second: f64) -> f64 {
2f64.powf(bits - 1.0) / guesses_per_second
}
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 →