Skip to content

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 →