1use std::fmt;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Algorithm {
22 Pbkdf2Sha256,
24}
25
26impl fmt::Display for Algorithm {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Self::Pbkdf2Sha256 => write!(f, "pbkdf2-sha256"),
30 }
31 }
32}
33
34impl Algorithm {
35 fn from_prefix(s: &str) -> Option<Self> {
37 if s.starts_with("$pbkdf2-sha256$") {
38 Some(Self::Pbkdf2Sha256)
39 } else {
40 None
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct HashConfig {
48 pub algorithm: Algorithm,
50 pub iterations: u32,
52 pub salt_len: usize,
54 pub hash_len: usize,
56}
57
58impl Default for HashConfig {
59 fn default() -> Self {
60 Self {
61 algorithm: Algorithm::Pbkdf2Sha256,
62 iterations: 600_000,
64 salt_len: 16,
65 hash_len: 32,
66 }
67 }
68}
69
70impl HashConfig {
71 #[must_use]
73 pub fn new() -> Self {
74 Self::default()
75 }
76
77 #[must_use]
79 pub fn iterations(mut self, n: u32) -> Self {
80 self.iterations = n;
81 self
82 }
83
84 #[must_use]
86 pub fn algorithm(mut self, alg: Algorithm) -> Self {
87 self.algorithm = alg;
88 self
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct PasswordHasher {
95 config: HashConfig,
96}
97
98impl PasswordHasher {
99 pub fn new(config: HashConfig) -> Self {
101 Self { config }
102 }
103
104 pub fn hash_password(&self, password: &str) -> String {
108 let salt = generate_salt(self.config.salt_len);
109 self.hash_with_salt(password, &salt)
110 }
111
112 fn hash_with_salt(&self, password: &str, salt: &[u8]) -> String {
114 match self.config.algorithm {
115 Algorithm::Pbkdf2Sha256 => {
116 let hash = pbkdf2_hmac_sha256(
117 password.as_bytes(),
118 salt,
119 self.config.iterations,
120 self.config.hash_len,
121 );
122 format!(
123 "$pbkdf2-sha256${}${}${}",
124 self.config.iterations,
125 base64_encode(salt),
126 base64_encode(&hash),
127 )
128 }
129 }
130 }
131
132 pub fn verify_password(&self, password: &str, stored_hash: &str) -> bool {
136 let Some(algorithm) = Algorithm::from_prefix(stored_hash) else {
137 return false;
138 };
139 match algorithm {
140 Algorithm::Pbkdf2Sha256 => self.verify_pbkdf2(password, stored_hash),
141 }
142 }
143
144 fn verify_pbkdf2(&self, password: &str, stored_hash: &str) -> bool {
145 let parts: Vec<&str> = stored_hash.split('$').collect();
147 if parts.len() != 5 {
148 return false;
149 }
150 let Ok(iterations) = parts[2].parse::<u32>() else {
153 return false;
154 };
155 let Some(salt) = base64_decode(parts[3]) else {
156 return false;
157 };
158 let Some(expected) = base64_decode(parts[4]) else {
159 return false;
160 };
161 let computed = pbkdf2_hmac_sha256(password.as_bytes(), &salt, iterations, expected.len());
162 constant_time_eq(&computed, &expected)
163 }
164
165 pub fn config(&self) -> &HashConfig {
167 &self.config
168 }
169}
170
171impl Default for PasswordHasher {
172 fn default() -> Self {
173 Self::new(HashConfig::default())
174 }
175}
176
177fn pbkdf2_hmac_sha256(password: &[u8], salt: &[u8], iterations: u32, dk_len: usize) -> Vec<u8> {
183 let mut result = Vec::with_capacity(dk_len);
184 let blocks_needed = dk_len.div_ceil(32);
185
186 for block_index in 1..=blocks_needed as u32 {
187 let mut u = hmac_sha256(password, &[salt, &block_index.to_be_bytes()].concat());
188 let mut block = u;
189 for _ in 1..iterations {
190 u = hmac_sha256(password, &u);
191 for (b, v) in block.iter_mut().zip(u.iter()) {
192 *b ^= v;
193 }
194 }
195 result.extend_from_slice(&block);
196 }
197
198 result.truncate(dk_len);
199 result
200}
201
202fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
204 let block_size = 64;
205 let mut padded_key = [0u8; 64];
206
207 if key.len() > block_size {
208 let hashed = sha256(key);
209 padded_key[..32].copy_from_slice(&hashed);
210 } else {
211 padded_key[..key.len()].copy_from_slice(key);
212 }
213
214 let mut ipad = [0x36u8; 64];
215 let mut opad = [0x5cu8; 64];
216 for i in 0..64 {
217 ipad[i] ^= padded_key[i];
218 opad[i] ^= padded_key[i];
219 }
220
221 let inner = sha256(&[&ipad[..], message].concat());
222 sha256(&[&opad[..], &inner[..]].concat())
223}
224
225#[allow(clippy::many_single_char_names)]
227fn sha256(data: &[u8]) -> [u8; 32] {
228 const K: [u32; 64] = [
229 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
230 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
231 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
232 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
233 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
234 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
235 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
236 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
237 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
238 0xc67178f2,
239 ];
240
241 let mut h: [u32; 8] = [
242 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
243 0x5be0cd19,
244 ];
245
246 let bit_len = (data.len() as u64) * 8;
248 let mut padded = data.to_vec();
249 padded.push(0x80);
250 while (padded.len() % 64) != 56 {
251 padded.push(0);
252 }
253 padded.extend_from_slice(&bit_len.to_be_bytes());
254
255 for chunk in padded.chunks(64) {
257 let mut w = [0u32; 64];
258 for i in 0..16 {
259 w[i] = u32::from_be_bytes([
260 chunk[i * 4],
261 chunk[i * 4 + 1],
262 chunk[i * 4 + 2],
263 chunk[i * 4 + 3],
264 ]);
265 }
266 for i in 16..64 {
267 let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
268 let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
269 w[i] = w[i - 16]
270 .wrapping_add(s0)
271 .wrapping_add(w[i - 7])
272 .wrapping_add(s1);
273 }
274
275 let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h;
276
277 for i in 0..64 {
278 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
279 let ch = (e & f) ^ ((!e) & g);
280 let temp1 = hh
281 .wrapping_add(s1)
282 .wrapping_add(ch)
283 .wrapping_add(K[i])
284 .wrapping_add(w[i]);
285 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
286 let maj = (a & b) ^ (a & c) ^ (b & c);
287 let temp2 = s0.wrapping_add(maj);
288
289 hh = g;
290 g = f;
291 f = e;
292 e = d.wrapping_add(temp1);
293 d = c;
294 c = b;
295 b = a;
296 a = temp1.wrapping_add(temp2);
297 }
298
299 h[0] = h[0].wrapping_add(a);
300 h[1] = h[1].wrapping_add(b);
301 h[2] = h[2].wrapping_add(c);
302 h[3] = h[3].wrapping_add(d);
303 h[4] = h[4].wrapping_add(e);
304 h[5] = h[5].wrapping_add(f);
305 h[6] = h[6].wrapping_add(g);
306 h[7] = h[7].wrapping_add(hh);
307 }
308
309 let mut result = [0u8; 32];
310 for (i, val) in h.iter().enumerate() {
311 result[i * 4..i * 4 + 4].copy_from_slice(&val.to_be_bytes());
312 }
313 result
314}
315
316fn generate_salt(len: usize) -> Vec<u8> {
325 if let Ok(bytes) = read_urandom(len) {
327 return bytes;
328 }
329
330 fallback_salt(len)
332}
333
334fn read_urandom(len: usize) -> std::io::Result<Vec<u8>> {
336 use std::io::Read;
337 let mut f = std::fs::File::open("/dev/urandom")?;
338 let mut buf = vec![0u8; len];
339 f.read_exact(&mut buf)?;
340 Ok(buf)
341}
342
343#[cold]
351fn fallback_salt(_len: usize) -> Vec<u8> {
352 panic!(
353 "FATAL: Cryptographically secure random source (/dev/urandom) is unavailable. \
354 Password hashing requires a CSPRNG for salt generation. \
355 Cannot safely generate password hashes without cryptographic entropy."
356 );
357}
358
359fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
361 if a.len() != b.len() {
362 return false;
363 }
364 let mut diff = 0u8;
365 for (x, y) in a.iter().zip(b.iter()) {
366 diff |= x ^ y;
367 }
368 diff == 0
369}
370
371fn base64_encode(data: &[u8]) -> String {
373 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
374 let mut result = String::with_capacity((data.len() * 4).div_ceil(3));
375 for chunk in data.chunks(3) {
376 let b0 = u32::from(chunk[0]);
377 let b1 = if chunk.len() > 1 {
378 u32::from(chunk[1])
379 } else {
380 0
381 };
382 let b2 = if chunk.len() > 2 {
383 u32::from(chunk[2])
384 } else {
385 0
386 };
387 let n = (b0 << 16) | (b1 << 8) | b2;
388 result.push(CHARS[((n >> 18) & 63) as usize] as char);
389 result.push(CHARS[((n >> 12) & 63) as usize] as char);
390 if chunk.len() > 1 {
391 result.push(CHARS[((n >> 6) & 63) as usize] as char);
392 }
393 if chunk.len() > 2 {
394 result.push(CHARS[(n & 63) as usize] as char);
395 }
396 }
397 result
398}
399
400fn base64_decode(s: &str) -> Option<Vec<u8>> {
405 fn char_val(c: u8) -> Option<u32> {
406 match c {
407 b'A'..=b'Z' => Some(u32::from(c - b'A')),
408 b'a'..=b'z' => Some(u32::from(c - b'a' + 26)),
409 b'0'..=b'9' => Some(u32::from(c - b'0' + 52)),
410 b'+' => Some(62),
411 b'/' => Some(63),
412 _ => None,
413 }
414 }
415
416 let s = s.trim_end_matches('=');
418 let bytes = s.as_bytes();
419 let mut result = Vec::with_capacity(bytes.len() * 3 / 4);
420 for chunk in bytes.chunks(4) {
421 let mut vals = [0u32; 4];
423 let mut count = 0;
424 for &b in chunk {
425 vals[count] = char_val(b)?; count += 1;
427 }
428 if count >= 2 {
429 result.push(((vals[0] << 2) | (vals[1] >> 4)) as u8);
430 }
431 if count >= 3 {
432 result.push((((vals[1] & 0xf) << 4) | (vals[2] >> 2)) as u8);
433 }
434 if count >= 4 {
435 result.push((((vals[2] & 0x3) << 6) | vals[3]) as u8);
436 }
437 }
438 Some(result)
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn hash_and_verify() {
447 let hasher = PasswordHasher::default();
448 let hash = hasher.hash_password("secret123");
449 assert!(hasher.verify_password("secret123", &hash));
450 }
451
452 #[test]
453 fn wrong_password_fails() {
454 let hasher = PasswordHasher::default();
455 let hash = hasher.hash_password("correct");
456 assert!(!hasher.verify_password("wrong", &hash));
457 }
458
459 #[test]
460 fn unique_salts() {
461 let hasher = PasswordHasher::default();
462 let h1 = hasher.hash_password("same");
463 let h2 = hasher.hash_password("same");
464 assert_ne!(h1, h2);
466 assert!(hasher.verify_password("same", &h1));
468 assert!(hasher.verify_password("same", &h2));
469 }
470
471 #[test]
472 fn deterministic_with_known_salt() {
473 let hasher = PasswordHasher::new(HashConfig::new().iterations(1000));
474 let salt = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
475 let h1 = hasher.hash_with_salt("test", &salt);
476 let h2 = hasher.hash_with_salt("test", &salt);
477 assert_eq!(h1, h2);
478 assert!(hasher.verify_password("test", &h1));
479 }
480
481 #[test]
482 fn hash_format() {
483 let hasher = PasswordHasher::default();
484 let hash = hasher.hash_password("password");
485 assert!(hash.starts_with("$pbkdf2-sha256$"));
486 let parts: Vec<&str> = hash.split('$').collect();
487 assert_eq!(parts.len(), 5);
488 assert_eq!(parts[1], "pbkdf2-sha256");
489 assert_eq!(parts[2], "600000"); }
491
492 #[test]
493 fn custom_iterations() {
494 let hasher = PasswordHasher::new(HashConfig::new().iterations(10_000));
495 let hash = hasher.hash_password("test");
496 assert!(hash.contains("$10000$"));
497 assert!(hasher.verify_password("test", &hash));
498 }
499
500 #[test]
501 fn invalid_hash_string() {
502 let hasher = PasswordHasher::default();
503 assert!(!hasher.verify_password("test", "not-a-hash"));
504 assert!(!hasher.verify_password("test", "$unknown$100$salt$hash"));
505 assert!(!hasher.verify_password("test", ""));
506 }
507
508 #[test]
509 fn empty_password() {
510 let hasher = PasswordHasher::default();
511 let hash = hasher.hash_password("");
512 assert!(hasher.verify_password("", &hash));
513 assert!(!hasher.verify_password("notempty", &hash));
514 }
515
516 #[test]
517 fn sha256_known_vector() {
518 let result = sha256(b"");
520 assert_eq!(result[0], 0xe3);
521 assert_eq!(result[1], 0xb0);
522 assert_eq!(result[2], 0xc4);
523 assert_eq!(result[3], 0x42);
524 }
525
526 #[test]
527 fn sha256_abc_vector() {
528 let result = sha256(b"abc");
530 assert_eq!(result[0], 0xba);
531 assert_eq!(result[1], 0x78);
532 assert_eq!(result[2], 0x16);
533 assert_eq!(result[3], 0xbf);
534 }
535
536 #[test]
537 fn base64_roundtrip() {
538 let data = b"hello world";
539 let encoded = base64_encode(data);
540 let decoded = base64_decode(&encoded).unwrap();
541 assert_eq!(&decoded, data);
542 }
543
544 #[test]
545 fn constant_time_eq_works() {
546 assert!(constant_time_eq(b"abc", b"abc"));
547 assert!(!constant_time_eq(b"abc", b"abd"));
548 assert!(!constant_time_eq(b"abc", b"ab"));
549 }
550
551 #[test]
552 fn algorithm_display() {
553 assert_eq!(Algorithm::Pbkdf2Sha256.to_string(), "pbkdf2-sha256");
554 }
555}