torrust_tracker_deployer_lib/shared/secrets/
random.rs1use rand::seq::IndexedRandom as _;
4use rand::seq::SliceRandom as _;
5use rand::Rng as _;
6
7use super::password::Password;
8
9const LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
10const UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
11const DIGIT: &[u8] = b"0123456789";
12const SYMBOL: &[u8] = b"!@#$%^&*()-_=+[]{}<>?";
16
17fn full_charset() -> Vec<u8> {
18 [LOWER, UPPER, DIGIT, SYMBOL].concat()
19}
20
21#[must_use]
39pub fn generate_random_password() -> Password {
40 let mut rng = rand::rng();
41
42 let mut password: Vec<u8> = vec![
44 *LOWER
45 .choose(&mut rng)
46 .expect("LOWER charset is non-empty; selection must succeed"),
47 *UPPER
48 .choose(&mut rng)
49 .expect("UPPER charset is non-empty; selection must succeed"),
50 *DIGIT
51 .choose(&mut rng)
52 .expect("DIGIT charset is non-empty; selection must succeed"),
53 *SYMBOL
54 .choose(&mut rng)
55 .expect("SYMBOL charset is non-empty; selection must succeed"),
56 ];
57
58 let charset = full_charset();
60 for _ in password.len()..32 {
61 let idx = rng.random_range(0..charset.len());
62 password.push(charset[idx]);
63 }
64
65 password.shuffle(&mut rng);
67
68 Password::new(
70 String::from_utf8(password)
71 .expect("Generated password contains only valid ASCII; UTF-8 conversion must succeed"),
72 )
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn it_should_generate_password_satisfying_mysql_medium_policy() {
81 for _ in 0..100 {
82 let pwd = generate_random_password();
83 let s = pwd.expose_secret();
84
85 assert_eq!(s.len(), 32, "password must be 32 characters");
86 assert!(s.chars().any(char::is_uppercase), "must contain uppercase");
87 assert!(s.chars().any(char::is_lowercase), "must contain lowercase");
88 assert!(s.chars().any(|c| c.is_ascii_digit()), "must contain digit");
89 assert!(
90 s.chars().any(|c| "!@#$%^&*()-_=+[]{}<>?".contains(c)),
91 "must contain symbol"
92 );
93 assert!(s.is_ascii(), "must be ASCII (safe in .env files and shell)");
94 }
95 }
96
97 #[test]
98 fn it_should_generate_unique_passwords() {
99 let a = generate_random_password();
100 let b = generate_random_password();
101 assert_ne!(
102 a.expose_secret(),
103 b.expose_secret(),
104 "two consecutive calls must not produce the same password"
105 );
106 }
107}