webx_host/
password_argon2.rs1use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
20use argon2::{Algorithm, Argon2, Params, Version};
21use rand_core::OsRng;
22use webx_core::error::{Error, Result};
23use webx_core::security::IPasswordHasher;
24
25const ARGON2ID_IDENT: &str = "argon2id";
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct Argon2Params {
35 pub memory_kib: u32,
37 pub iterations: u32,
39 pub parallelism: u32,
41}
42
43impl Argon2Params {
44 pub const fn new(memory_kib: u32, iterations: u32, parallelism: u32) -> Self {
47 Self {
48 memory_kib,
49 iterations,
50 parallelism,
51 }
52 }
53
54 pub const fn owasp_default() -> Self {
56 Self::new(19 * 1024, 2, 1)
57 }
58
59 fn to_argon2_params(self) -> Result<Params> {
61 Params::new(self.memory_kib, self.iterations, self.parallelism, None).map_err(|error| {
62 Error::Internal(format!(
63 "invalid Argon2 parameters (m={}, t={}, p={}): {error}",
64 self.memory_kib, self.iterations, self.parallelism
65 ))
66 })
67 }
68}
69
70impl Default for Argon2Params {
71 fn default() -> Self {
72 Self::owasp_default()
73 }
74}
75
76#[derive(Debug, Clone, Copy, Default)]
81pub struct Argon2PasswordHasher {
82 params: Argon2Params,
83}
84
85impl Argon2PasswordHasher {
86 pub fn new() -> Self {
88 Self::default()
89 }
90
91 pub fn with_params(params: Argon2Params) -> Self {
93 Self { params }
94 }
95
96 pub fn params(&self) -> Argon2Params {
98 self.params
99 }
100}
101
102fn verify_blocking(password: &str, stored_hash: &str) -> Result<bool> {
105 let parsed = PasswordHash::new(stored_hash)
106 .map_err(|error| Error::Internal(format!("stored password hash is malformed: {error}")))?;
107
108 if parsed.salt.is_none() || parsed.hash.is_none() {
112 return Err(Error::Internal(
113 "stored password hash is malformed: missing salt or hash output".to_string(),
114 ));
115 }
116 Params::try_from(&parsed).map_err(|error| {
119 Error::Internal(format!(
120 "stored password hash parameters are invalid: {error}"
121 ))
122 })?;
123
124 let algorithm = match parsed.algorithm.as_str() {
127 ARGON2ID_IDENT => Algorithm::Argon2id,
128 "argon2i" => Algorithm::Argon2i,
129 "argon2d" => Algorithm::Argon2d,
130 other => {
131 return Err(Error::Internal(format!(
132 "stored password hash uses an unsupported algorithm '{other}'"
133 )))
134 }
135 };
136 let argon2 = Argon2::new(algorithm, Version::V0x13, Params::DEFAULT);
137
138 match argon2.verify_password(password.as_bytes(), &parsed) {
139 Ok(()) => Ok(true),
140 Err(argon2::password_hash::Error::Password) => Ok(false),
142 Err(error) => Err(Error::Internal(format!(
143 "password verification failed: {error}"
144 ))),
145 }
146}
147
148#[async_trait::async_trait]
149impl IPasswordHasher for Argon2PasswordHasher {
150 async fn hash(&self, password: &str) -> Result<String> {
151 let params = self.params;
152 let password = password.to_owned();
153 params.to_argon2_params()?;
156
157 tokio::task::spawn_blocking(move || {
158 let argon2 = Argon2::new(
159 Algorithm::Argon2id,
160 Version::V0x13,
161 params.to_argon2_params()?,
162 );
163 let salt = SaltString::generate(&mut OsRng);
164 argon2
165 .hash_password(password.as_bytes(), &salt)
166 .map(|hash| hash.to_string())
167 .map_err(|error| Error::Internal(format!("password hashing failed: {error}")))
168 })
169 .await
170 .map_err(|error| Error::Internal(format!("password hashing task failed: {error}")))?
171 }
172
173 async fn verify(&self, password: &str, stored_hash: &str) -> Result<bool> {
174 let password = password.to_owned();
175 let stored_hash = stored_hash.to_owned();
176 tokio::task::spawn_blocking(move || verify_blocking(&password, &stored_hash))
177 .await
178 .map_err(|error| {
179 Error::Internal(format!("password verification task failed: {error}"))
180 })?
181 }
182
183 fn needs_rehash(&self, stored_hash: &str) -> bool {
184 let Ok(parsed) = PasswordHash::new(stored_hash) else {
185 return true;
187 };
188 if parsed.salt.is_none()
189 || parsed.hash.is_none()
190 || parsed.algorithm.as_str() != ARGON2ID_IDENT
191 || parsed.version != Some(0x13)
192 {
193 return true;
196 }
197 let Ok(stored) = Params::try_from(&parsed) else {
198 return true;
199 };
200 stored.m_cost() != self.params.memory_kib
201 || stored.t_cost() != self.params.iterations
202 || stored.p_cost() != self.params.parallelism
203 }
204}