1#[derive(Debug, PartialEq)]
24pub enum CheckHashResult {
25 Valid,
26 PasswordTooLong,
27 InvalidHash(InvalidHash),
28 Invalid,
29}
30
31#[derive(Debug, PartialEq)]
33pub enum InvalidHash {
34 BadLength,
35 UnsupportedHashType,
36 InvalidRounds,
37 InvalidBase64(base64::DecodeError),
38}
39
40#[derive(Debug)]
42pub struct PhpbbHash<'a> {
43 hash_type: &'a str,
44 rounds: usize,
45 salt: &'a str,
46 hashed: &'a str,
47}
48
49static ALPHABET: &str = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
51
52pub fn parse_hash(salted_hash: &str) -> Result<PhpbbHash, InvalidHash> {
70 if salted_hash.len() != 34 {
72 return Err(InvalidHash::BadLength);
73 }
74
75 let hash_type = &salted_hash[0..3];
77 if hash_type != "$H$" {
78 return Err(InvalidHash::UnsupportedHashType);
79 };
80
81 let rounds = match ALPHABET.find(salted_hash.chars().nth(3).unwrap()) {
83 None => return Err(InvalidHash::InvalidRounds),
84 Some(offset) if offset < 7 || offset > 30 => return Err(InvalidHash::InvalidRounds),
85 Some(offset) => 1 << offset,
86 };
87
88 let salt = &salted_hash[4..12];
90 let hashed = &salted_hash[12..];
91
92 Ok(PhpbbHash {
93 hash_type,
94 rounds,
95 salt,
96 hashed,
97 })
98}
99
100fn decode64(val: &[u8]) -> Result<Vec<u8>, base64::DecodeError> {
105 let len = val.len();
107 let bytes = base64::decode_config(
108 std::iter::repeat(b'.')
109 .take(3 - len % 3)
111 .chain(val.iter().cloned().rev())
112 .collect::<Vec<_>>(),
113 base64::CRYPT,
114 )?
115 .iter()
116 .rev()
118 .take(16)
119 .copied()
120 .collect::<Vec<_>>();
121
122 Ok(bytes)
123}
124
125pub fn check_hash(salted_hash: &str, password: &str) -> CheckHashResult {
127 if password.len() > 4096 {
129 return CheckHashResult::PasswordTooLong;
130 }
131 let password_bytes = password.as_bytes();
132 let password_bytes_len = password_bytes.len();
133
134 let parsed = match parse_hash(salted_hash) {
136 Ok(p) => p,
137 Err(e) => return CheckHashResult::InvalidHash(e),
138 };
139
140 let decoded_hashed = match decode64(parsed.hashed.as_bytes()) {
142 Ok(d) => d,
143 Err(e) => return CheckHashResult::InvalidHash(InvalidHash::InvalidBase64(e)),
144 };
145
146 let mut buf: Vec<u8> = Vec::with_capacity(8 + password_bytes_len);
148 buf.extend_from_slice(parsed.salt.as_bytes());
149 buf.extend_from_slice(password.as_bytes());
150 let mut hash = md5::compute(&buf);
151
152 for _ in 0..parsed.rounds {
155 let mut buf: Vec<u8> = Vec::with_capacity(16 + password_bytes_len);
156 buf.extend_from_slice(&hash.0);
157 buf.extend_from_slice(password_bytes);
158 hash = md5::compute(&buf);
159 }
160
161 if hash.0.as_ref() == decoded_hashed {
162 CheckHashResult::Valid
163 } else {
164 CheckHashResult::Invalid
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[derive(Debug)]
173 struct TestCase {
174 encoded_hash: &'static str,
175 password: &'static str,
176 result: CheckHashResult,
177 }
178
179 #[test]
180 fn test_validation() {
181 let test_cases = [
182 TestCase {
183 encoded_hash: "$H$9/O41.qQjQNlleivjbckbSNpfS4xgh0",
184 password: "pass1234",
185 result: CheckHashResult::Valid,
186 },
187 TestCase {
188 encoded_hash: "$H$9PoEptdBNUJZuamBBKOr/KPdi1ZmSw1",
189 password: "pass1234",
190 result: CheckHashResult::Valid,
191 },
192 TestCase {
193 encoded_hash: "$H$94VS2e40wcTQ38TK2P2yBc0TnmMfLC1",
194 password: "pass1234",
195 result: CheckHashResult::Valid,
196 },
197 TestCase {
198 encoded_hash: "$H$9/O41.qQjQNlleivjbckbSNpfS4xgh0",
199 password: "pass1235",
200 result: CheckHashResult::Invalid,
201 },
202 TestCase {
203 encoded_hash: "$H$9/O41.qQjQNlleivjbckbSNpfS4xgh012",
204 password: "pass1234",
205 result: CheckHashResult::InvalidHash(InvalidHash::BadLength),
206 },
207 TestCase {
208 encoded_hash: "$X$9/O41.qQjQNlleivjbckbSNpfS4xgh0",
209 password: "pass1234",
210 result: CheckHashResult::InvalidHash(InvalidHash::UnsupportedHashType),
211 },
212 TestCase {
213 encoded_hash: "$H$1/O41.qQjQNlleivjbckbSNpfS4xgh0",
214 password: "pass1234",
215 result: CheckHashResult::InvalidHash(InvalidHash::InvalidRounds),
216 },
217 ];
218 for case in &test_cases {
219 let result = check_hash(case.encoded_hash, case.password);
220 assert_eq!(result, case.result, "{:?}", case);
221 }
222 }
223}