1use rand::{RngCore, SeedableRng};
6use rand::rngs::{OsRng, StdRng};
7use rand_chacha::ChaCha20Rng;
8use sha2::{Digest, Sha256};
9
10pub const LOWERCASE: &str = "abcdefghijklmnopqrstuvwxyz";
12pub const UPPERCASE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
13pub const DIGITS: &str = "0123456789";
14pub const SYMBOLS: &str = r#"!@#$%^&*()-_=+[]{};:'\",.<>/?`~|\\"#;
15
16#[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()), }
37 }
38}
39
40pub struct PasswordGenerator {
42 charset: Vec<char>,
43 salt: Option<String>,
44}
45
46impl PasswordGenerator {
47 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 pub fn generate(&self, length: usize) -> String {
66 let mut buf = vec![0u8; length * 2];
68 OsRng.fill_bytes(&mut buf);
70
71 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 if let Some(salt) = &self.salt {
79 let mut hasher = Sha256::new();
81 hasher.update(salt.as_bytes());
82 let salt_hash = hasher.finalize();
83
84 for (i, byte) in salt_hash.iter().enumerate() {
86 if i < buf.len() {
87 buf[i] ^= *byte;
88 }
89 }
90 }
91
92 let hash = Sha256::digest(&buf);
94 let mut std_rng = StdRng::from_seed(hash.into());
95 std_rng.fill_bytes(&mut buf);
96
97 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}