Skip to main content

fastapi_core/
password.rs

1//! Password hashing utilities for secure credential storage.
2//!
3//! Provides a simple, safe API for hashing and verifying passwords
4//! with configurable algorithms and work factors.
5//!
6//! # Example
7//!
8//! ```
9//! use fastapi_core::{Algorithm, HashConfig, PasswordHasher};
10//!
11//! let hasher = PasswordHasher::new(HashConfig::default());
12//! let hash = hasher.hash_password("secret123");
13//! assert!(hasher.verify_password("secret123", &hash));
14//! assert!(!hasher.verify_password("wrong", &hash));
15//! ```
16
17use std::fmt;
18
19/// Supported hashing algorithms.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Algorithm {
22    /// PBKDF2-HMAC-SHA256 (built-in, no external deps).
23    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    /// Parse algorithm from the prefix of a stored hash string.
36    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/// Configuration for password hashing.
46#[derive(Debug, Clone)]
47pub struct HashConfig {
48    /// The algorithm to use.
49    pub algorithm: Algorithm,
50    /// Number of iterations (work factor).
51    pub iterations: u32,
52    /// Salt length in bytes.
53    pub salt_len: usize,
54    /// Output hash length in bytes.
55    pub hash_len: usize,
56}
57
58impl Default for HashConfig {
59    fn default() -> Self {
60        Self {
61            algorithm: Algorithm::Pbkdf2Sha256,
62            // OWASP 2023 recommendation: minimum 600,000 for PBKDF2-SHA256
63            iterations: 600_000,
64            salt_len: 16,
65            hash_len: 32,
66        }
67    }
68}
69
70impl HashConfig {
71    /// Create a new config with defaults.
72    #[must_use]
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Set the number of iterations.
78    #[must_use]
79    pub fn iterations(mut self, n: u32) -> Self {
80        self.iterations = n;
81        self
82    }
83
84    /// Set the algorithm.
85    #[must_use]
86    pub fn algorithm(mut self, alg: Algorithm) -> Self {
87        self.algorithm = alg;
88        self
89    }
90}
91
92/// Password hasher with configurable algorithm and work factors.
93#[derive(Debug, Clone)]
94pub struct PasswordHasher {
95    config: HashConfig,
96}
97
98impl PasswordHasher {
99    /// Create a new hasher with the given config.
100    pub fn new(config: HashConfig) -> Self {
101        Self { config }
102    }
103
104    /// Hash a password, returning a string in PHC format.
105    ///
106    /// Format: `$pbkdf2-sha256$iterations$base64(salt)$base64(hash)`
107    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    /// Hash with a specific salt (for testing determinism).
113    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    /// Verify a password against a stored hash.
133    ///
134    /// Uses timing-safe comparison to prevent timing attacks.
135    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        // Parse: $pbkdf2-sha256$iterations$salt$hash
146        let parts: Vec<&str> = stored_hash.split('$').collect();
147        if parts.len() != 5 {
148            return false;
149        }
150        // parts[0] = "" (before first $), parts[1] = "pbkdf2-sha256",
151        // parts[2] = iterations, parts[3] = salt, parts[4] = hash
152        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    /// Returns the config.
166    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
177// ============================================================
178// PBKDF2-HMAC-SHA256 implementation (no external deps)
179// ============================================================
180
181/// PBKDF2 using HMAC-SHA256 per RFC 2898.
182fn 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
202/// HMAC-SHA256.
203fn 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/// SHA-256 (pure Rust, no deps).
226#[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    // Padding
247    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    // Process blocks
256    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
316// ============================================================
317// Utility functions
318// ============================================================
319
320/// Generate random salt bytes from the OS entropy source.
321///
322/// Uses `/dev/urandom` on Unix systems for cryptographically secure randomness.
323/// Falls back to entropy mixing from multiple sources if `/dev/urandom` is unavailable.
324fn generate_salt(len: usize) -> Vec<u8> {
325    // Try /dev/urandom first (available on Linux, macOS, BSDs)
326    if let Ok(bytes) = read_urandom(len) {
327        return bytes;
328    }
329
330    // Fallback: mix multiple entropy sources via SHA-256
331    fallback_salt(len)
332}
333
334/// Read `len` bytes from `/dev/urandom`.
335fn 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/// Fallback when `/dev/urandom` is unavailable.
344///
345/// # Panics
346///
347/// Always panics. This function is only called when `/dev/urandom` is unavailable,
348/// which indicates a severely misconfigured or compromised system. Password hashing
349/// MUST use a CSPRNG - there is no safe way to generate salts without one.
350#[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
359/// Timing-safe byte comparison.
360pub 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
371/// Trait providing a timing-safe equality check (`secure_eq`) for common types.
372///
373/// This is intentionally tiny (no extra dependencies) and is used by auth and
374/// security-sensitive middleware.
375pub trait SecureCompare<Rhs: ?Sized = Self> {
376    /// Timing-safe equality check.
377    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
392/// Simple base64 encode (standard alphabet, no padding).
393fn 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
421/// Simple base64 decode (standard alphabet, no padding required).
422///
423/// Returns `None` if the input contains invalid characters. Padding
424/// characters (`=`) are stripped before decoding.
425fn 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    // Strip padding before decoding
438    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        // Every byte in the chunk must be a valid base64 character
443        let mut vals = [0u32; 4];
444        let mut count = 0;
445        for &b in chunk {
446            vals[count] = char_val(b)?; // Return None on invalid char
447            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        // Different salts produce different hashes
486        assert_ne!(h1, h2);
487        // But both verify
488        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"); // OWASP 2023 minimum for PBKDF2-SHA256
511    }
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        // SHA-256("") = e3b0c44298fc1c149afbf4c8996fb924...
540        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        // SHA-256("abc") = ba7816bf8f01cfea414140de5dae2223...
550        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}