Skip to main content

modo/auth/
password.rs

1use argon2::{
2    Algorithm, Argon2, Params, Version,
3    password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
4};
5use serde::Deserialize;
6
7/// Argon2id hashing parameters.
8///
9/// Deserializes from YAML/TOML config. All fields have OWASP-recommended defaults:
10/// 19 MiB memory, 2 iterations, 1 thread, 32-byte output.
11#[non_exhaustive]
12#[derive(Debug, Clone, Deserialize)]
13#[serde(default)]
14pub struct PasswordConfig {
15    /// Memory cost in kibibytes (default: 19456 = 19 MiB).
16    pub memory_cost_kib: u32,
17    /// Number of iterations (default: 2).
18    pub time_cost: u32,
19    /// Degree of parallelism (default: 1).
20    pub parallelism: u32,
21    /// Output hash length in bytes (default: 32).
22    pub output_len: usize,
23}
24
25impl Default for PasswordConfig {
26    fn default() -> Self {
27        Self {
28            memory_cost_kib: 19456,
29            time_cost: 2,
30            parallelism: 1,
31            output_len: 32,
32        }
33    }
34}
35
36/// Hashes `password` with Argon2id using the provided configuration.
37///
38/// Runs on a blocking thread via `tokio::task::spawn_blocking` so it does not
39/// starve the async runtime. Returns a PHC-formatted string that embeds the
40/// algorithm, parameters, salt, and hash — suitable for storage in a database.
41///
42/// Requires feature `"auth"`.
43///
44/// # Errors
45///
46/// Returns `Error::internal` if the Argon2id parameters are invalid or the
47/// blocking task panics.
48pub async fn hash(password: &str, config: &PasswordConfig) -> crate::Result<String> {
49    let config = config.clone();
50    let password = password.to_string();
51    tokio::task::spawn_blocking(move || hash_blocking(&password, &config))
52        .await
53        .map_err(|e| crate::Error::internal(format!("password hash task failed: {e}")))?
54}
55
56/// Verifies `password` against a PHC-formatted `hash` produced by [`hash`].
57///
58/// Runs on a blocking thread. Returns `true` if the password matches, `false`
59/// otherwise. Never returns an error for a wrong password — only for a
60/// malformed hash string.
61///
62/// Requires feature `"auth"`.
63///
64/// # Errors
65///
66/// Returns `Error::internal` if the hash string is structurally invalid (not
67/// PHC-formatted) or the blocking task panics.
68pub async fn verify(password: &str, hash: &str) -> crate::Result<bool> {
69    let password = password.to_string();
70    let hash = hash.to_string();
71    tokio::task::spawn_blocking(move || verify_blocking(&password, &hash))
72        .await
73        .map_err(|e| crate::Error::internal(format!("password verify task failed: {e}")))?
74}
75
76fn hash_blocking(password: &str, config: &PasswordConfig) -> crate::Result<String> {
77    let params = Params::new(
78        config.memory_cost_kib,
79        config.time_cost,
80        config.parallelism,
81        Some(config.output_len),
82    )
83    .map_err(|e| crate::Error::internal(format!("invalid argon2 params: {e}")))?;
84
85    let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
86    let salt = SaltString::generate(&mut OsRng);
87    let hash = argon2
88        .hash_password(password.as_bytes(), &salt)
89        .map_err(|e| crate::Error::internal(format!("password hashing failed: {e}")))?;
90
91    Ok(hash.to_string())
92}
93
94fn verify_blocking(password: &str, hash: &str) -> crate::Result<bool> {
95    let parsed = PasswordHash::new(hash)
96        .map_err(|e| crate::Error::internal(format!("invalid password hash: {e}")))?;
97
98    Ok(Argon2::default()
99        .verify_password(password.as_bytes(), &parsed)
100        .is_ok())
101}