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;