passgenlib/
lib.rs

1pub mod gen_engine;
2pub mod lang;
3
4use crate::gen_engine::gen_engine::{LETTERS_CHARSET, NUM_CHARSET, SPEC_SYMB_CHARSET, U_LETTERS_CHARSET};
5use crate::lang::lang::{Language, StrengthTranslations};
6
7/// Main [Passgen] structure.
8///
9/// # Examples
10///
11/// You can create a token that includes lowercase letters and numbers up to 30 characters long:
12///
13/// ```
14/// use passgenlib::Passgen;
15/// let result = Passgen::new().set_enabled_letters(true).set_enabled_numbers(true).generate(30);
16/// ```
17///
18/// You can create a default strong password including all literals, numbers and symbols:
19///
20/// ```
21/// use passgenlib::Passgen;
22/// let result = Passgen::default().generate(12);
23/// ```
24///
25/// You can create a strong and usability password.
26/// Including all characters, but
27/// the first position in the password is a capital or small letter,
28/// the last position is the symbol.
29/// 🔸 Excluded ambiguous characters `"0oOiIlL1"`.
30///
31/// ```
32/// use passgenlib::Passgen;
33/// let result = Passgen::default_strong_and_usab().generate(8);
34/// ```
35/// You can create a set from your custom charset:
36///
37/// ```
38/// use passgenlib::Passgen;
39/// let result = Passgen::new().set_custom_charset("bla@.321").generate(8);
40/// ```
41///
42/// You can validate the existing password against the added rules:
43///
44/// ```
45/// use passgenlib::Passgen;
46/// let mut generator = Passgen::default();
47/// generator.set_enabled_letters(true).set_enabled_numbers(true);
48/// generator.set_password("MyP@ssw0rd");
49/// assert!(generator.validate_password());
50/// ```
51///
52/// You can get password strength score:
53///
54/// ```
55/// use passgenlib::Passgen;
56/// let mut generator = Passgen::default();
57/// generator.set_password("MyP@ssw0rd");
58/// let score = generator.password_strength_score();
59/// assert!(score >= 0 && score <= 100);
60/// ```
61///
62/// You can get password strength level in multiple languages:
63///
64/// ```
65/// use passgenlib::Passgen;
66/// use passgenlib::lang::lang::{Language, StrengthTranslations};
67/// let mut generator = Passgen::default();
68/// generator.set_password("MyP@ssw0rd");
69///
70/// // English (default)
71/// assert_eq!(generator.password_strength_level(), "Strong");
72///
73/// // Russian
74/// generator.set_language(Language::Russian);
75/// assert_eq!(generator.password_strength_level(), "Сильный");
76///
77/// // Spanish
78/// generator.set_language(Language::Spanish);
79/// assert_eq!(generator.password_strength_level(), "Fuerte");
80/// ```
81///
82/// You can generate password and immediately get its strength score:
83///
84/// ```
85/// use passgenlib::Passgen;
86/// let mut generator = Passgen::default();
87/// let password = generator.generate(12);
88///
89/// // The generated password is stored in the password field
90/// assert_eq!(generator.get_password(), password);
91///
92/// // You can immediately get the strength score
93/// let score = generator.password_strength_score();
94/// assert!(score > 0);
95/// ```
96pub struct Passgen {
97    /// Presence of letters.
98    pub enab_letters: bool,
99
100    /// Presence of a capital letters.
101    pub enab_u_letters: bool,
102
103    /// Presence of numeric characters.
104    pub enab_num: bool,
105
106    /// Presence of special characters.
107    pub enab_spec_symbs: bool,
108
109    /// Including all characters, but
110    /// the first position in the password is a capital or small letter,
111    /// the last position is the symbol. Excluded ambiguous characters `"0oOiIlL1"`.
112    ///
113    /// ⚠️ If this rule is enabled, the other consistency rules of the generating are not taken,
114    /// except for a rule `custom_charset`.
115    pub enab_strong_usab: bool,
116
117    /// User defined character set.
118    ///
119    /// ⚠️This set of characters will exclude all other rules except for a rule `"enab_strong_usab"`.
120    ///
121    /// ⚙️If `"enab_strong_usab"` on too then you can generate combined strong and usability result with custom charset.
122    pub custom_charset: &'static str,
123
124    /// Current password stored for validation and strength checking.
125    /// This field is automatically populated when using the `generate()` method
126    /// or manually set using the `set_password()` method.
127    pub password: String,
128
129    /// Language for password strength level descriptions.
130    /// Default is English.
131    pub language: Language,
132}
133
134impl Passgen {
135    /// Get an instance of `Passgen` without any rules.
136    pub fn new() -> Passgen {
137        Passgen {
138            enab_letters: false,
139            enab_u_letters: false,
140            enab_num: false,
141            enab_spec_symbs: false,
142            enab_strong_usab: false,
143            custom_charset: "",
144            password: String::new(),
145            language: Language::English,
146        }
147    }
148
149    /// Set default ruleset of `Passgen` to *"all simple rules are enabled"*.
150    pub fn default() -> Passgen {
151        Passgen {
152            enab_letters: true,
153            enab_u_letters: true,
154            enab_num: true,
155            enab_spec_symbs: true,
156            enab_strong_usab: false,
157            custom_charset: "",
158            password: String::new(),
159            language: Language::English,
160        }
161    }
162
163    /// Set default ruleset of `Passgen` to *"Strong & usability"*.
164    ///
165    /// Including all characters, but
166    /// the first position in the password is a capital or small letter,
167    /// the last position is the symbol. Excluded ambiguous characters `"0oOiIlL1"`.
168    ///
169    /// ⚠️ If this rule is enabled, the other consistency rules of the generating are not taken,
170    /// except for a rule `custom_charset`.
171    pub fn default_strong_and_usab() -> Passgen {
172        Passgen {
173            enab_letters: false,
174            enab_u_letters: false,
175            enab_num: false,
176            enab_spec_symbs: false,
177            custom_charset: "",
178            enab_strong_usab: true,
179            password: String::new(),
180            language: Language::English,
181        }
182    }
183
184    /// Set value of the field `enab_letters` for `Passgen`.
185    pub fn set_enabled_letters(&mut self, value: bool) -> &mut Passgen {
186        self.enab_letters = value;
187        self
188    }
189
190    /// Set value of the field `enab_u_letters` for `Passgen`.
191    pub fn set_enabled_uppercase_letters(&mut self, value: bool) -> &mut Passgen {
192        self.enab_u_letters = value;
193        self
194    }
195
196    /// Set value of the field `enab_num` for `Passgen`.
197    pub fn set_enabled_numbers(&mut self, value: bool) -> &mut Passgen {
198        self.enab_num = value;
199        self
200    }
201
202    /// Set value of the field `enab_spec_symbs` for `Passgen`.
203    pub fn set_enabled_spec_symbols(&mut self, value: bool) -> &mut Passgen {
204        self.enab_spec_symbs = value;
205        self
206    }
207
208    /// Set value of the field `enab_strong_usab` for `Passgen`.
209    ///
210    /// Including all characters, but
211    /// the first position in the password is a capital or small letter,
212    /// the last position is the symbol. Excluded ambiguous characters `"0oOiIlL1"`.
213    ///
214    /// ⚠️ If this rule is enabled, the other consistency rules of the generating are not taken,
215    /// except for a rule `custom_charset`.
216    pub fn set_enabled_strong_usab(&mut self, value: bool) -> &mut Passgen {
217        self.enab_strong_usab = value;
218        self
219    }
220
221    /// Set user defined character set.
222    /// You can use any Unicode characters and emoji. For example: abcABC123⭕➖❎⚫⬛n₼⁂🙂
223    ///
224    /// ⚠️This set of characters will exclude all other rules except for a rule `"enab_strong_usab"`.
225    ///
226    /// ⚙️If `"enab_strong_usab"` on too then you can generate combined strong and usability result with custom charset.
227    pub fn set_custom_charset(&mut self, value: &'static str) -> &mut Passgen {
228        self.custom_charset = value;
229        self
230    }
231
232    /// Set password for validation and strength checking.
233    /// This method is useful when you want to validate or check the strength
234    /// of an existing password.
235    pub fn set_password(&mut self, password: &str) -> &mut Passgen {
236        self.password = password.to_string();
237        self
238    }
239
240    /// Get current password.
241    /// Returns the password that was either generated using `generate()` method
242    /// or set using `set_password()` method.
243    pub fn get_password(&self) -> &str {
244        &self.password
245    }
246
247    /// Set language for password strength level descriptions.
248    pub fn set_language(&mut self, language: Language) -> &mut Passgen {
249        self.language = language;
250        self
251    }
252
253    /// Generate result. Argument "length" will not be less than 4.
254    /// The generated password is automatically stored in the `password` field
255    /// for immediate validation or strength checking.
256    pub fn generate(&mut self, length: u32) -> String {
257        if !self.is_ruleset_clean() {
258            let res_len = if length < 4 { 4 } else { length };
259
260            let mut pwd = self.generate_pass(res_len);
261
262            if self.custom_charset.len() == 0 {
263                while !self.validate_password_rules(pwd.clone()) {
264                    pwd = self.generate_pass(res_len);
265                }
266            }
267
268            self.password = pwd.clone();
269            pwd
270        } else {
271            self.password.clear();
272            "".to_string()
273        }
274    }
275
276    /// Validate if the current password matches the configured rules.
277    pub fn validate_password(&self) -> bool {
278        if self.password.is_empty() {
279            return false;
280        }
281
282        if self.custom_charset.len() > 0 {
283            // If custom charset is set, check if all characters are from that charset
284            for ch in self.password.chars() {
285                if !self.custom_charset.contains(ch) {
286                    return false;
287                }
288            }
289            return true;
290        }
291
292        self.validate_password_rules(self.password.clone())
293    }
294
295    /// Calculate password strength score (0-100).
296    /// Based on multiple factors: length, character variety, entropy, and common patterns.
297    pub fn password_strength_score(&self) -> u8 {
298        if self.password.is_empty() {
299            return 0;
300        }
301
302        let password = &self.password;
303        let length = password.len();
304
305        // Very short passwords get 0
306        if length < 4 {
307            return 0;
308        }
309
310        let mut score = 0i32;
311
312        // 1. Length score (max 25 points)
313        score += match length {
314            0..=4 => 0,
315            5..=6 => 5,
316            7..=8 => 10,
317            9..=10 => 15,
318            11..=12 => 20,
319            _ => 25,
320        };
321
322        // 2. Character variety analysis
323        let mut has_lowercase = false;
324        let mut has_uppercase = false;
325        let mut has_digits = false;
326        let mut has_special = false;
327        let mut unique_chars = std::collections::HashSet::new();
328
329        for ch in password.chars() {
330            unique_chars.insert(ch);
331
332            if ch.is_ascii_lowercase() {
333                has_lowercase = true;
334            } else if ch.is_ascii_uppercase() {
335                has_uppercase = true;
336            } else if ch.is_ascii_digit() {
337                has_digits = true;
338            } else if ch.is_ascii_punctuation() || "[]{}()<>".contains(ch) {
339                has_special = true;
340            }
341        }
342
343        // Count character types
344        let char_type_count = [has_lowercase, has_uppercase, has_digits, has_special]
345            .iter()
346            .filter(|&&x| x)
347            .count();
348
349        // Character variety score (max 25 points)
350        let mut variety_score = 0;
351        if has_lowercase { variety_score += 5; }
352        if has_uppercase { variety_score += 5; }
353        if has_digits { variety_score += 5; }
354        if has_special { variety_score += 10; }
355
356        // Bonus for multiple character types
357        match char_type_count {
358            2 => variety_score += 5,
359            3 => variety_score += 10,
360            4 => variety_score += 15,
361            _ => {}
362        }
363
364        // Cap variety score at 25
365        score += variety_score.min(25);
366
367        // 3. Unique characters ratio (max 20 points)
368        let uniqueness_ratio = unique_chars.len() as f32 / length as f32;
369        score += (uniqueness_ratio * 20.0) as i32;
370
371        // 4. Check for exact matches with weak passwords (immediate 0)
372        let weak_passwords = [
373            "password", "123456", "qwerty", "admin", "welcome",
374            "12345678", "123456789", "12345", "1234", "111111",
375        ];
376
377        let lower_pwd = password.to_lowercase();
378        for weak in &weak_passwords {
379            if lower_pwd == *weak {
380                return 0;
381            }
382        }
383
384        // 5. Check for containing weak patterns
385        let weak_patterns = ["password", "123", "qwerty", "admin", "letmein"];
386
387        let mut pattern_penalty = 0;
388        for pattern in &weak_patterns {
389            if lower_pwd.contains(pattern) {
390                pattern_penalty += 15;
391            }
392        }
393
394        // 6. Penalties for weak patterns
395        let mut penalty = pattern_penalty;
396        let chars: Vec<char> = password.chars().collect();
397
398        // Check for sequential characters
399        for i in 0..chars.len().saturating_sub(2) {
400            let c1 = chars[i] as u32;
401            let c2 = chars[i + 1] as u32;
402            let c3 = chars[i + 2] as u32;
403
404            if c2 == c1 + 1 && c3 == c2 + 1 {
405                penalty += 10;
406                break;
407            }
408        }
409
410        // Check for repeated characters
411        for i in 0..chars.len().saturating_sub(2) {
412            if chars[i] == chars[i + 1] && chars[i] == chars[i + 2] {
413                penalty += 10;
414                break;
415            }
416        }
417
418        // Penalty for too few character types
419        if char_type_count < 2 {
420            penalty += 10;
421        }
422
423        // Apply penalty (max 30 points penalty)
424        score -= penalty.min(30);
425
426        // 7. Simple entropy estimation (max 10 points)
427        // This is a simplified calculation to avoid over-scoring
428        let mut charset_size = 0;
429        if has_lowercase { charset_size += 26; }
430        if has_uppercase { charset_size += 26; }
431        if has_digits { charset_size += 10; }
432        if has_special { charset_size += 32; }
433
434        if charset_size > 0 {
435            // Very conservative entropy calculation
436            // We cap it at 10 points to avoid over-scoring
437            let entropy_per_char = (charset_size as f32).log2();
438            let total_entropy = length as f32 * entropy_per_char;
439
440            // Normalize to 0-10 points (very conservative)
441            let entropy_score = (total_entropy / 10.0).min(10.0);
442            score += entropy_score as i32;
443        }
444
445        // Ensure score is between 0 and 100
446        score = score.max(0).min(100);
447
448        score as u8
449    }
450
451    /// Get password strength level description in the selected language.
452    pub fn password_strength_level(&self) -> &'static str {
453        let score = self.password_strength_score();
454        StrengthTranslations::get_level(self.language, score)
455    }
456
457    fn is_ruleset_clean(&self) -> bool {
458        !self.enab_letters
459            && !self.enab_u_letters
460            && !self.enab_num
461            && !self.enab_spec_symbs
462            && !self.enab_strong_usab
463            && self.custom_charset.len() == 0
464    }
465
466    fn validate_password_rules(&self, pass: String) -> bool {
467        let check_to_available_for = |symbols: &str| -> bool {
468            let mut res = false;
469            for ch in pass.chars() {
470                if symbols.contains(ch) {
471                    res = true;
472                    break;
473                }
474            }
475            res
476        };
477
478        // compliance check
479        if self.enab_letters || self.enab_strong_usab {
480            if !check_to_available_for(LETTERS_CHARSET) {
481                return false;
482            }
483        }
484        if self.enab_u_letters || self.enab_strong_usab {
485            if !check_to_available_for(U_LETTERS_CHARSET) {
486                return false;
487            }
488        }
489        if self.enab_num || self.enab_strong_usab {
490            if !check_to_available_for(NUM_CHARSET) {
491                return false;
492            }
493        }
494        if self.enab_spec_symbs || self.enab_strong_usab {
495            if !check_to_available_for(SPEC_SYMB_CHARSET) {
496                return false;
497            }
498        }
499        true
500    }
501}
502
503#[cfg(test)]
504mod tests;