1use argon2::{
2 Algorithm, Argon2, Params, Version,
3 password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
4};
5use serde::Deserialize;
6
7#[non_exhaustive]
12#[derive(Debug, Clone, Deserialize)]
13#[serde(default)]
14pub struct PasswordConfig {
15 pub memory_cost_kib: u32,
17 pub time_cost: u32,
19 pub parallelism: u32,
21 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
36pub async fn hash(password: &str, config: &PasswordConfig) -> crate::Result<String> {
47 let config = config.clone();
48 let password = password.to_string();
49 tokio::task::spawn_blocking(move || hash_blocking(&password, &config))
50 .await
51 .map_err(|e| crate::Error::internal(format!("password hash task failed: {e}")))?
52}
53
54pub async fn verify(password: &str, hash: &str) -> crate::Result<bool> {
65 let password = password.to_string();
66 let hash = hash.to_string();
67 tokio::task::spawn_blocking(move || verify_blocking(&password, &hash))
68 .await
69 .map_err(|e| crate::Error::internal(format!("password verify task failed: {e}")))?
70}
71
72fn hash_blocking(password: &str, config: &PasswordConfig) -> crate::Result<String> {
73 let params = Params::new(
74 config.memory_cost_kib,
75 config.time_cost,
76 config.parallelism,
77 Some(config.output_len),
78 )
79 .map_err(|e| crate::Error::internal(format!("invalid argon2 params: {e}")))?;
80
81 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
82 let salt = SaltString::generate(&mut OsRng);
83 let hash = argon2
84 .hash_password(password.as_bytes(), &salt)
85 .map_err(|e| crate::Error::internal(format!("password hashing failed: {e}")))?;
86
87 Ok(hash.to_string())
88}
89
90fn verify_blocking(password: &str, hash: &str) -> crate::Result<bool> {
91 let parsed = PasswordHash::new(hash)
92 .map_err(|e| crate::Error::internal(format!("invalid password hash: {e}")))?;
93
94 Ok(Argon2::default()
95 .verify_password(password.as_bytes(), &parsed)
96 .is_ok())
97}