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
359pub fn 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
371pub trait SecureCompare<Rhs: ?Sized = Self> {
376 fn secure_eq(&self, other: &Rhs) -> bool;
378}
379
380impl SecureCompare for [u8] {
381 fn secure_eq(&self, other: &[u8]) -> bool {
382 constant_time_eq(self, other)
383 }
384}
385
386impl SecureCompare for str {
387 fn secure_eq(&self, other: &str) -> bool {
388 constant_time_eq(self.as_bytes(), other.as_bytes())
389 }
390}
391
392fn base64_encode(data: &[u8]) -> String {
394 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
395 let mut result = String::with_capacity((data.len() * 4).div_ceil(3));
396 for chunk in data.chunks(3) {
397 let b0 = u32::from(chunk[0]);
398 let b1 = if chunk.len() > 1 {
399 u32::from(chunk[1])
400 } else {
401 0
402 };
403 let b2 = if chunk.len() > 2 {
404 u32::from(chunk[2])
405 } else {
406 0
407 };
408 let n = (b0 << 16) | (b1 << 8) | b2;
409 result.push(CHARS[((n >> 18) & 63) as usize] as char);
410 result.push(CHARS[((n >> 12) & 63) as usize] as char);
411 if chunk.len() > 1 {
412 result.push(CHARS[((n >> 6) & 63) as usize] as char);
413 }
414 if chunk.len() > 2 {
415 result.push(CHARS[(n & 63) as usize] as char);
416 }
417 }
418 result
419}
420
421fn base64_decode(s: &str) -> Option<Vec<u8>> {
426 fn char_val(c: u8) -> Option<u32> {
427 match c {
428 b'A'..=b'Z' => Some(u32::from(c - b'A')),
429 b'a'..=b'z' => Some(u32::from(c - b'a' + 26)),
430 b'0'..=b'9' => Some(u32::from(c - b'0' + 52)),
431 b'+' => Some(62),
432 b'/' => Some(63),
433 _ => None,
434 }
435 }
436
437 let s = s.trim_end_matches('=');
439 let bytes = s.as_bytes();
440 let mut result = Vec::with_capacity(bytes.len() * 3 / 4);
441 for chunk in bytes.chunks(4) {
442 let mut vals = [0u32; 4];
444 let mut count = 0;
445 for &b in chunk {
446 vals[count] = char_val(b)?; count += 1;
448 }
449 if count >= 2 {
450 result.push(((vals[0] << 2) | (vals[1] >> 4)) as u8);
451 }
452 if count >= 3 {
453 result.push((((vals[1] & 0xf) << 4) | (vals[2] >> 2)) as u8);
454 }
455 if count >= 4 {
456 result.push((((vals[2] & 0x3) << 6) | vals[3]) as u8);
457 }
458 }
459 Some(result)
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 #[test]
467 fn hash_and_verify() {
468 let hasher = PasswordHasher::default();
469 let hash = hasher.hash_password("secret123");
470 assert!(hasher.verify_password("secret123", &hash));
471 }
472
473 #[test]
474 fn wrong_password_fails() {
475 let hasher = PasswordHasher::default();
476 let hash = hasher.hash_password("correct");
477 assert!(!hasher.verify_password("wrong", &hash));
478 }
479
480 #[test]
481 fn unique_salts() {
482 let hasher = PasswordHasher::default();
483 let h1 = hasher.hash_password("same");
484 let h2 = hasher.hash_password("same");
485 assert_ne!(h1, h2);
487 assert!(hasher.verify_password("same", &h1));
489 assert!(hasher.verify_password("same", &h2));
490 }
491
492 #[test]
493 fn deterministic_with_known_salt() {
494 let hasher = PasswordHasher::new(HashConfig::new().iterations(1000));
495 let salt = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
496 let h1 = hasher.hash_with_salt("test", &salt);
497 let h2 = hasher.hash_with_salt("test", &salt);
498 assert_eq!(h1, h2);
499 assert!(hasher.verify_password("test", &h1));
500 }
501
502 #[test]
503 fn hash_format() {
504 let hasher = PasswordHasher::default();
505 let hash = hasher.hash_password("password");
506 assert!(hash.starts_with("$pbkdf2-sha256$"));
507 let parts: Vec<&str> = hash.split('$').collect();
508 assert_eq!(parts.len(), 5);
509 assert_eq!(parts[1], "pbkdf2-sha256");
510 assert_eq!(parts[2], "600000"); }
512
513 #[test]
514 fn custom_iterations() {
515 let hasher = PasswordHasher::new(HashConfig::new().iterations(10_000));
516 let hash = hasher.hash_password("test");
517 assert!(hash.contains("$10000$"));
518 assert!(hasher.verify_password("test", &hash));
519 }
520
521 #[test]
522 fn invalid_hash_string() {
523 let hasher = PasswordHasher::default();
524 assert!(!hasher.verify_password("test", "not-a-hash"));
525 assert!(!hasher.verify_password("test", "$unknown$100$salt$hash"));
526 assert!(!hasher.verify_password("test", ""));
527 }
528
529 #[test]
530 fn empty_password() {
531 let hasher = PasswordHasher::default();
532 let hash = hasher.hash_password("");
533 assert!(hasher.verify_password("", &hash));
534 assert!(!hasher.verify_password("notempty", &hash));
535 }
536
537 #[test]
538 fn sha256_known_vector() {
539 let result = sha256(b"");
541 assert_eq!(result[0], 0xe3);
542 assert_eq!(result[1], 0xb0);
543 assert_eq!(result[2], 0xc4);
544 assert_eq!(result[3], 0x42);
545 }
546
547 #[test]
548 fn sha256_abc_vector() {
549 let result = sha256(b"abc");
551 assert_eq!(result[0], 0xba);
552 assert_eq!(result[1], 0x78);
553 assert_eq!(result[2], 0x16);
554 assert_eq!(result[3], 0xbf);
555 }
556
557 #[test]
558 fn base64_roundtrip() {
559 let data = b"hello world";
560 let encoded = base64_encode(data);
561 let decoded = base64_decode(&encoded).unwrap();
562 assert_eq!(&decoded, data);
563 }
564
565 #[test]
566 fn constant_time_eq_works() {
567 assert!(constant_time_eq(b"abc", b"abc"));
568 assert!(!constant_time_eq(b"abc", b"abd"));
569 assert!(!constant_time_eq(b"abc", b"ab"));
570 }
571
572 #[test]
573 fn algorithm_display() {
574 assert_eq!(Algorithm::Pbkdf2Sha256.to_string(), "pbkdf2-sha256");
575 }
576}