1#[cfg(any(feature = "base64", all(doc, not(doctest))))]
48use alloc::string::String;
49#[cfg(any(feature = "base64", all(doc, not(doctest))))]
50use alloc::vec::Vec;
51
52#[cfg(feature = "serde")]
53use serde::{Deserialize, Serialize};
54use zeroize::{Zeroize, Zeroizing};
55
56#[cfg(feature = "base64")]
57use crate::argon2::ARGON2_VERSION_NUMBER;
58use crate::argon2::{self, argon2_hash};
59use crate::constants::*;
60use crate::error::Error;
61use crate::utils::verify_ct;
62
63pub(crate) const STR_HASHBYTES: usize = 32;
64
65#[derive(Zeroize, Clone, Copy, Debug, Eq, PartialEq)]
66#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
67pub enum PasswordHashAlgorithm {
69 Argon2i13 = 1,
71 Argon2id13 = 2,
73}
74
75impl TryFrom<u32> for PasswordHashAlgorithm {
76 type Error = Error;
77
78 fn try_from(num: u32) -> Result<Self, Self::Error> {
79 match num {
80 num if num == PasswordHashAlgorithm::Argon2i13 as u32 => {
81 Ok(PasswordHashAlgorithm::Argon2i13)
82 }
83 num if num == PasswordHashAlgorithm::Argon2id13 as u32 => {
84 Ok(PasswordHashAlgorithm::Argon2id13)
85 }
86 _ => Err(Error::InvalidValue {
87 context: crate::ErrorContext::PasswordHashAlgorithm,
88 actual: num as u64,
89 constraint: crate::ValueConstraint::Between {
90 min: PasswordHashAlgorithm::Argon2i13 as u64,
91 max: PasswordHashAlgorithm::Argon2id13 as u64,
92 },
93 }),
94 }
95 }
96}
97
98impl From<PasswordHashAlgorithm> for argon2::Argon2Type {
99 fn from(algo: PasswordHashAlgorithm) -> Self {
100 match algo {
101 PasswordHashAlgorithm::Argon2i13 => argon2::Argon2Type::Argon2i,
102 PasswordHashAlgorithm::Argon2id13 => argon2::Argon2Type::Argon2id,
103 }
104 }
105}
106
107pub fn crypto_pwhash(
133 output: &mut [u8],
134 password: &[u8],
135 salt: &[u8],
136 opslimit: u64,
137 memlimit: usize,
138 algorithm: PasswordHashAlgorithm,
139) -> Result<(), Error> {
140 validate_pwhash_parameters(
141 output.len(),
142 password.len(),
143 salt.len(),
144 opslimit,
145 memlimit,
146 algorithm,
147 )?;
148
149 let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
150
151 argon2_hash(
152 t_cost,
153 m_cost,
154 1,
155 password,
156 salt,
157 None,
158 None,
159 output,
160 algorithm.into(),
161 )
162}
163
164pub(crate) fn validate_pwhash_parameters(
165 output_len: usize,
166 password_len: usize,
167 salt_len: usize,
168 opslimit: u64,
169 memlimit: usize,
170 algorithm: PasswordHashAlgorithm,
171) -> Result<(), Error> {
172 let (
173 bytes_min,
174 bytes_max,
175 password_max,
176 opslimit_min,
177 opslimit_max,
178 memlimit_min,
179 memlimit_max,
180 ) = match algorithm {
181 PasswordHashAlgorithm::Argon2i13 => (
182 CRYPTO_PWHASH_ARGON2I_BYTES_MIN,
183 CRYPTO_PWHASH_ARGON2I_BYTES_MAX,
184 CRYPTO_PWHASH_ARGON2I_PASSWD_MAX,
185 CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN,
186 CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MAX,
187 CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MIN,
188 CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MAX,
189 ),
190 PasswordHashAlgorithm::Argon2id13 => (
191 CRYPTO_PWHASH_ARGON2ID_BYTES_MIN,
192 CRYPTO_PWHASH_ARGON2ID_BYTES_MAX,
193 CRYPTO_PWHASH_ARGON2ID_PASSWD_MAX,
194 CRYPTO_PWHASH_ARGON2ID_OPSLIMIT_MIN,
195 CRYPTO_PWHASH_ARGON2ID_OPSLIMIT_MAX,
196 CRYPTO_PWHASH_ARGON2ID_MEMLIMIT_MIN,
197 CRYPTO_PWHASH_ARGON2ID_MEMLIMIT_MAX,
198 ),
199 };
200
201 validate_length!(
202 bytes_min,
203 bytes_max,
204 output_len,
205 crate::ErrorContext::Output
206 );
207 validate_length!(
208 CRYPTO_PWHASH_PASSWD_MIN,
209 password_max,
210 password_len,
211 crate::ErrorContext::Password
212 );
213 validate_length!(
214 exact CRYPTO_PWHASH_SALTBYTES,
215 salt_len,
216 crate::ErrorContext::PasswordHashSalt
217 );
218 validate_value!(
219 opslimit_min,
220 opslimit_max,
221 opslimit,
222 crate::ErrorContext::OperationsLimit
223 );
224 validate_value!(
225 memlimit_min,
226 memlimit_max,
227 memlimit,
228 crate::ErrorContext::MemoryLimit
229 );
230
231 Ok(())
232}
233
234#[cfg(any(feature = "base64", all(doc, not(doctest))))]
235#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
236pub(crate) fn pwhash_to_string(
237 algorithm: PasswordHashAlgorithm,
238 t_cost: u32,
239 m_cost: u32,
240 parallelism: u32,
241 salt: &[u8],
242 hash: &[u8],
243) -> String {
244 let algorithm_name = pwhash_algorithm_name(algorithm);
245 format!(
246 "${algorithm_name}$v={}$m={},t={},p={parallelism}${}${}",
247 argon2::ARGON2_VERSION_NUMBER,
248 m_cost,
249 t_cost,
250 base64_no_pad_encode(salt),
251 base64_no_pad_encode(hash),
252 )
253}
254
255#[cfg(any(feature = "base64", all(doc, not(doctest))))]
256pub(crate) fn pwhash_string_len(
257 algorithm: PasswordHashAlgorithm,
258 t_cost: u32,
259 m_cost: u32,
260 parallelism: u32,
261 salt_len: usize,
262 hash_len: usize,
263) -> Option<usize> {
264 let salt_len = base64_no_pad_encoded_len(salt_len)?;
265 let hash_len = base64_no_pad_encoded_len(hash_len)?;
266 1usize
267 .checked_add(pwhash_algorithm_name(algorithm).len())?
268 .checked_add(3 + decimal_len(ARGON2_VERSION_NUMBER))?
269 .checked_add(3 + decimal_len(m_cost))?
270 .checked_add(3 + decimal_len(t_cost))?
271 .checked_add(3 + decimal_len(parallelism))?
272 .checked_add(1)?
273 .checked_add(salt_len)?
274 .checked_add(1)?
275 .checked_add(hash_len)
276}
277
278#[cfg(any(feature = "base64", all(doc, not(doctest))))]
279const fn pwhash_algorithm_name(algorithm: PasswordHashAlgorithm) -> &'static str {
280 match algorithm {
281 PasswordHashAlgorithm::Argon2i13 => "argon2i",
282 PasswordHashAlgorithm::Argon2id13 => "argon2id",
283 }
284}
285
286#[cfg(any(feature = "base64", all(doc, not(doctest))))]
287const fn decimal_len(value: u32) -> usize {
288 if value == 0 {
289 1
290 } else {
291 value.ilog10() as usize + 1
292 }
293}
294
295#[cfg(any(feature = "base64", all(doc, not(doctest))))]
296const fn base64_no_pad_encoded_len(input_len: usize) -> Option<usize> {
297 let remainder_len = match input_len % 3 {
298 0 => 0,
299 1 => 2,
300 _ => 3,
301 };
302 match (input_len / 3).checked_mul(4) {
303 Some(full_len) => full_len.checked_add(remainder_len),
304 None => None,
305 }
306}
307
308#[cfg(any(feature = "base64", all(doc, not(doctest))))]
309fn base64_no_pad_encode(input: &[u8]) -> String {
310 const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
311
312 let mut output = String::with_capacity(input.len().div_ceil(3) * 4);
313
314 let (chunks, rem) = input.as_chunks::<3>();
315
316 for chunk in chunks {
317 let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
318 output.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
319 output.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
320 output.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
321 output.push(ALPHABET[(n & 0x3f) as usize] as char);
322 }
323
324 if rem.len() == 1 {
325 let n = (rem[0] as u32) << 16;
326 output.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
327 output.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
328 } else if rem.len() == 2 {
329 let n = ((rem[0] as u32) << 16) | ((rem[1] as u32) << 8);
330 output.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
331 output.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
332 output.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
333 }
334
335 output
336}
337
338#[cfg(feature = "base64")]
339fn base64_no_pad_decode(input: &str) -> Option<Vec<u8>> {
340 fn decode_byte(byte: u8) -> Option<u8> {
341 match byte {
342 b'A'..=b'Z' => Some(byte - b'A'),
343 b'a'..=b'z' => Some(byte - b'a' + 26),
344 b'0'..=b'9' => Some(byte - b'0' + 52),
345 b'+' => Some(62),
346 b'/' => Some(63),
347 _ => None,
348 }
349 }
350
351 let input = input.as_bytes();
352 if input.len() % 4 == 1 || input.contains(&b'=') {
353 return None;
354 }
355
356 let mut output = Vec::with_capacity(input.len() / 4 * 3 + 2);
357 let (chunks, rem) = input.as_chunks::<4>();
358
359 for chunk in chunks {
360 let n = ((decode_byte(chunk[0])? as u32) << 18)
361 | ((decode_byte(chunk[1])? as u32) << 12)
362 | ((decode_byte(chunk[2])? as u32) << 6)
363 | decode_byte(chunk[3])? as u32;
364 output.push((n >> 16) as u8);
365 output.push((n >> 8) as u8);
366 output.push(n as u8);
367 }
368
369 if rem.len() == 2 {
370 let second = decode_byte(rem[1])?;
371 if second & 0x0f != 0 {
372 return None;
373 }
374 let n = ((decode_byte(rem[0])? as u32) << 18) | ((second as u32) << 12);
375 output.push((n >> 16) as u8);
376 } else if rem.len() == 3 {
377 let third = decode_byte(rem[2])?;
378 if third & 0x03 != 0 {
379 return None;
380 }
381 let n = ((decode_byte(rem[0])? as u32) << 18)
382 | ((decode_byte(rem[1])? as u32) << 12)
383 | ((third as u32) << 6);
384 output.push((n >> 16) as u8);
385 output.push((n >> 8) as u8);
386 }
387
388 Some(output)
389}
390
391pub(crate) fn convert_costs(opslimit: u64, memlimit: usize) -> (u32, u32) {
392 (opslimit as u32, (memlimit / 1024) as u32)
393}
394
395pub(crate) fn convert_costs_checked(opslimit: u64, memlimit: usize) -> Result<(u32, u32), Error> {
396 let t_cost = u32::try_from(opslimit).map_err(|_| Error::InvalidValue {
397 context: crate::ErrorContext::OperationsLimit,
398 actual: opslimit,
399 constraint: crate::ValueConstraint::Between {
400 min: 0,
401 max: u32::MAX as u64,
402 },
403 })?;
404 let m_cost = u32::try_from(memlimit / 1024).map_err(|_| Error::InvalidValue {
405 context: crate::ErrorContext::MemoryLimit,
406 actual: memlimit as u64,
407 constraint: crate::ValueConstraint::Between {
408 min: 0,
409 max: (u32::MAX as u64) * 1024 + 1023,
410 },
411 })?;
412 Ok((t_cost, m_cost))
413}
414
415#[cfg(any(feature = "base64", all(doc, not(doctest))))]
433#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
434pub fn crypto_pwhash_str(password: &[u8], opslimit: u64, memlimit: usize) -> Result<String, Error> {
435 crypto_pwhash_str_alg(
436 password,
437 opslimit,
438 memlimit,
439 PasswordHashAlgorithm::Argon2id13,
440 )
441}
442
443#[cfg(any(feature = "base64", all(doc, not(doctest))))]
457#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
458pub fn crypto_pwhash_str_alg(
459 password: &[u8],
460 opslimit: u64,
461 memlimit: usize,
462 algorithm: PasswordHashAlgorithm,
463) -> Result<String, Error> {
464 validate_pwhash_parameters(
465 STR_HASHBYTES,
466 password.len(),
467 CRYPTO_PWHASH_SALTBYTES,
468 opslimit,
469 memlimit,
470 algorithm,
471 )?;
472
473 let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
474 let mut hash = [0u8; STR_HASHBYTES];
475 crate::rng::copy_randombytes(&mut salt);
476
477 let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
478
479 crypto_pwhash(&mut hash, password, &salt, opslimit, memlimit, algorithm)?;
480
481 let pw = pwhash_to_string(algorithm, t_cost, m_cost, 1, &salt, &hash);
482
483 Ok(pw)
484}
485
486#[cfg(feature = "base64")]
487#[derive(Default)]
488pub(crate) struct Pwhash {
489 pub(crate) pwhash: Option<Vec<u8>>,
490 pub(crate) salt: Option<Vec<u8>>,
491 pub(crate) type_: Option<PasswordHashAlgorithm>,
492 pub(crate) t_cost: Option<u32>,
493 pub(crate) m_cost: Option<u32>,
494 pub(crate) parallelism: Option<u32>,
495}
496
497#[cfg(feature = "base64")]
498impl Pwhash {
499 pub(crate) fn parse_encoded_pwhash(hashed_password: &str) -> Result<Self, Error> {
500 let encoded = hashed_password
501 .strip_prefix('$')
502 .ok_or_else(|| Error::invalid_encoding(crate::ErrorContext::PasswordHash))?;
503 let mut fields = encoded.split('$');
504
505 let algorithm = match fields.next().filter(|field| !field.is_empty()) {
506 Some("argon2i") => PasswordHashAlgorithm::Argon2i13,
507 Some("argon2id") => PasswordHashAlgorithm::Argon2id13,
508 Some(field) if field.starts_with("v=") => {
509 return Err(Error::missing_data(
510 crate::ErrorContext::PasswordHashAlgorithm,
511 ));
512 }
513 Some(_) => {
514 return Err(Error::invalid_encoding(
515 crate::ErrorContext::PasswordHashAlgorithm,
516 ));
517 }
518 None => {
519 return Err(Error::missing_data(
520 crate::ErrorContext::PasswordHashAlgorithm,
521 ));
522 }
523 };
524
525 let version = fields
526 .next()
527 .ok_or(Error::missing_data(
528 crate::ErrorContext::PasswordHashVersion,
529 ))?
530 .strip_prefix("v=")
531 .ok_or(Error::invalid_encoding(
532 crate::ErrorContext::PasswordHashVersion,
533 ))?;
534 let version =
535 parse_minimal_pwhash_decimal(version, crate::ErrorContext::PasswordHashVersion)?;
536 if version != ARGON2_VERSION_NUMBER {
537 return Err(Error::invalid_encoding(
538 crate::ErrorContext::PasswordHashVersion,
539 ));
540 }
541
542 let parameters = fields.next().ok_or(Error::missing_data(
543 crate::ErrorContext::PasswordHashMemoryCost,
544 ))?;
545 let mut parameters = parameters.split(',');
546 let m_cost = parse_pwhash_parameter(
547 parameters.next(),
548 "m=",
549 crate::ErrorContext::PasswordHashMemoryCost,
550 )?;
551 let t_cost = parse_pwhash_parameter(
552 parameters.next(),
553 "t=",
554 crate::ErrorContext::PasswordHashTimeCost,
555 )?;
556 let parallelism = parse_pwhash_parameter(
557 parameters.next(),
558 "p=",
559 crate::ErrorContext::PasswordHashParallelism,
560 )?;
561 if parameters.next().is_some() {
562 return Err(Error::invalid_encoding(crate::ErrorContext::PasswordHash));
563 }
564
565 let salt = fields
566 .next()
567 .filter(|field| !field.is_empty())
568 .ok_or(Error::missing_data(crate::ErrorContext::PasswordHashSalt))?;
569 let salt = base64_no_pad_decode(salt).ok_or(Error::invalid_encoding(
570 crate::ErrorContext::PasswordHashSalt,
571 ))?;
572
573 let pwhash = fields
574 .next()
575 .filter(|field| !field.is_empty())
576 .ok_or(Error::missing_data(crate::ErrorContext::PasswordHash))?;
577 let pwhash = base64_no_pad_decode(pwhash)
578 .ok_or(Error::invalid_encoding(crate::ErrorContext::PasswordHash))?;
579
580 if fields.next().is_some() {
581 return Err(Error::invalid_encoding(crate::ErrorContext::PasswordHash));
582 }
583
584 crate::argon2::validate_argon2_pwhash_parameters(
585 pwhash.len(),
586 salt.len(),
587 t_cost,
588 m_cost,
589 parallelism,
590 )?;
591
592 Ok(Self {
593 pwhash: Some(pwhash),
594 salt: Some(salt),
595 type_: Some(algorithm),
596 t_cost: Some(t_cost),
597 m_cost: Some(m_cost),
598 parallelism: Some(parallelism),
599 })
600 }
601}
602
603#[cfg(feature = "base64")]
604fn parse_pwhash_parameter(
605 parameter: Option<&str>,
606 prefix: &str,
607 context: crate::ErrorContext,
608) -> Result<u32, Error> {
609 let value = parameter
610 .ok_or(Error::missing_data(context))?
611 .strip_prefix(prefix)
612 .ok_or(Error::invalid_encoding(context))?;
613 parse_minimal_pwhash_decimal(value, context)
614}
615
616#[cfg(feature = "base64")]
617fn parse_minimal_pwhash_decimal(value: &str, context: crate::ErrorContext) -> Result<u32, Error> {
618 if value.is_empty()
619 || !value.bytes().all(|byte| byte.is_ascii_digit())
620 || (value.len() > 1 && value.starts_with('0'))
621 {
622 return Err(Error::invalid_encoding(context));
623 }
624 value
625 .parse::<u32>()
626 .map_err(|_| Error::invalid_encoding(context))
627}
628
629#[cfg(any(feature = "base64", all(doc, not(doctest))))]
639#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
640pub fn crypto_pwhash_str_verify(hashed_password: &str, password: &[u8]) -> Result<(), Error> {
641 let pwhash = Pwhash::parse_encoded_pwhash(hashed_password)?;
642 let t_cost = pwhash.t_cost.ok_or(Error::missing_data(
643 crate::ErrorContext::PasswordHashTimeCost,
644 ))?;
645 let m_cost = pwhash.m_cost.ok_or(Error::missing_data(
646 crate::ErrorContext::PasswordHashMemoryCost,
647 ))?;
648 let parallelism = pwhash.parallelism.ok_or(Error::missing_data(
649 crate::ErrorContext::PasswordHashParallelism,
650 ))?;
651 let salt = pwhash
652 .salt
653 .ok_or(Error::missing_data(crate::ErrorContext::PasswordHashSalt))?;
654 let algorithm = pwhash.type_.ok_or(Error::missing_data(
655 crate::ErrorContext::PasswordHashAlgorithm,
656 ))?;
657 let expected_hash = pwhash
658 .pwhash
659 .ok_or(Error::missing_data(crate::ErrorContext::PasswordHash))?;
660
661 verify_pwhash_parts(
662 &expected_hash,
663 password,
664 &salt,
665 t_cost,
666 m_cost,
667 parallelism,
668 algorithm,
669 )
670}
671
672pub(crate) fn verify_pwhash_parts(
673 expected_hash: &[u8],
674 password: &[u8],
675 salt: &[u8],
676 t_cost: u32,
677 m_cost: u32,
678 parallelism: u32,
679 algorithm: PasswordHashAlgorithm,
680) -> Result<(), Error> {
681 let mut hash = Zeroizing::new(vec![0u8; expected_hash.len()]);
682 argon2_hash(
683 t_cost,
684 m_cost,
685 parallelism,
686 password,
687 salt,
688 None,
689 None,
690 &mut hash,
691 algorithm.into(),
692 )?;
693
694 verify_ct(hash.as_slice(), expected_hash)
695}
696
697#[cfg(any(feature = "base64", all(doc, not(doctest))))]
708#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
709pub fn crypto_pwhash_str_needs_rehash(
710 hashed_password: &str,
711 opslimit: u64,
712 memlimit: usize,
713) -> Result<bool, Error> {
714 validate_length!(
715 max CRYPTO_PWHASH_STRBYTES - 1,
716 hashed_password.len(),
717 crate::ErrorContext::PasswordHash
718 );
719 let (t_cost, m_cost) = convert_costs_checked(opslimit, memlimit)?;
720 let pwhash = Pwhash::parse_encoded_pwhash(hashed_password)?;
721 let parsed_t_cost = pwhash.t_cost.ok_or(Error::missing_data(
722 crate::ErrorContext::PasswordHashTimeCost,
723 ))?;
724 let parsed_m_cost = pwhash.m_cost.ok_or(Error::missing_data(
725 crate::ErrorContext::PasswordHashMemoryCost,
726 ))?;
727
728 if t_cost != parsed_t_cost || m_cost != parsed_m_cost {
729 Ok(true)
730 } else {
731 Ok(false)
732 }
733}
734
735#[cfg(test)]
736mod tests {
737 #[cfg(feature = "base64")]
738 use alloc::borrow::ToOwned;
739
740 use super::*;
741
742 #[cfg(dryoc_native_tests)]
743 #[test]
744 fn test_crypto_pwhash() {
745 use crate::native_test_util::pwhash_argon2id;
746 use crate::rng::copy_randombytes;
747
748 crate::native_test_util::init();
749
750 let mut hash = [0u8; 32];
751 let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
752
753 copy_randombytes(&mut salt);
754
755 let password = b"donkey kong";
756
757 crypto_pwhash(
758 &mut hash,
759 password,
760 &salt,
761 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
762 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
763 PasswordHashAlgorithm::Argon2id13,
764 )
765 .expect("pwhash failed");
766
767 let so_hash: [u8; 32] = pwhash_argon2id(
768 password,
769 &salt,
770 libsodium_sys::crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE.into(),
771 libsodium_sys::crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE as usize,
772 );
773
774 assert_eq!(hash, so_hash);
775 }
776
777 #[cfg(feature = "base64")]
778 #[test]
779 fn test_base64_no_pad_matches_base64_crate() {
780 use base64::Engine as _;
781 use base64::engine::general_purpose;
782
783 for len in 0..128 {
784 let input: Vec<u8> = (0..len).map(|i| (i * 31 + len) as u8).collect();
785 let encoded = base64_no_pad_encode(&input);
786 assert_eq!(encoded, general_purpose::STANDARD_NO_PAD.encode(&input));
787 assert_eq!(
788 base64_no_pad_decode(&encoded).as_deref(),
789 Some(input.as_slice())
790 );
791 }
792
793 assert_eq!(base64_no_pad_decode("A"), None);
794 assert_eq!(base64_no_pad_decode("AA="), None);
795 assert_eq!(base64_no_pad_decode("A/"), None);
796 assert_eq!(base64_no_pad_decode("AA/"), None);
797
798 let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
799 let hash = [0u8; STR_HASHBYTES];
800 let encoded = pwhash_to_string(
801 PasswordHashAlgorithm::Argon2id13,
802 2,
803 65_536,
804 1,
805 &salt,
806 &hash,
807 );
808 assert_eq!(
809 pwhash_string_len(
810 PasswordHashAlgorithm::Argon2id13,
811 2,
812 65_536,
813 1,
814 salt.len(),
815 hash.len(),
816 ),
817 Some(encoded.len())
818 );
819
820 assert_eq!(
821 pwhash_string_len(
822 PasswordHashAlgorithm::Argon2id13,
823 2,
824 65_536,
825 1,
826 usize::MAX,
827 0
828 ),
829 None,
830 );
831 #[cfg(target_pointer_width = "32")]
832 assert_eq!(
833 pwhash_string_len(
834 PasswordHashAlgorithm::Argon2id13,
835 2,
836 65_536,
837 1,
838 3_221_225_471,
839 0,
840 ),
841 None,
842 );
843 }
844
845 #[cfg(feature = "base64")]
846 #[test]
847 fn malformed_password_hash_fields_have_structured_errors() {
848 const SALT: &str = "AAAAAAAAAAA";
849 const HASH: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
850 let cases = [
851 (
852 format!("$argon2wat$v=19$m=65536,t=2,p=1${SALT}${HASH}"),
853 crate::ErrorContext::PasswordHashAlgorithm,
854 ),
855 (
856 format!("$argon2id$v=nope$m=65536,t=2,p=1${SALT}${HASH}"),
857 crate::ErrorContext::PasswordHashVersion,
858 ),
859 (
860 format!("$argon2id$v=19$m=nope,t=2,p=1${SALT}${HASH}"),
861 crate::ErrorContext::PasswordHashMemoryCost,
862 ),
863 (
864 format!("$argon2id$v=19$m=65536,t=nope,p=1${SALT}${HASH}"),
865 crate::ErrorContext::PasswordHashTimeCost,
866 ),
867 (
868 format!("$argon2id$v=19$m=65536,t=2,p=nope${SALT}${HASH}"),
869 crate::ErrorContext::PasswordHashParallelism,
870 ),
871 (
872 format!("$argon2id$v=19$m=65536,t=2,p=1$A${HASH}"),
873 crate::ErrorContext::PasswordHashSalt,
874 ),
875 (
876 format!("$argon2id$v=19$m=65536,t=2,p=1${SALT}$A"),
877 crate::ErrorContext::PasswordHash,
878 ),
879 (
880 format!("$argon2id$v=18$m=65536,t=2,p=1${SALT}${HASH}"),
881 crate::ErrorContext::PasswordHashVersion,
882 ),
883 (
884 format!("$argon2id$v=019$m=65536,t=2,p=1${SALT}${HASH}"),
885 crate::ErrorContext::PasswordHashVersion,
886 ),
887 (
888 format!("$argon2id$v=19$m=065536,t=2,p=1${SALT}${HASH}"),
889 crate::ErrorContext::PasswordHashMemoryCost,
890 ),
891 (
892 format!("$argon2id$v=19$m=65536,t=+2,p=1${SALT}${HASH}"),
893 crate::ErrorContext::PasswordHashTimeCost,
894 ),
895 (
896 format!("$argon2id$v=19$m=65536,t=2,p=01${SALT}${HASH}"),
897 crate::ErrorContext::PasswordHashParallelism,
898 ),
899 ];
900
901 for (encoded, expected_context) in cases {
902 let error = match Pwhash::parse_encoded_pwhash(&encoded) {
903 Ok(_) => panic!("the malformed field should be rejected"),
904 Err(error) => error,
905 };
906 assert!(matches!(
907 error,
908 Error::InvalidEncoding { context } if context == expected_context
909 ));
910 }
911
912 let missing_hash = format!("$argon2id$v=19$m=65536,t=2,p=1${SALT}");
913 assert!(matches!(
914 Pwhash::parse_encoded_pwhash(&missing_hash),
915 Err(Error::MissingData {
916 context: crate::ErrorContext::PasswordHash,
917 })
918 ));
919
920 let missing_algorithm = format!("$v=19$m=65536,t=2,p=1${SALT}${HASH}");
921 assert!(matches!(
922 Pwhash::parse_encoded_pwhash(&missing_algorithm),
923 Err(Error::MissingData {
924 context: crate::ErrorContext::PasswordHashAlgorithm,
925 })
926 ));
927 }
928
929 #[test]
930 fn password_hashing_reports_invalid_resource_limits() {
931 let mut output = [0u8; CRYPTO_PWHASH_BYTES_MIN];
932 let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
933 let password = b"password";
934
935 for (opslimit, memlimit, expected_context) in [
936 (
937 CRYPTO_PWHASH_OPSLIMIT_MIN - 1,
938 CRYPTO_PWHASH_MEMLIMIT_MIN,
939 crate::ErrorContext::OperationsLimit,
940 ),
941 (
942 CRYPTO_PWHASH_OPSLIMIT_MIN,
943 CRYPTO_PWHASH_MEMLIMIT_MIN - 1,
944 crate::ErrorContext::MemoryLimit,
945 ),
946 ] {
947 let error = crypto_pwhash(
948 &mut output,
949 password,
950 &salt,
951 opslimit,
952 memlimit,
953 PasswordHashAlgorithm::Argon2id13,
954 )
955 .expect_err("invalid resource limits should fail");
956 assert!(matches!(
957 error,
958 Error::InvalidValue { context, .. } if context == expected_context
959 ));
960
961 #[cfg(feature = "base64")]
962 {
963 let error = crypto_pwhash_str(password, opslimit, memlimit)
964 .expect_err("invalid resource limits should fail");
965 assert!(matches!(
966 error,
967 Error::InvalidValue { context, .. } if context == expected_context
968 ));
969 }
970 }
971 }
972
973 #[test]
976 fn invalid_parameters_leave_output_unchanged() {
977 let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
978 let password = b"password";
979 let sentinel = [0xa5u8; CRYPTO_PWHASH_BYTES_MIN];
980
981 let mut too_short = [0xa5u8; CRYPTO_PWHASH_BYTES_MIN - 1];
982 assert!(
983 crypto_pwhash(
984 &mut too_short,
985 password,
986 &salt,
987 CRYPTO_PWHASH_OPSLIMIT_MIN,
988 CRYPTO_PWHASH_MEMLIMIT_MIN,
989 PasswordHashAlgorithm::Argon2id13,
990 )
991 .is_err()
992 );
993 assert_eq!(too_short, [0xa5u8; CRYPTO_PWHASH_BYTES_MIN - 1]);
994
995 for case in [0usize, 1, 2, 3] {
996 let mut output = sentinel;
997 let result = match case {
998 0 => crypto_pwhash(
999 &mut output,
1000 password,
1001 &salt[..CRYPTO_PWHASH_SALTBYTES - 1],
1002 CRYPTO_PWHASH_OPSLIMIT_MIN,
1003 CRYPTO_PWHASH_MEMLIMIT_MIN,
1004 PasswordHashAlgorithm::Argon2id13,
1005 ),
1006 1 => crypto_pwhash(
1007 &mut output,
1008 password,
1009 &salt,
1010 CRYPTO_PWHASH_OPSLIMIT_MIN - 1,
1011 CRYPTO_PWHASH_MEMLIMIT_MIN,
1012 PasswordHashAlgorithm::Argon2id13,
1013 ),
1014 2 => crypto_pwhash(
1015 &mut output,
1016 password,
1017 &salt,
1018 CRYPTO_PWHASH_OPSLIMIT_MIN,
1019 CRYPTO_PWHASH_MEMLIMIT_MIN - 1,
1020 PasswordHashAlgorithm::Argon2id13,
1021 ),
1022 _ => crypto_pwhash(
1023 &mut output,
1024 password,
1025 &salt,
1026 CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN - 1,
1027 CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MIN,
1028 PasswordHashAlgorithm::Argon2i13,
1029 ),
1030 };
1031 assert!(result.is_err(), "case {case}");
1032 assert_eq!(output, sentinel, "case {case}");
1033 }
1034 }
1035 #[test]
1036 fn password_hashing_enforces_classic_parameter_contract() {
1037 let mut output = [0u8; CRYPTO_PWHASH_BYTES_MIN];
1038 let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
1039
1040 for opslimit in 1..CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN {
1041 assert!(matches!(
1042 crypto_pwhash(
1043 &mut output,
1044 b"password",
1045 &salt,
1046 opslimit,
1047 CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MIN,
1048 PasswordHashAlgorithm::Argon2i13,
1049 ),
1050 Err(Error::InvalidValue {
1051 context: crate::ErrorContext::OperationsLimit,
1052 ..
1053 })
1054 ));
1055 }
1056
1057 assert!(matches!(
1058 crypto_pwhash(
1059 &mut output,
1060 b"password",
1061 &salt[..CRYPTO_PWHASH_SALTBYTES - 1],
1062 CRYPTO_PWHASH_OPSLIMIT_MIN,
1063 CRYPTO_PWHASH_MEMLIMIT_MIN,
1064 PasswordHashAlgorithm::Argon2id13,
1065 ),
1066 Err(Error::InvalidLength {
1067 context: crate::ErrorContext::PasswordHashSalt,
1068 constraint: crate::LengthConstraint::Exact(CRYPTO_PWHASH_SALTBYTES),
1069 ..
1070 })
1071 ));
1072
1073 assert!(PasswordHashAlgorithm::try_from(0).is_err());
1074 assert_eq!(
1075 PasswordHashAlgorithm::try_from(CRYPTO_PWHASH_ALG_ARGON2ID13 as u32)
1076 .expect("valid algorithm"),
1077 PasswordHashAlgorithm::Argon2id13
1078 );
1079 }
1080
1081 #[cfg(dryoc_native_tests)]
1082 #[cfg(feature = "base64")]
1083 #[test]
1084 fn test_crypto_pwhash_str() {
1085 use crate::native_test_util::pwhash_argon2id_str_verify;
1086
1087 let password = b"donkey kong";
1088
1089 let pwhash = crypto_pwhash_str(
1090 password,
1091 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1092 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
1093 )
1094 .expect("pwhash failed");
1095 let pwhash2 = crypto_pwhash_str(
1096 password,
1097 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1098 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
1099 )
1100 .expect("pwhash failed");
1101
1102 let parsed = Pwhash::parse_encoded_pwhash(&pwhash).expect("couldn't parse pwhash");
1103 let parsed2 = Pwhash::parse_encoded_pwhash(&pwhash2).expect("couldn't parse pwhash");
1104
1105 assert_ne!(
1106 parsed.salt.as_ref().expect("missing salt"),
1107 &vec![0u8; CRYPTO_PWHASH_SALTBYTES]
1108 );
1109 assert_ne!(parsed.salt, parsed2.salt);
1110
1111 let mut pwhash_bytes = [0u8; CRYPTO_PWHASH_STRBYTES];
1112 pwhash_bytes[..pwhash.len()].copy_from_slice(pwhash.as_bytes());
1113
1114 assert!(pwhash_argon2id_str_verify(&pwhash_bytes, password));
1115
1116 let argon2i = crypto_pwhash_str_alg(
1117 password,
1118 CRYPTO_PWHASH_ARGON2I_OPSLIMIT_INTERACTIVE,
1119 CRYPTO_PWHASH_ARGON2I_MEMLIMIT_INTERACTIVE,
1120 PasswordHashAlgorithm::Argon2i13,
1121 )
1122 .expect("argon2i pwhash failed");
1123 assert!(argon2i.starts_with(CRYPTO_PWHASH_ARGON2I_STRPREFIX));
1124 crypto_pwhash_str_verify(&argon2i, password).expect("argon2i verify failed");
1125 }
1126
1127 #[cfg(dryoc_native_tests)]
1128 #[cfg(feature = "base64")]
1129 #[test]
1130 fn test_crypto_pwhash_str_verify() {
1131 use crate::native_test_util::pwhash_argon2id_str;
1132
1133 crate::native_test_util::init();
1134
1135 let password = b"donkey kong";
1136
1137 let pwhash = pwhash_argon2id_str(
1138 password,
1139 libsodium_sys::crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE.into(),
1140 libsodium_sys::crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE as usize,
1141 );
1142
1143 let pw_str = core::str::from_utf8(&pwhash)
1144 .expect("from ut8 failed")
1145 .trim_end_matches('\x00');
1146
1147 crypto_pwhash_str_verify(pw_str, password).expect("verify failed");
1148 crypto_pwhash_str_verify(pw_str, b"invalid password")
1149 .expect_err("verify should have failed");
1150
1151 for encoded in [
1152 concat!(
1153 "$argon2id$v=19$m=256,t=3,p=1$MDEyMzQ1Njc$",
1154 "G5ajKFCoUzaXRLdz7UJb5wGkb2Xt+X5/GQjUYtS2+TE",
1155 ),
1156 concat!(
1157 "$argon2i$v=19$m=4096,t=3,p=2$b2RpZHVlamRpc29kaXNrdw$",
1158 "TNnWIwlu1061JHrnCqIAmjs3huSxYIU+0jWipu7Kc9M",
1159 ),
1160 ] {
1161 crypto_pwhash_str_verify(encoded, b"password")
1162 .expect("valid libsodium Argon2 vector should verify");
1163 }
1164
1165 assert!(crypto_pwhash_str_verify(&format!("{pw_str}$garbage"), password).is_err());
1166 assert!(crypto_pwhash_str_verify(pw_str.trim_start_matches('$'), password).is_err());
1167 for invalid_parallelism in ["0", "4294967295"] {
1168 let malformed = pw_str.replace(",p=1", &format!(",p={invalid_parallelism}"));
1169 assert!(crypto_pwhash_str_verify(&malformed, password).is_err());
1170 assert!(
1171 crypto_pwhash_str_needs_rehash(
1172 &malformed,
1173 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1174 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
1175 )
1176 .is_err()
1177 );
1178 }
1179
1180 assert!(
1182 !crypto_pwhash_str_needs_rehash(
1183 pw_str,
1184 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1185 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
1186 )
1187 .expect("verify rehash failed")
1188 );
1189
1190 assert!(
1192 crypto_pwhash_str_needs_rehash(
1193 pw_str,
1194 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE + 1,
1195 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
1196 )
1197 .expect("verify rehash failed")
1198 );
1199
1200 assert!(
1201 crypto_pwhash_str_needs_rehash(pw_str, 0, 0,)
1202 .expect("zero costs are a valid rehash comparison")
1203 );
1204
1205 assert!(matches!(
1206 crypto_pwhash_str_needs_rehash(pw_str, u32::MAX as u64 + 1, 0),
1207 Err(Error::InvalidValue {
1208 context: crate::ErrorContext::OperationsLimit,
1209 ..
1210 })
1211 ));
1212 }
1213
1214 #[cfg(feature = "base64")]
1215 const FIXED_PASSWORD_HASH: &str = concat!(
1216 "$argon2id$v=19$m=8,t=1,p=1$9ekTPbzKHDiIpMlbQaGlhw$",
1217 "hNvEox1vwA2JfhZp1wZR15ZgoNTLZbWZo6XJcN6naXw",
1218 );
1219 #[cfg(feature = "base64")]
1220 const FIXED_PASSWORD_HASH_M16: &str = concat!(
1221 "$argon2id$v=19$m=16,t=1,p=1$s1CQM+Z7bXo6yOSiVIBY8A$",
1222 "Qx7ztSAZoE3T52niRU2LRwiV4hVtE7XHI+GxwYep4D0",
1223 );
1224
1225 #[cfg(feature = "base64")]
1226 fn exact_max_password_hash() -> String {
1227 let salt = [0x42u8; 8];
1228 let mut hash = [0u8; 66];
1229 argon2_hash(
1230 1,
1231 8,
1232 1,
1233 b"password",
1234 &salt,
1235 None,
1236 None,
1237 &mut hash,
1238 PasswordHashAlgorithm::Argon2id13.into(),
1239 )
1240 .expect("argon2");
1241 let encoded = pwhash_to_string(PasswordHashAlgorithm::Argon2id13, 1, 8, 1, &salt, &hash);
1242 assert_eq!(encoded.len(), CRYPTO_PWHASH_STRBYTES - 1);
1243 encoded
1244 }
1245
1246 #[cfg(feature = "base64")]
1247 fn mutated_password_hashes() -> Vec<(&'static str, String, bool)> {
1248 let valid = FIXED_PASSWORD_HASH;
1249 let mut cases = Vec::new();
1250 for (i, _) in valid.match_indices('$') {
1251 cases.push(("truncated before $", valid[..i].to_owned(), false));
1252 cases.push(("truncated after $", valid[..=i].to_owned(), false));
1253 }
1254 cases.extend([
1255 (
1256 "wrong prefix",
1257 valid.replacen("$argon2id$", "$argon2wat$", 1),
1258 false,
1259 ),
1260 ("extra field", format!("{valid}$extra"), false),
1261 (
1262 "non-canonical salt pad bits",
1263 valid.replace("9ekTPbzKHDiIpMlbQaGlhw", "9ekTPbzKHDiIpMlbQaGlhx"),
1264 false,
1265 ),
1266 (
1267 "non-canonical hash pad bits",
1268 format!("{}x", &valid[..valid.len() - 1]),
1269 false,
1270 ),
1271 ("m overflow", valid.replace("m=8", "m=4294967296"), false),
1272 ("t overflow", valid.replace("t=1", "t=4294967296"), false),
1273 ("p overflow", valid.replace("p=1", "p=4294967296"), false),
1274 ("m below one KiB block", valid.replace("m=8", "m=7"), false),
1275 ]);
1276 cases
1277 }
1278
1279 #[cfg(feature = "base64")]
1282 #[test]
1283 fn fixed_and_mutated_password_hashes_have_expected_results() {
1284 Pwhash::parse_encoded_pwhash(FIXED_PASSWORD_HASH).expect("fixed string parses");
1285 crypto_pwhash_str_verify(FIXED_PASSWORD_HASH, b"password").expect("fixed string verifies");
1286 crypto_pwhash_str_verify(FIXED_PASSWORD_HASH, b"wrong").expect_err("wrong password");
1287 assert!(!crypto_pwhash_str_needs_rehash(FIXED_PASSWORD_HASH, 1, 8192).expect("rehash"));
1288 assert!(crypto_pwhash_str_needs_rehash(FIXED_PASSWORD_HASH, 2, 8192).expect("rehash"));
1289
1290 for (name, encoded, valid) in mutated_password_hashes() {
1291 assert_eq!(
1292 Pwhash::parse_encoded_pwhash(&encoded).is_ok(),
1293 valid,
1294 "parse: {name}"
1295 );
1296 assert!(
1297 crypto_pwhash_str_verify(&encoded, b"password").is_err(),
1298 "verify: {name}"
1299 );
1300 assert!(
1301 crypto_pwhash_str_needs_rehash(&encoded, 1, 8192).is_err(),
1302 "needs_rehash: {name}"
1303 );
1304 }
1305
1306 let salt = [0x5au8; 16];
1309 let mut hash = [0u8; 64];
1310 argon2_hash(
1311 1,
1312 8,
1313 1,
1314 b"password",
1315 &salt,
1316 None,
1317 None,
1318 &mut hash,
1319 PasswordHashAlgorithm::Argon2id13.into(),
1320 )
1321 .expect("derive long hash");
1322 let long = pwhash_to_string(PasswordHashAlgorithm::Argon2id13, 1, 8, 1, &salt, &hash);
1323 assert_eq!(long.len(), 136);
1324 Pwhash::parse_encoded_pwhash(&long).expect("long hash parses");
1325 crypto_pwhash_str_verify(&long, b"password").expect("long hash verifies");
1326 assert!(crypto_pwhash_str_needs_rehash(&long, 1, 8192).is_err());
1327 let encoded = exact_max_password_hash();
1328 Pwhash::parse_encoded_pwhash(&encoded).expect("max string parses");
1329 crypto_pwhash_str_verify(&encoded, b"password").expect("max string verifies");
1330 assert!(!crypto_pwhash_str_needs_rehash(&encoded, 1, 8192).expect("rehash"));
1331 let argon2i = FIXED_PASSWORD_HASH.replacen("$argon2id$", "$argon2i$", 1);
1332
1333 Pwhash::parse_encoded_pwhash(&argon2i).expect("algorithm mutation parses");
1334 assert!(crypto_pwhash_str_verify(&argon2i, b"password").is_err());
1335 assert!(!crypto_pwhash_str_needs_rehash(&argon2i, 1, 8192).expect("rehash"));
1336
1337 let parallel = FIXED_PASSWORD_HASH_M16.replace("p=1", "p=2");
1338 Pwhash::parse_encoded_pwhash(¶llel).expect("parallelism mutation parses");
1339 assert!(crypto_pwhash_str_verify(¶llel, b"password").is_err());
1340 assert!(!crypto_pwhash_str_needs_rehash(¶llel, 1, 16_384).expect("rehash"));
1341 }
1342
1343 #[cfg(all(feature = "base64", dryoc_native_tests))]
1346 #[test]
1347 fn mutation_matrix_matches_libsodium() {
1348 use crate::native_test_util::{pwhash_str_needs_rehash, pwhash_str_verify};
1349
1350 for (name, encoded, _) in mutated_password_hashes() {
1351 let sodium_verify = pwhash_str_verify(&encoded, b"password");
1352 let sodium_rehash = pwhash_str_needs_rehash(&encoded, 1, 8192);
1353 assert_eq!(
1354 crypto_pwhash_str_verify(&encoded, b"password").is_ok(),
1355 sodium_verify,
1356 "verify: {name}"
1357 );
1358 let ours_rehash = crypto_pwhash_str_needs_rehash(&encoded, 1, 8192);
1359 assert_eq!(
1360 ours_rehash.is_err(),
1361 sodium_rehash.is_err(),
1362 "rehash validity: {name}"
1363 );
1364 if let Ok(ours_rehash) = ours_rehash {
1365 assert_eq!(
1366 ours_rehash,
1367 sodium_rehash == Ok(true),
1368 "rehash result: {name}"
1369 );
1370 }
1371 }
1372 }
1373
1374 #[cfg(all(feature = "base64", dryoc_native_tests))]
1375 #[test]
1376 fn algorithm_and_parallelism_mutations_match_libsodium() {
1377 use crate::native_test_util::{pwhash_str_needs_rehash, pwhash_str_verify};
1378
1379 for (encoded, memlimit) in [
1380 (
1381 FIXED_PASSWORD_HASH.replacen("$argon2id$", "$argon2i$", 1),
1382 8192,
1383 ),
1384 (FIXED_PASSWORD_HASH_M16.replace("p=1", "p=2"), 16_384),
1385 ] {
1386 assert_eq!(
1387 crypto_pwhash_str_verify(&encoded, b"password").is_ok(),
1388 pwhash_str_verify(&encoded, b"password")
1389 );
1390 assert_eq!(
1391 crypto_pwhash_str_needs_rehash(&encoded, 1, memlimit).expect("rehash"),
1392 pwhash_str_needs_rehash(&encoded, 1, memlimit) == Ok(true)
1393 );
1394 }
1395 }
1396 #[cfg(all(feature = "base64", dryoc_native_tests))]
1399 #[test]
1400 fn exact_maximum_encoded_length_matches_libsodium() {
1401 use crate::native_test_util::{pwhash_str_needs_rehash, pwhash_str_verify};
1402
1403 let encoded = exact_max_password_hash();
1404 crypto_pwhash_str_verify(&encoded, b"password").expect("dryoc verifies");
1410 assert!(
1411 pwhash_str_verify(&encoded, b"password"),
1412 "libsodium verifies the 66-byte hash"
1413 );
1414 assert_eq!(
1415 crypto_pwhash_str_needs_rehash(&encoded, 1, 8192).expect("rehash"),
1416 pwhash_str_needs_rehash(&encoded, 1, 8192) == Ok(true)
1417 );
1418 }
1419
1420 #[cfg(all(feature = "base64", dryoc_native_tests))]
1422 #[test]
1423 fn longer_encoded_hash_verifies_like_libsodium() {
1424 let password = b"long hash test password";
1425 let salt = [0x5au8; 16];
1426 let hash = crate::native_test_util::pwhash_argon2id::<64>(password, &salt, 1, 8192);
1427 let encoded = pwhash_to_string(PasswordHashAlgorithm::Argon2id13, 1, 8, 1, &salt, &hash);
1428 assert_eq!(encoded.len(), 136);
1429
1430 let parsed = Pwhash::parse_encoded_pwhash(&encoded).expect("long hash parses");
1431 assert_eq!(parsed.pwhash.as_deref(), Some(hash.as_slice()));
1432 crypto_pwhash_str_verify(&encoded, password).expect("dryoc verifies long hash");
1433
1434 assert!(
1435 crate::native_test_util::pwhash_str_verify(&encoded, password),
1436 "libsodium verifies the 64-byte hash"
1437 );
1438 }
1439}