rusty-pwgen 0.1.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
//! RNG sources for rusty-pwgen.
//!
//! Two implementations:
//! - [`OsRngSource`] — wraps [`rand::rngs::OsRng`] for non-reproducible generation.
//! - [`SeededSource`] — wraps [`rand_chacha::ChaCha20Rng`] for deterministic `-H` mode.

use rand::RngCore;
use rand_chacha::ChaCha20Rng;
use rand_chacha::rand_core::SeedableRng;

/// Abstract RNG source. Object-safe so the same generator code drives both
/// modes via a trait object — see [`crate::builder::Pwgen`].
pub trait RngSource {
    /// Generate one byte uniformly from `0..=255`.
    fn next_u8(&mut self) -> u8;

    /// Generate one integer uniformly from `0..upper` (exclusive). Panics on `upper == 0`.
    fn gen_range(&mut self, upper: u32) -> u32 {
        assert!(upper > 0, "gen_range upper must be > 0");
        // Rejection sampling for bias-free uniform mapping.
        let bound = (u32::MAX / upper) * upper;
        loop {
            let v = self.next_u32();
            if v < bound {
                return v % upper;
            }
        }
    }

    /// Generate one u32. Default implementation composes from `next_u8`.
    fn next_u32(&mut self) -> u32 {
        let bytes = [
            self.next_u8(),
            self.next_u8(),
            self.next_u8(),
            self.next_u8(),
        ];
        u32::from_le_bytes(bytes)
    }
}

/// OS CSPRNG source (FR-001). Wraps `OsRng` which pulls from the OS's
/// cryptographically-secure RNG (`getrandom(2)` on Linux, `BCryptGenRandom`
/// on Windows, `CCRandomGenerateBytes` on macOS).
pub struct OsRngSource;

impl Default for OsRngSource {
    fn default() -> Self {
        Self
    }
}

impl OsRngSource {
    pub fn new() -> Self {
        Self
    }
}

impl RngSource for OsRngSource {
    fn next_u8(&mut self) -> u8 {
        let mut buf = [0u8; 1];
        rand::rngs::OsRng.fill_bytes(&mut buf);
        buf[0]
    }

    fn next_u32(&mut self) -> u32 {
        rand::rngs::OsRng.next_u32()
    }
}

/// Deterministic ChaCha20Rng seeded by the 32-byte SHA256 digest from `-H` mode
/// (FR-028, AD-002, AD-015). The seed-to-bitstream mapping is locked at v0.1.0
/// per FR-067 — any change is a MAJOR-bump-only change.
pub struct SeededSource {
    inner: ChaCha20Rng,
}

impl SeededSource {
    pub fn from_seed(seed: [u8; 32]) -> Self {
        Self {
            inner: ChaCha20Rng::from_seed(seed),
        }
    }
}

impl RngSource for SeededSource {
    fn next_u8(&mut self) -> u8 {
        let mut buf = [0u8; 1];
        self.inner.fill_bytes(&mut buf);
        buf[0]
    }

    fn next_u32(&mut self) -> u32 {
        self.inner.next_u32()
    }
}

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

    #[test]
    fn seeded_source_is_deterministic() {
        // FR-067: same seed → identical bitstream.
        let seed = [42u8; 32];
        let mut a = SeededSource::from_seed(seed);
        let mut b = SeededSource::from_seed(seed);
        for _ in 0..1000 {
            assert_eq!(a.next_u8(), b.next_u8());
        }
    }

    #[test]
    fn os_rng_produces_varied_output() {
        // Sanity check: OsRng produces non-constant output.
        let mut rng = OsRngSource::new();
        let first = rng.next_u8();
        let mut all_same = true;
        for _ in 0..256 {
            if rng.next_u8() != first {
                all_same = false;
                break;
            }
        }
        assert!(!all_same, "OsRng should produce varied output");
    }

    #[test]
    fn gen_range_stays_in_bounds() {
        let mut rng = OsRngSource::new();
        for _ in 0..1000 {
            let v = rng.gen_range(10);
            assert!(v < 10);
        }
    }

    #[test]
    #[should_panic]
    fn gen_range_zero_upper_panics() {
        let mut rng = OsRngSource::new();
        let _ = rng.gen_range(0);
    }
}