1pub fn md5_password(password: &str, username: &str, salt: [u8; 4]) -> String {
27 StoredHash::generate(password.as_bytes(), username).salted(salt)
28}
29
30fn to_hex_string(bytes: &[u8]) -> String {
32 let mut hex = String::with_capacity(bytes.len() * 2);
33 for &byte in bytes {
34 hex.push_str(&format!("{byte:02x}"));
35 }
36 hex
37}
38
39#[derive(Clone, Copy, Debug)]
41pub struct StoredHash {
42 pub hash: [u8; 16],
43}
44
45impl StoredHash {
46 pub fn generate(password: &[u8], username: &str) -> Self {
47 let mut hasher = md5::Context::new();
49 hasher.consume(password);
50 hasher.consume(username.as_bytes());
51 let first_hash = hasher.compute();
52 Self { hash: first_hash.0 }
53 }
54
55 pub fn matches(&self, client_exchange: &[u8], salt: [u8; 4]) -> bool {
56 let server_exchange = self.salted(salt);
57 constant_time_eq::constant_time_eq(client_exchange, server_exchange.as_bytes())
58 }
59
60 pub fn salted(&self, salt: [u8; 4]) -> String {
61 let this = &self;
62 let salt: &[u8; 4] = &salt;
63 let first_hash_hex = to_hex_string(&this.hash);
65
66 let mut hasher = md5::Context::new();
68 hasher.consume(first_hash_hex.as_bytes());
69 hasher.consume(salt);
70 let second_hash = hasher.compute();
71
72 let second_hash_hex = to_hex_string(&second_hash.0);
74
75 format!("md5{second_hash_hex}")
76 }
77}
78
79impl PartialEq for StoredHash {
80 fn eq(&self, other: &Self) -> bool {
81 constant_time_eq::constant_time_eq(&self.hash, &other.hash)
82 }
83}
84
85impl Eq for StoredHash {}