gen_pass/
lib.rs

1//! Secure password generation library
2//! Provides flexible password generator with multiple random sources.
3//! Comments in English per user preference.
4
5use rand::{RngCore, SeedableRng};
6use rand::rngs::{OsRng, StdRng};
7use rand_chacha::ChaCha20Rng;
8use sha2::{Digest, Sha256};
9
10/// Character sets
11pub const LOWERCASE: &str = "abcdefghijklmnopqrstuvwxyz";
12pub const UPPERCASE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
13pub const DIGITS: &str = "0123456789";
14pub const SYMBOLS: &str = r#"!@#$%^&*()-_=+[]{};:'\",.<>/?`~|\\"#;
15
16/// Password generator configuration
17#[derive(Debug, Clone)]
18pub struct PassConfig {
19    pub length: usize,
20    pub use_lowercase: bool,
21    pub use_uppercase: bool,
22    pub use_digits: bool,
23    pub use_symbols: bool,
24    pub salt: Option<String>,
25}
26
27impl Default for PassConfig {
28    fn default() -> Self {
29        Self {
30            length: 16,
31            use_lowercase: true,
32            use_uppercase: true,
33            use_digits: true,
34            use_symbols: true,
35            salt: Some("suenot".to_string()), // Easter egg with author's nickname
36        }
37    }
38}
39
40/// Core password generator structure
41pub struct PasswordGenerator {
42    charset: Vec<char>,
43    salt: Option<String>,
44}
45
46impl PasswordGenerator {
47    /// Create new generator from config
48    pub fn from_config(cfg: &PassConfig) -> anyhow::Result<Self> {
49        let mut charset = String::new();
50        if cfg.use_lowercase { charset.push_str(LOWERCASE); }
51        if cfg.use_uppercase { charset.push_str(UPPERCASE); }
52        if cfg.use_digits    { charset.push_str(DIGITS); }
53        if cfg.use_symbols   { charset.push_str(SYMBOLS); }
54
55        if charset.is_empty() {
56            anyhow::bail!("Character set is empty; enable at least one category");
57        }
58        Ok(Self { 
59            charset: charset.chars().collect(),
60            salt: cfg.salt.clone(),
61        })
62    }
63
64    /// Generate password using mixed entropy from multiple RNGs and SHA-256
65    pub fn generate(&self, length: usize) -> String {
66        // Collect raw random bytes from multiple sources
67        let mut buf = vec![0u8; length * 2];
68        // OsRng (system entropy)
69        OsRng.fill_bytes(&mut buf);
70
71        // ChaCha20 seeded with OsRng-derived seed for extra unpredictability
72        let mut seed = [0u8; 32];
73        OsRng.fill_bytes(&mut seed);
74        let mut chacha = ChaCha20Rng::from_seed(seed);
75        chacha.fill_bytes(&mut buf);
76
77        // Apply salt if provided
78        if let Some(salt) = &self.salt {
79            // Create a hash of the salt
80            let mut hasher = Sha256::new();
81            hasher.update(salt.as_bytes());
82            let salt_hash = hasher.finalize();
83            
84            // XOR the salt hash with the buffer for additional entropy
85            for (i, byte) in salt_hash.iter().enumerate() {
86                if i < buf.len() {
87                    buf[i] ^= *byte;
88                }
89            }
90        }
91
92        // StdRng seeded with SHA256 of previous buffer
93        let hash = Sha256::digest(&buf);
94        let mut std_rng = StdRng::from_seed(hash.into());
95        std_rng.fill_bytes(&mut buf);
96
97        // Convert random bytes to password characters
98        buf.iter()
99            .take(length)
100            .map(|b| {
101                let idx = (*b as usize) % self.charset.len();
102                self.charset[idx]
103            })
104            .collect()
105    }
106}