rusty-pwgen 0.2.0

Generate pronounceable or random passwords from the OS CSPRNG — a Rust port of Theodore Ts'o's `pwgen` with strict-compat mode, deterministic `-H` reproducible mode (SHA256 + ChaCha20), and a typed library API.
Documentation
//! Secure (random) mode generation (FR-005, AD-001).
//!
//! Uniform random sampling from the active character set via [`crate::rng::RngSource`].

use crate::rng::RngSource;

/// Generate one secure-mode password by sampling `length` bytes uniformly
/// from `charset`. Panics if `charset` is empty (caller guarantees non-empty
/// via the builder's validation path).
pub fn generate<R: RngSource + ?Sized>(rng: &mut R, charset: &[u8], length: usize) -> String {
    assert!(
        !charset.is_empty(),
        "secure::generate requires non-empty charset"
    );
    let mut out = Vec::with_capacity(length);
    for _ in 0..length {
        let idx = rng.gen_range(charset.len() as u32) as usize;
        out.push(charset[idx]);
    }
    String::from_utf8(out).expect("charset is ASCII-only per FR-010")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::rng::OsRngSource;

    #[test]
    fn length_matches_request() {
        let mut rng = OsRngSource::new();
        let charset = b"abcdefghij";
        for n in 1..50 {
            let s = generate(&mut rng, charset, n);
            assert_eq!(s.len(), n);
        }
    }

    #[test]
    fn output_drawn_only_from_charset() {
        let mut rng = OsRngSource::new();
        let charset = b"ABC";
        for _ in 0..100 {
            let s = generate(&mut rng, charset, 16);
            assert!(s.bytes().all(|b| charset.contains(&b)));
        }
    }

    #[test]
    fn ambiguous_filter_no_lone_zero_or_one() {
        // FR-005 + FR-007: secure with ambiguous filter → no l 1 0 O I.
        let mut rng = OsRngSource::new();
        let charset = crate::charset::build(
            crate::charset::CharSetFlags {
                ambiguous_filter: true,
                ..Default::default()
            },
            &[],
        );
        for _ in 0..100 {
            let s = generate(&mut rng, &charset, 16);
            for byte in s.bytes() {
                assert!(!b"l1O0I".contains(&byte), "ambiguous char in output: {s:?}");
            }
        }
    }

    #[test]
    fn no_vowels_no_vowels() {
        // FR-008.
        let mut rng = OsRngSource::new();
        let charset = crate::charset::build(
            crate::charset::CharSetFlags {
                no_vowels: true,
                ..Default::default()
            },
            &[],
        );
        for _ in 0..100 {
            let s = generate(&mut rng, &charset, 16);
            for byte in s.bytes() {
                assert!(!b"aeiouAEIOU".contains(&byte), "vowel in output: {s:?}");
            }
        }
    }

    #[test]
    fn deterministic_with_seeded_rng() {
        use crate::rng::SeededSource;
        let seed = [7u8; 32];
        let mut a = SeededSource::from_seed(seed);
        let mut b = SeededSource::from_seed(seed);
        let cs = b"abcdef";
        for _ in 0..10 {
            assert_eq!(generate(&mut a, cs, 16), generate(&mut b, cs, 16));
        }
    }
}