1#[cfg(feature = "serde")]
45use serde::{Deserialize, Serialize};
46#[cfg(feature = "base64")]
47use subtle::ConstantTimeEq;
48use zeroize::Zeroize;
49
50#[cfg(feature = "base64")]
51use crate::argon2::ARGON2_VERSION_NUMBER;
52use crate::argon2::{self, argon2_hash};
53use crate::constants::*;
54use crate::error::Error;
55
56pub(crate) const STR_HASHBYTES: usize = 32;
57
58#[cfg_attr(
59 feature = "serde",
60 derive(Zeroize, Clone, Debug, Serialize, Deserialize)
61)]
62#[cfg_attr(not(feature = "serde"), derive(Zeroize, Clone, Debug))]
63pub enum PasswordHashAlgorithm {
65 Argon2i13 = 1,
67 Argon2id13 = 2,
69}
70
71impl From<u32> for PasswordHashAlgorithm {
72 fn from(num: u32) -> Self {
73 match num {
75 num if num == PasswordHashAlgorithm::Argon2i13 as u32 => {
76 PasswordHashAlgorithm::Argon2i13
77 }
78 num if num == PasswordHashAlgorithm::Argon2id13 as u32 => {
79 PasswordHashAlgorithm::Argon2id13
80 }
81 _ => panic!("invalid password hash algorithm type: {}", num),
82 }
83 }
84}
85
86impl From<PasswordHashAlgorithm> for argon2::Argon2Type {
87 fn from(algo: PasswordHashAlgorithm) -> Self {
88 match algo {
89 PasswordHashAlgorithm::Argon2i13 => argon2::Argon2Type::Argon2i,
90 PasswordHashAlgorithm::Argon2id13 => argon2::Argon2Type::Argon2id,
91 }
92 }
93}
94
95pub fn crypto_pwhash(
116 output: &mut [u8],
117 password: &[u8],
118 salt: &[u8],
119 opslimit: u64,
120 memlimit: usize,
121 algorithm: PasswordHashAlgorithm,
122) -> Result<(), Error> {
123 validate!(
124 CRYPTO_PWHASH_OPSLIMIT_MIN,
125 CRYPTO_PWHASH_OPSLIMIT_MAX,
126 opslimit,
127 "opslimit"
128 );
129 validate!(
130 CRYPTO_PWHASH_MEMLIMIT_MIN,
131 CRYPTO_PWHASH_MEMLIMIT_MAX,
132 memlimit,
133 "memlimit"
134 );
135
136 let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
137
138 argon2_hash(
139 t_cost,
140 m_cost,
141 1,
142 password,
143 salt,
144 None,
145 None,
146 output,
147 algorithm.into(),
148 )
149}
150
151#[cfg(any(feature = "base64", all(doc, not(doctest))))]
152#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
153pub(crate) fn pwhash_to_string(t_cost: u32, m_cost: u32, salt: &[u8], hash: &[u8]) -> String {
154 use base64::Engine as _;
155 use base64::engine::general_purpose;
156
157 format!(
158 "$argon2id$v={}$m={},t={},p=1${}${}",
159 argon2::ARGON2_VERSION_NUMBER,
160 m_cost,
161 t_cost,
162 general_purpose::STANDARD_NO_PAD.encode(salt),
163 general_purpose::STANDARD_NO_PAD.encode(hash),
164 )
165}
166
167pub(crate) fn convert_costs(opslimit: u64, memlimit: usize) -> (u32, u32) {
168 (opslimit as u32, (memlimit / 1024) as u32)
169}
170
171#[cfg(any(feature = "base64", all(doc, not(doctest))))]
180#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
181pub fn crypto_pwhash_str(password: &[u8], opslimit: u64, memlimit: usize) -> Result<String, Error> {
182 validate!(
183 CRYPTO_PWHASH_OPSLIMIT_MIN,
184 CRYPTO_PWHASH_OPSLIMIT_MAX,
185 opslimit,
186 "opslimit"
187 );
188 validate!(
189 CRYPTO_PWHASH_MEMLIMIT_MIN,
190 CRYPTO_PWHASH_MEMLIMIT_MAX,
191 memlimit,
192 "memlimit"
193 );
194
195 let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
196 let mut hash = [0u8; STR_HASHBYTES];
197
198 let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
199
200 argon2_hash(
201 t_cost,
202 m_cost,
203 1,
204 password,
205 &salt,
206 None,
207 None,
208 &mut hash,
209 argon2::Argon2Type::Argon2id,
210 )?;
211
212 let pw = pwhash_to_string(t_cost, m_cost, &salt, &hash);
213
214 Ok(pw)
215}
216
217#[cfg(feature = "base64")]
218#[derive(Default)]
219pub(crate) struct Pwhash {
220 pub(crate) pwhash: Option<Vec<u8>>,
221 pub(crate) salt: Option<Vec<u8>>,
222 pub(crate) type_: Option<PasswordHashAlgorithm>,
223 pub(crate) t_cost: Option<u32>,
224 pub(crate) m_cost: Option<u32>,
225 pub(crate) parallelism: Option<u32>,
226 pub(crate) version: Option<u32>,
227}
228
229#[cfg(feature = "base64")]
230impl Pwhash {
231 pub(crate) fn parse_encoded_pwhash(hashed_password: &str) -> Result<Self, Error> {
232 use base64::Engine;
233 let mut pwhash = Pwhash::default();
234 let base64_engine = base64::engine::general_purpose::GeneralPurpose::new(
235 &base64::alphabet::STANDARD,
236 base64::engine::general_purpose::NO_PAD,
237 );
238
239 for s in hashed_password.split('$') {
240 if s.is_empty() {
241 } else if s.starts_with("argon2") {
243 match s {
244 "argon2i" => pwhash.type_ = Some(PasswordHashAlgorithm::Argon2i13),
245 "argon2id" => pwhash.type_ = Some(PasswordHashAlgorithm::Argon2id13),
246 _ => return Err(dryoc_error!(format!("invalid type: {}", s))),
247 }
248 } else if let Some(stripped) = s.strip_prefix("v=") {
249 pwhash.version = Some(
250 stripped
251 .parse::<u32>()
252 .map_err(|_| dryoc_error!("unable to decode password hash version"))?,
253 );
254 } else if s.contains("m=") && s.contains("t=") && s.contains("p=") {
255 for p in s.split(',') {
256 if let Some(m_cost) = p.strip_prefix("m=") {
257 pwhash.m_cost = Some(m_cost.parse::<u32>().map_err(|_| {
258 dryoc_error!("unable to decode password hash parameter m_cost")
259 })?);
260 } else if let Some(t_cost) = p.strip_prefix("t=") {
261 pwhash.t_cost = Some(t_cost.parse::<u32>().map_err(|_| {
262 dryoc_error!("unable to decode password hash parameter t_cost")
263 })?);
264 } else if let Some(parallelism) = p.strip_prefix("p=") {
265 pwhash.parallelism = Some(parallelism.parse::<u32>().map_err(|_| {
266 dryoc_error!("unable to decode password hash parameter t_cost")
267 })?);
268 }
269 }
270 } else if pwhash.salt.is_none() {
271 pwhash.salt = base64_engine.decode(s).ok();
272 } else if pwhash.pwhash.is_none() {
273 pwhash.pwhash = base64_engine.decode(s).ok();
274 }
275 }
276
277 if pwhash.version.is_none() || pwhash.version.unwrap() != ARGON2_VERSION_NUMBER {
279 Err(dryoc_error!("unsupported password hash"))
280 } else if pwhash.parallelism.is_none() || pwhash.parallelism.unwrap() != 1 {
282 Err(dryoc_error!("parallelism missing or invalid"))
283 } else if pwhash.pwhash.is_none() || pwhash.pwhash.as_ref().unwrap().is_empty() {
285 Err(dryoc_error!("password hash missing"))
286 } else if pwhash.salt.is_none() || pwhash.salt.as_ref().unwrap().is_empty() {
287 Err(dryoc_error!("password salt missing"))
288 } else if pwhash.type_.is_none() {
289 Err(dryoc_error!("algorithm type missing"))
290 } else if pwhash.m_cost.is_none() {
291 Err(dryoc_error!("m_cost missing"))
292 } else if pwhash.t_cost.is_none() {
293 Err(dryoc_error!("t_cost missing"))
294 } else {
295 Ok(pwhash)
296 }
297 }
298}
299
300#[cfg(any(feature = "base64", all(doc, not(doctest))))]
305#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
306pub fn crypto_pwhash_str_verify(hashed_password: &str, password: &[u8]) -> Result<(), Error> {
307 let mut hash = [0u8; STR_HASHBYTES];
308
309 let pwhash = Pwhash::parse_encoded_pwhash(hashed_password)?;
310
311 argon2_hash(
312 pwhash.t_cost.unwrap(),
313 pwhash.m_cost.unwrap(),
314 pwhash.parallelism.unwrap(),
315 password,
316 pwhash.salt.unwrap().as_ref(),
317 None,
318 None,
319 &mut hash,
320 pwhash.type_.unwrap().into(),
321 )?;
322
323 if hash.ct_eq(pwhash.pwhash.unwrap().as_ref()).unwrap_u8() == 1 {
324 Ok(())
325 } else {
326 Err(dryoc_error!("password hashes do not match"))
327 }
328}
329
330#[cfg(any(feature = "base64", all(doc, not(doctest))))]
336#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
337pub fn crypto_pwhash_str_needs_rehash(
338 hashed_password: &str,
339 opslimit: u64,
340 memlimit: usize,
341) -> Result<bool, Error> {
342 let pwhash = Pwhash::parse_encoded_pwhash(hashed_password)?;
343
344 let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
345
346 if t_cost != pwhash.t_cost.unwrap() || m_cost != pwhash.m_cost.unwrap() {
347 Ok(true)
348 } else {
349 Ok(false)
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn test_crypto_pwhash() {
359 use sodiumoxide::crypto::pwhash;
360
361 use crate::rng::copy_randombytes;
362
363 let mut hash = [0u8; 32];
364 let mut so_hash = [0u8; 32];
365 let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
366
367 copy_randombytes(&mut salt);
368
369 let password = b"donkey kong";
370
371 crypto_pwhash(
372 &mut hash,
373 password,
374 &salt,
375 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
376 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
377 PasswordHashAlgorithm::Argon2id13,
378 )
379 .expect("pwhash failed");
380
381 let _ = pwhash::argon2id13::derive_key(
382 &mut so_hash,
383 password,
384 &pwhash::argon2id13::Salt::from_slice(&salt).expect("salt failed"),
385 pwhash::argon2id13::OPSLIMIT_INTERACTIVE,
386 pwhash::argon2id13::MEMLIMIT_INTERACTIVE,
387 )
388 .expect("so pwhash failed");
389
390 assert_eq!(hash, so_hash);
391 }
392
393 #[cfg(feature = "base64")]
394 #[test]
395 fn test_crypto_pwhash_str() {
396 use sodiumoxide::crypto::pwhash;
397
398 let password = b"donkey kong";
399
400 let pwhash = crypto_pwhash_str(
401 password,
402 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
403 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
404 )
405 .expect("pwhash failed");
406
407 let mut pwhash_bytes = [0u8; CRYPTO_PWHASH_STRBYTES];
408 pwhash_bytes[..pwhash.len()].copy_from_slice(pwhash.as_bytes());
409
410 assert!(pwhash::argon2id13::pwhash_verify(
411 &pwhash::argon2id13::HashedPassword::from_slice(&pwhash_bytes)
412 .expect("hashed password failed"),
413 password,
414 ));
415 }
416
417 #[cfg(feature = "base64")]
418 #[test]
419 fn test_crypto_pwhash_str_verify() {
420 use sodiumoxide::crypto::pwhash;
421
422 let password = b"donkey kong";
423
424 let pwhash = pwhash::argon2id13::pwhash(
425 password,
426 pwhash::argon2id13::OPSLIMIT_INTERACTIVE,
427 pwhash::argon2id13::MEMLIMIT_INTERACTIVE,
428 )
429 .expect("so pwhash failed");
430
431 let pw_str = std::str::from_utf8(&pwhash.0)
432 .expect("from ut8 failed")
433 .trim_end_matches('\x00');
434
435 crypto_pwhash_str_verify(pw_str, password).expect("verify failed");
436 crypto_pwhash_str_verify(pw_str, b"invalid password")
437 .expect_err("verify should have failed");
438
439 assert!(
441 !crypto_pwhash_str_needs_rehash(
442 pw_str,
443 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
444 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
445 )
446 .expect("verify rehash failed")
447 );
448
449 assert!(
451 crypto_pwhash_str_needs_rehash(
452 pw_str,
453 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE + 1,
454 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
455 )
456 .expect("verify rehash failed")
457 );
458 }
459}