//! Генератор паролей и оценка их стойкости. use rand::Rng; use rand::rngs::OsRng; use rand::seq::SliceRandom; use serde::{Deserialize, Serialize}; const LOWER: &str = "abcdefghijklmnopqrstuvwxyz"; const UPPER: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const DIGITS: &str = "0123456789"; const SYMBOLS: &str = "!@#$%^&*()-_=+[]{};:,.?/~"; const AMBIGUOUS: &str = "Il1O0o|"; #[derive(Serialize, Deserialize, Clone, PartialEq)] #[serde(default)] pub struct GenOptions { pub length: usize, pub lower: bool, pub upper: bool, pub digits: bool, pub symbols: bool, pub avoid_ambiguous: bool, } impl Default for GenOptions { fn default() -> Self { Self { length: 20, lower: true, upper: true, digits: true, symbols: true, avoid_ambiguous: true } } } pub fn generate(o: &GenOptions) -> String { let pick = |set: &str| -> Vec { set.chars().filter(|c| !o.avoid_ambiguous || !AMBIGUOUS.contains(*c)).collect() }; let mut sets: Vec> = [(o.lower, LOWER), (o.upper, UPPER), (o.digits, DIGITS), (o.symbols, SYMBOLS)] .into_iter() .filter(|(on, _)| *on) .map(|(_, s)| pick(s)) .collect(); if sets.is_empty() { sets.push(pick(LOWER)); } let all: Vec = sets.concat(); let len = o.length.clamp(4, 128); let mut rng = OsRng; // По одному символу из каждого включённого набора, остальное — из общего. let mut out: Vec = sets.iter().map(|s| s[rng.gen_range(0..s.len())]).collect(); while out.len() < len { out.push(all[rng.gen_range(0..all.len())]); } out.truncate(len); out.shuffle(&mut rng); out.into_iter().collect() } #[derive(Clone, Copy)] pub struct Strength { /// 0 — очень слабый … 4 — отличный. pub score: u8, pub bits: f64, } impl Strength { pub fn label(&self) -> &'static str { ["Очень слабый", "Слабый", "Средний", "Хороший", "Отличный"][self.score as usize] } } /// Грубая оценка энтропии: размер алфавита × длина, со штрафом за повторы. pub fn strength(pw: &str) -> Strength { if pw.is_empty() { return Strength { score: 0, bits: 0.0 }; } let mut pool = 0u32; if pw.chars().any(|c| c.is_ascii_lowercase()) { pool += 26; } if pw.chars().any(|c| c.is_ascii_uppercase()) { pool += 26; } if pw.chars().any(|c| c.is_ascii_digit()) { pool += 10; } if pw.chars().any(|c| c.is_ascii_punctuation() || c == ' ') { pool += 33; } if !pw.is_ascii() { pool += 66; // кириллица и прочее } let mut unique: Vec = pw.chars().collect(); unique.sort_unstable(); unique.dedup(); let len = pw.chars().count() as f64; // Повторяющиеся символы почти не добавляют стойкости. let effective = (unique.len() as f64).max(len * 0.5).min(len); let bits = effective * (pool.max(1) as f64).log2(); let score = match bits { b if b < 28.0 => 0, b if b < 40.0 => 1, b if b < 60.0 => 2, b if b < 80.0 => 3, _ => 4, }; Strength { score, bits } } #[cfg(test)] mod tests { use super::*; #[test] fn respects_options() { let o = GenOptions { length: 32, symbols: false, ..Default::default() }; for _ in 0..50 { let p = generate(&o); assert_eq!(p.chars().count(), 32); assert!(p.chars().all(|c| c.is_ascii_alphanumeric())); assert!(p.chars().any(|c| c.is_ascii_digit())); assert!(!p.chars().any(|c| AMBIGUOUS.contains(c))); } } #[test] fn strength_ordering() { assert!(strength("123456").score < strength("Tr0ub4dor&3xK!9qZ").score); } }