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 `getrandom`, which dispatches to `getrandom(2)` / `/dev/urandom` on
323/// Unix and `BCryptGenRandom` on Windows, so all supported targets receive
324/// cryptographically secure entropy.
325fn 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/// # Panics
334///
335/// Always panics. Reached only when the OS CSPRNG is unavailable, which
336/// indicates a severely misconfigured or compromised system. Password hashing
337/// MUST use a CSPRNG - there is no safe way to generate salts without one.
338#[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
347/// Timing-safe byte comparison.
348pub 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
359/// Trait providing a timing-safe equality check (`secure_eq`) for common types.
360///
361/// This is intentionally tiny (no extra dependencies) and is used by auth and
362/// security-sensitive middleware.
363pub trait SecureCompare<Rhs: ?Sized = Self> {
364    /// Timing-safe equality check.
365    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
380/// Simple base64 encode (standard alphabet, no padding).
381fn 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
409/// Simple base64 decode (standard alphabet, no padding required).
410///
411/// Returns `None` if the input contains invalid characters. Padding
412/// characters (`=`) are stripped before decoding.
413fn 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    // Strip padding before decoding
426    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        // Every byte in the chunk must be a valid base64 character
431        let mut vals = [0u32; 4];
432        let mut count = 0;
433        for &b in chunk {
434            vals[count] = char_val(b)?; // Return None on invalid char
435            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        // Different salts produce different hashes
474        assert_ne!(h1, h2);
475        // But both verify
476        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"); // OWASP 2023 minimum for PBKDF2-SHA256
499    }
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        // SHA-256("") = e3b0c44298fc1c149afbf4c8996fb924...
528        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        // SHA-256("abc") = ba7816bf8f01cfea414140de5dae2223...
538        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}