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> {
326 let mut buf = vec![0u8; len];
327 if let Err(err) = getrandom::fill(&mut buf) {
328 unavailable_csprng(err);
329 }
330 buf
331}
332
333#[cold]
339fn unavailable_csprng(err: getrandom::Error) -> ! {
340 panic!(
341 "FATAL: OS cryptographically secure random source is unavailable ({err}). \
342 Password hashing requires a CSPRNG for salt generation. \
343 Cannot safely generate password hashes without cryptographic entropy."
344 );
345}
346
347pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
349 if a.len() != b.len() {
350 return false;
351 }
352 let mut diff = 0u8;
353 for (x, y) in a.iter().zip(b.iter()) {
354 diff |= x ^ y;
355 }
356 diff == 0
357}
358
359pub trait SecureCompare<Rhs: ?Sized = Self> {
364 fn secure_eq(&self, other: &Rhs) -> bool;
366}
367
368impl SecureCompare for [u8] {
369 fn secure_eq(&self, other: &[u8]) -> bool {
370 constant_time_eq(self, other)
371 }
372}
373
374impl SecureCompare for str {
375 fn secure_eq(&self, other: &str) -> bool {
376 constant_time_eq(self.as_bytes(), other.as_bytes())
377 }
378}
379
380fn base64_encode(data: &[u8]) -> String {
382 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
383 let mut result = String::with_capacity((data.len() * 4).div_ceil(3));
384 for chunk in data.chunks(3) {
385 let b0 = u32::from(chunk[0]);
386 let b1 = if chunk.len() > 1 {
387 u32::from(chunk[1])
388 } else {
389 0
390 };
391 let b2 = if chunk.len() > 2 {
392 u32::from(chunk[2])
393 } else {
394 0
395 };
396 let n = (b0 << 16) | (b1 << 8) | b2;
397 result.push(CHARS[((n >> 18) & 63) as usize] as char);
398 result.push(CHARS[((n >> 12) & 63) as usize] as char);
399 if chunk.len() > 1 {
400 result.push(CHARS[((n >> 6) & 63) as usize] as char);
401 }
402 if chunk.len() > 2 {
403 result.push(CHARS[(n & 63) as usize] as char);
404 }
405 }
406 result
407}
408
409fn base64_decode(s: &str) -> Option<Vec<u8>> {
414 fn char_val(c: u8) -> Option<u32> {
415 match c {
416 b'A'..=b'Z' => Some(u32::from(c - b'A')),
417 b'a'..=b'z' => Some(u32::from(c - b'a' + 26)),
418 b'0'..=b'9' => Some(u32::from(c - b'0' + 52)),
419 b'+' => Some(62),
420 b'/' => Some(63),
421 _ => None,
422 }
423 }
424
425 let s = s.trim_end_matches('=');
427 let bytes = s.as_bytes();
428 let mut result = Vec::with_capacity(bytes.len() * 3 / 4);
429 for chunk in bytes.chunks(4) {
430 let mut vals = [0u32; 4];
432 let mut count = 0;
433 for &b in chunk {
434 vals[count] = char_val(b)?; count += 1;
436 }
437 if count >= 2 {
438 result.push(((vals[0] << 2) | (vals[1] >> 4)) as u8);
439 }
440 if count >= 3 {
441 result.push((((vals[1] & 0xf) << 4) | (vals[2] >> 2)) as u8);
442 }
443 if count >= 4 {
444 result.push((((vals[2] & 0x3) << 6) | vals[3]) as u8);
445 }
446 }
447 Some(result)
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 #[test]
455 fn hash_and_verify() {
456 let hasher = PasswordHasher::default();
457 let hash = hasher.hash_password("secret123");
458 assert!(hasher.verify_password("secret123", &hash));
459 }
460
461 #[test]
462 fn wrong_password_fails() {
463 let hasher = PasswordHasher::default();
464 let hash = hasher.hash_password("correct");
465 assert!(!hasher.verify_password("wrong", &hash));
466 }
467
468 #[test]
469 fn unique_salts() {
470 let hasher = PasswordHasher::default();
471 let h1 = hasher.hash_password("same");
472 let h2 = hasher.hash_password("same");
473 assert_ne!(h1, h2);
475 assert!(hasher.verify_password("same", &h1));
477 assert!(hasher.verify_password("same", &h2));
478 }
479
480 #[test]
481 fn deterministic_with_known_salt() {
482 let hasher = PasswordHasher::new(HashConfig::new().iterations(1000));
483 let salt = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
484 let h1 = hasher.hash_with_salt("test", &salt);
485 let h2 = hasher.hash_with_salt("test", &salt);
486 assert_eq!(h1, h2);
487 assert!(hasher.verify_password("test", &h1));
488 }
489
490 #[test]
491 fn hash_format() {
492 let hasher = PasswordHasher::default();
493 let hash = hasher.hash_password("password");
494 assert!(hash.starts_with("$pbkdf2-sha256$"));
495 let parts: Vec<&str> = hash.split('$').collect();
496 assert_eq!(parts.len(), 5);
497 assert_eq!(parts[1], "pbkdf2-sha256");
498 assert_eq!(parts[2], "600000"); }
500
501 #[test]
502 fn custom_iterations() {
503 let hasher = PasswordHasher::new(HashConfig::new().iterations(10_000));
504 let hash = hasher.hash_password("test");
505 assert!(hash.contains("$10000$"));
506 assert!(hasher.verify_password("test", &hash));
507 }
508
509 #[test]
510 fn invalid_hash_string() {
511 let hasher = PasswordHasher::default();
512 assert!(!hasher.verify_password("test", "not-a-hash"));
513 assert!(!hasher.verify_password("test", "$unknown$100$salt$hash"));
514 assert!(!hasher.verify_password("test", ""));
515 }
516
517 #[test]
518 fn empty_password() {
519 let hasher = PasswordHasher::default();
520 let hash = hasher.hash_password("");
521 assert!(hasher.verify_password("", &hash));
522 assert!(!hasher.verify_password("notempty", &hash));
523 }
524
525 #[test]
526 fn sha256_known_vector() {
527 let result = sha256(b"");
529 assert_eq!(result[0], 0xe3);
530 assert_eq!(result[1], 0xb0);
531 assert_eq!(result[2], 0xc4);
532 assert_eq!(result[3], 0x42);
533 }
534
535 #[test]
536 fn sha256_abc_vector() {
537 let result = sha256(b"abc");
539 assert_eq!(result[0], 0xba);
540 assert_eq!(result[1], 0x78);
541 assert_eq!(result[2], 0x16);
542 assert_eq!(result[3], 0xbf);
543 }
544
545 #[test]
546 fn base64_roundtrip() {
547 let data = b"hello world";
548 let encoded = base64_encode(data);
549 let decoded = base64_decode(&encoded).unwrap();
550 assert_eq!(&decoded, data);
551 }
552
553 #[test]
554 fn constant_time_eq_works() {
555 assert!(constant_time_eq(b"abc", b"abc"));
556 assert!(!constant_time_eq(b"abc", b"abd"));
557 assert!(!constant_time_eq(b"abc", b"ab"));
558 }
559
560 #[test]
561 fn algorithm_display() {
562 assert_eq!(Algorithm::Pbkdf2Sha256.to_string(), "pbkdf2-sha256");
563 }
564}