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::password::{PasswordHasher, HashConfig, Algorithm};
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.
360fn 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/// Simple base64 encode (standard alphabet, no padding).
372fn 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
400/// Simple base64 decode (standard alphabet, no padding required).
401///
402/// Returns `None` if the input contains invalid characters. Padding
403/// characters (`=`) are stripped before decoding.
404fn 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    // Strip padding before decoding
417    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        // Every byte in the chunk must be a valid base64 character
422        let mut vals = [0u32; 4];
423        let mut count = 0;
424        for &b in chunk {
425            vals[count] = char_val(b)?; // Return None on invalid char
426            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        // Different salts produce different hashes
465        assert_ne!(h1, h2);
466        // But both verify
467        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"); // OWASP 2023 minimum for PBKDF2-SHA256
490    }
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        // SHA-256("") = e3b0c44298fc1c149afbf4c8996fb924...
519        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        // SHA-256("abc") = ba7816bf8f01cfea414140de5dae2223...
529        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}