Skip to main content

rskit_auth/password/
hasher.rs

1use argon2::{
2    Algorithm, Argon2, Params, Version,
3    password_hash::{PasswordHash, PasswordHasher as Argon2Hasher, PasswordVerifier, SaltString},
4};
5use rskit_errors::{AppError, AppResult};
6
7const ARGON2_MEMORY_COST_KIB: u32 = 65_536;
8const ARGON2_TIME_COST: u32 = 3;
9const ARGON2_PARALLELISM: u32 = 4;
10const ARGON2_HASH_LEN: usize = 32;
11
12/// Supported password hashing algorithms.
13#[derive(Debug, Clone, Default)]
14#[non_exhaustive]
15pub enum HashAlgorithm {
16    /// Argon2id — recommended default.
17    #[default]
18    Argon2id,
19}
20
21/// Hashes and verifies passwords.
22#[derive(Debug, Clone, Default)]
23pub struct PasswordHasher {
24    /// Hashing algorithm to use.
25    pub algorithm: HashAlgorithm,
26}
27
28impl PasswordHasher {
29    /// Create a new [`PasswordHasher`] with the given algorithm.
30    pub const fn new(algorithm: HashAlgorithm) -> Self {
31        Self { algorithm }
32    }
33
34    /// Hash a plaintext `password`.
35    pub fn hash(&self, password: &str) -> AppResult<String> {
36        let mut rng = argon2::password_hash::rand_core::OsRng;
37        let salt = SaltString::generate(&mut rng);
38        let argon2 = configured_argon2()?;
39        argon2
40            .hash_password(password.as_bytes(), &salt)
41            .map(|h| h.to_string())
42            .map_err(|e| {
43                AppError::new(
44                    rskit_errors::ErrorCode::Internal,
45                    format!("password hash error: {e}"),
46                )
47            })
48    }
49
50    /// Verify that `password` matches the stored `hash`.
51    pub fn verify(&self, password: &str, hash: &str) -> AppResult<bool> {
52        let parsed = PasswordHash::new(hash).map_err(|e| {
53            AppError::new(
54                rskit_errors::ErrorCode::Internal,
55                format!("invalid hash format: {e}"),
56            )
57        })?;
58        Ok(configured_argon2()?
59            .verify_password(password.as_bytes(), &parsed)
60            .is_ok())
61    }
62}
63
64fn configured_argon2() -> AppResult<Argon2<'static>> {
65    let params = Params::new(
66        ARGON2_MEMORY_COST_KIB,
67        ARGON2_TIME_COST,
68        ARGON2_PARALLELISM,
69        Some(ARGON2_HASH_LEN),
70    )
71    .map_err(|_e| AppError::internal(std::io::Error::other("invalid argon2 params")))?;
72    Ok(Argon2::new(Algorithm::Argon2id, Version::V0x13, params))
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn hash_and_verify_roundtrip() {
81        let h = PasswordHasher::default();
82        let hash = h.hash("hunter2").unwrap();
83        assert!(h.verify("hunter2", &hash).unwrap());
84        assert!(!h.verify("wrong", &hash).unwrap());
85    }
86
87    #[test]
88    fn hash_uses_locked_group_05_parameters() {
89        let hash = PasswordHasher::default().hash("hunter2hunter2").unwrap();
90        assert!(hash.contains("$argon2id$"));
91        assert!(hash.contains("m=65536"));
92        assert!(hash.contains("t=3"));
93        assert!(hash.contains("p=4"));
94    }
95}