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> {
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
56pub 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}