1#[cfg(feature = "serde")]
45use serde::{Deserialize, Serialize};
46use subtle::ConstantTimeEq;
47use zeroize::{Zeroize, Zeroizing};
48
49#[cfg(feature = "base64")]
50use crate::argon2::ARGON2_VERSION_NUMBER;
51use crate::argon2::{self, argon2_hash};
52use crate::constants::*;
53use crate::error::Error;
54
55pub(crate) const STR_HASHBYTES: usize = 32;
56
57#[cfg_attr(
58 feature = "serde",
59 derive(Zeroize, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)
60)]
61#[cfg_attr(
62 not(feature = "serde"),
63 derive(Zeroize, Clone, Copy, Debug, Eq, PartialEq)
64)]
65pub enum PasswordHashAlgorithm {
67 Argon2i13 = 1,
69 Argon2id13 = 2,
71}
72
73impl TryFrom<u32> for PasswordHashAlgorithm {
74 type Error = Error;
75
76 fn try_from(num: u32) -> Result<Self, Self::Error> {
77 match num {
78 num if num == PasswordHashAlgorithm::Argon2i13 as u32 => {
79 Ok(PasswordHashAlgorithm::Argon2i13)
80 }
81 num if num == PasswordHashAlgorithm::Argon2id13 as u32 => {
82 Ok(PasswordHashAlgorithm::Argon2id13)
83 }
84 _ => Err(Error::InvalidValue {
85 context: crate::ErrorContext::PasswordHashAlgorithm,
86 actual: num as u64,
87 constraint: crate::ValueConstraint::Between {
88 min: PasswordHashAlgorithm::Argon2i13 as u64,
89 max: PasswordHashAlgorithm::Argon2id13 as u64,
90 },
91 }),
92 }
93 }
94}
95
96impl From<PasswordHashAlgorithm> for argon2::Argon2Type {
97 fn from(algo: PasswordHashAlgorithm) -> Self {
98 match algo {
99 PasswordHashAlgorithm::Argon2i13 => argon2::Argon2Type::Argon2i,
100 PasswordHashAlgorithm::Argon2id13 => argon2::Argon2Type::Argon2id,
101 }
102 }
103}
104
105pub fn crypto_pwhash(
131 output: &mut [u8],
132 password: &[u8],
133 salt: &[u8],
134 opslimit: u64,
135 memlimit: usize,
136 algorithm: PasswordHashAlgorithm,
137) -> Result<(), Error> {
138 validate_pwhash_parameters(
139 output.len(),
140 password.len(),
141 salt.len(),
142 opslimit,
143 memlimit,
144 algorithm,
145 )?;
146
147 let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
148
149 argon2_hash(
150 t_cost,
151 m_cost,
152 1,
153 password,
154 salt,
155 None,
156 None,
157 output,
158 algorithm.into(),
159 )
160}
161
162pub(crate) fn validate_pwhash_parameters(
163 output_len: usize,
164 password_len: usize,
165 salt_len: usize,
166 opslimit: u64,
167 memlimit: usize,
168 algorithm: PasswordHashAlgorithm,
169) -> Result<(), Error> {
170 let (
171 bytes_min,
172 bytes_max,
173 password_max,
174 opslimit_min,
175 opslimit_max,
176 memlimit_min,
177 memlimit_max,
178 ) = match algorithm {
179 PasswordHashAlgorithm::Argon2i13 => (
180 CRYPTO_PWHASH_ARGON2I_BYTES_MIN,
181 CRYPTO_PWHASH_ARGON2I_BYTES_MAX,
182 CRYPTO_PWHASH_ARGON2I_PASSWD_MAX,
183 CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN,
184 CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MAX,
185 CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MIN,
186 CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MAX,
187 ),
188 PasswordHashAlgorithm::Argon2id13 => (
189 CRYPTO_PWHASH_ARGON2ID_BYTES_MIN,
190 CRYPTO_PWHASH_ARGON2ID_BYTES_MAX,
191 CRYPTO_PWHASH_ARGON2ID_PASSWD_MAX,
192 CRYPTO_PWHASH_ARGON2ID_OPSLIMIT_MIN,
193 CRYPTO_PWHASH_ARGON2ID_OPSLIMIT_MAX,
194 CRYPTO_PWHASH_ARGON2ID_MEMLIMIT_MIN,
195 CRYPTO_PWHASH_ARGON2ID_MEMLIMIT_MAX,
196 ),
197 };
198
199 validate_length!(
200 bytes_min,
201 bytes_max,
202 output_len,
203 crate::ErrorContext::Output
204 );
205 validate_length!(
206 CRYPTO_PWHASH_PASSWD_MIN,
207 password_max,
208 password_len,
209 crate::ErrorContext::Password
210 );
211 validate_length!(
212 exact CRYPTO_PWHASH_SALTBYTES,
213 salt_len,
214 crate::ErrorContext::PasswordHashSalt
215 );
216 validate_value!(
217 opslimit_min,
218 opslimit_max,
219 opslimit,
220 crate::ErrorContext::OperationsLimit
221 );
222 validate_value!(
223 memlimit_min,
224 memlimit_max,
225 memlimit,
226 crate::ErrorContext::MemoryLimit
227 );
228
229 Ok(())
230}
231
232#[cfg(any(feature = "base64", all(doc, not(doctest))))]
233#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
234pub(crate) fn pwhash_to_string(
235 algorithm: PasswordHashAlgorithm,
236 t_cost: u32,
237 m_cost: u32,
238 parallelism: u32,
239 salt: &[u8],
240 hash: &[u8],
241) -> String {
242 let algorithm_name = pwhash_algorithm_name(algorithm);
243 format!(
244 "${algorithm_name}$v={}$m={},t={},p={parallelism}${}${}",
245 argon2::ARGON2_VERSION_NUMBER,
246 m_cost,
247 t_cost,
248 base64_no_pad_encode(salt),
249 base64_no_pad_encode(hash),
250 )
251}
252
253#[cfg(any(feature = "base64", all(doc, not(doctest))))]
254pub(crate) fn pwhash_string_len(
255 algorithm: PasswordHashAlgorithm,
256 t_cost: u32,
257 m_cost: u32,
258 parallelism: u32,
259 salt_len: usize,
260 hash_len: usize,
261) -> Option<usize> {
262 let salt_len = base64_no_pad_encoded_len(salt_len)?;
263 let hash_len = base64_no_pad_encoded_len(hash_len)?;
264 1usize
265 .checked_add(pwhash_algorithm_name(algorithm).len())?
266 .checked_add(3 + decimal_len(ARGON2_VERSION_NUMBER))?
267 .checked_add(3 + decimal_len(m_cost))?
268 .checked_add(3 + decimal_len(t_cost))?
269 .checked_add(3 + decimal_len(parallelism))?
270 .checked_add(1)?
271 .checked_add(salt_len)?
272 .checked_add(1)?
273 .checked_add(hash_len)
274}
275
276#[cfg(any(feature = "base64", all(doc, not(doctest))))]
277const fn pwhash_algorithm_name(algorithm: PasswordHashAlgorithm) -> &'static str {
278 match algorithm {
279 PasswordHashAlgorithm::Argon2i13 => "argon2i",
280 PasswordHashAlgorithm::Argon2id13 => "argon2id",
281 }
282}
283
284#[cfg(any(feature = "base64", all(doc, not(doctest))))]
285const fn decimal_len(value: u32) -> usize {
286 if value == 0 {
287 1
288 } else {
289 value.ilog10() as usize + 1
290 }
291}
292
293#[cfg(any(feature = "base64", all(doc, not(doctest))))]
294const fn base64_no_pad_encoded_len(input_len: usize) -> Option<usize> {
295 let remainder_len = match input_len % 3 {
296 0 => 0,
297 1 => 2,
298 _ => 3,
299 };
300 match (input_len / 3).checked_mul(4) {
301 Some(full_len) => full_len.checked_add(remainder_len),
302 None => None,
303 }
304}
305
306#[cfg(any(feature = "base64", all(doc, not(doctest))))]
307fn base64_no_pad_encode(input: &[u8]) -> String {
308 const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
309
310 let mut output = String::with_capacity(input.len().div_ceil(3) * 4);
311
312 let (chunks, rem) = input.as_chunks::<3>();
313
314 for chunk in chunks {
315 let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
316 output.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
317 output.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
318 output.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
319 output.push(ALPHABET[(n & 0x3f) as usize] as char);
320 }
321
322 if rem.len() == 1 {
323 let n = (rem[0] as u32) << 16;
324 output.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
325 output.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
326 } else if rem.len() == 2 {
327 let n = ((rem[0] as u32) << 16) | ((rem[1] as u32) << 8);
328 output.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
329 output.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
330 output.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
331 }
332
333 output
334}
335
336#[cfg(feature = "base64")]
337fn base64_no_pad_decode(input: &str) -> Option<Vec<u8>> {
338 fn decode_byte(byte: u8) -> Option<u8> {
339 match byte {
340 b'A'..=b'Z' => Some(byte - b'A'),
341 b'a'..=b'z' => Some(byte - b'a' + 26),
342 b'0'..=b'9' => Some(byte - b'0' + 52),
343 b'+' => Some(62),
344 b'/' => Some(63),
345 _ => None,
346 }
347 }
348
349 let input = input.as_bytes();
350 if input.len() % 4 == 1 || input.contains(&b'=') {
351 return None;
352 }
353
354 let mut output = Vec::with_capacity(input.len() / 4 * 3 + 2);
355 let (chunks, rem) = input.as_chunks::<4>();
356
357 for chunk in chunks {
358 let n = ((decode_byte(chunk[0])? as u32) << 18)
359 | ((decode_byte(chunk[1])? as u32) << 12)
360 | ((decode_byte(chunk[2])? as u32) << 6)
361 | decode_byte(chunk[3])? as u32;
362 output.push((n >> 16) as u8);
363 output.push((n >> 8) as u8);
364 output.push(n as u8);
365 }
366
367 if rem.len() == 2 {
368 let second = decode_byte(rem[1])?;
369 if second & 0x0f != 0 {
370 return None;
371 }
372 let n = ((decode_byte(rem[0])? as u32) << 18) | ((second as u32) << 12);
373 output.push((n >> 16) as u8);
374 } else if rem.len() == 3 {
375 let third = decode_byte(rem[2])?;
376 if third & 0x03 != 0 {
377 return None;
378 }
379 let n = ((decode_byte(rem[0])? as u32) << 18)
380 | ((decode_byte(rem[1])? as u32) << 12)
381 | ((third as u32) << 6);
382 output.push((n >> 16) as u8);
383 output.push((n >> 8) as u8);
384 }
385
386 Some(output)
387}
388
389pub(crate) fn convert_costs(opslimit: u64, memlimit: usize) -> (u32, u32) {
390 (opslimit as u32, (memlimit / 1024) as u32)
391}
392
393pub(crate) fn convert_costs_checked(opslimit: u64, memlimit: usize) -> Result<(u32, u32), Error> {
394 let t_cost = u32::try_from(opslimit).map_err(|_| Error::InvalidValue {
395 context: crate::ErrorContext::OperationsLimit,
396 actual: opslimit,
397 constraint: crate::ValueConstraint::Between {
398 min: 0,
399 max: u32::MAX as u64,
400 },
401 })?;
402 let m_cost = u32::try_from(memlimit / 1024).map_err(|_| Error::InvalidValue {
403 context: crate::ErrorContext::MemoryLimit,
404 actual: memlimit as u64,
405 constraint: crate::ValueConstraint::Between {
406 min: 0,
407 max: (u32::MAX as u64) * 1024 + 1023,
408 },
409 })?;
410 Ok((t_cost, m_cost))
411}
412
413#[cfg(any(feature = "base64", all(doc, not(doctest))))]
431#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
432pub fn crypto_pwhash_str(password: &[u8], opslimit: u64, memlimit: usize) -> Result<String, Error> {
433 crypto_pwhash_str_alg(
434 password,
435 opslimit,
436 memlimit,
437 PasswordHashAlgorithm::Argon2id13,
438 )
439}
440
441#[cfg(any(feature = "base64", all(doc, not(doctest))))]
455#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
456pub fn crypto_pwhash_str_alg(
457 password: &[u8],
458 opslimit: u64,
459 memlimit: usize,
460 algorithm: PasswordHashAlgorithm,
461) -> Result<String, Error> {
462 validate_pwhash_parameters(
463 STR_HASHBYTES,
464 password.len(),
465 CRYPTO_PWHASH_SALTBYTES,
466 opslimit,
467 memlimit,
468 algorithm,
469 )?;
470
471 let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
472 let mut hash = [0u8; STR_HASHBYTES];
473 crate::rng::copy_randombytes(&mut salt);
474
475 let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
476
477 crypto_pwhash(&mut hash, password, &salt, opslimit, memlimit, algorithm)?;
478
479 let pw = pwhash_to_string(algorithm, t_cost, m_cost, 1, &salt, &hash);
480
481 Ok(pw)
482}
483
484#[cfg(feature = "base64")]
485#[derive(Default)]
486pub(crate) struct Pwhash {
487 pub(crate) pwhash: Option<Vec<u8>>,
488 pub(crate) salt: Option<Vec<u8>>,
489 pub(crate) type_: Option<PasswordHashAlgorithm>,
490 pub(crate) t_cost: Option<u32>,
491 pub(crate) m_cost: Option<u32>,
492 pub(crate) parallelism: Option<u32>,
493}
494
495#[cfg(feature = "base64")]
496impl Pwhash {
497 pub(crate) fn parse_encoded_pwhash(hashed_password: &str) -> Result<Self, Error> {
498 if hashed_password.len() >= CRYPTO_PWHASH_STRBYTES {
499 return Err(length_error!(
500 crate::ErrorContext::PasswordHash,
501 hashed_password.len(),
502 max CRYPTO_PWHASH_STRBYTES - 1
503 ));
504 }
505
506 let encoded = hashed_password
507 .strip_prefix('$')
508 .ok_or_else(|| Error::invalid_encoding(crate::ErrorContext::PasswordHash))?;
509 let mut fields = encoded.split('$');
510
511 let algorithm = match fields.next().filter(|field| !field.is_empty()) {
512 Some("argon2i") => PasswordHashAlgorithm::Argon2i13,
513 Some("argon2id") => PasswordHashAlgorithm::Argon2id13,
514 Some(field) if field.starts_with("v=") => {
515 return Err(Error::missing_data(
516 crate::ErrorContext::PasswordHashAlgorithm,
517 ));
518 }
519 Some(_) => {
520 return Err(Error::invalid_encoding(
521 crate::ErrorContext::PasswordHashAlgorithm,
522 ));
523 }
524 None => {
525 return Err(Error::missing_data(
526 crate::ErrorContext::PasswordHashAlgorithm,
527 ));
528 }
529 };
530
531 let version = fields
532 .next()
533 .ok_or(Error::missing_data(
534 crate::ErrorContext::PasswordHashVersion,
535 ))?
536 .strip_prefix("v=")
537 .ok_or(Error::invalid_encoding(
538 crate::ErrorContext::PasswordHashVersion,
539 ))?;
540 let version =
541 parse_minimal_pwhash_decimal(version, crate::ErrorContext::PasswordHashVersion)?;
542 if version != ARGON2_VERSION_NUMBER {
543 return Err(Error::invalid_encoding(
544 crate::ErrorContext::PasswordHashVersion,
545 ));
546 }
547
548 let parameters = fields.next().ok_or(Error::missing_data(
549 crate::ErrorContext::PasswordHashMemoryCost,
550 ))?;
551 let mut parameters = parameters.split(',');
552 let m_cost = parse_pwhash_parameter(
553 parameters.next(),
554 "m=",
555 crate::ErrorContext::PasswordHashMemoryCost,
556 )?;
557 let t_cost = parse_pwhash_parameter(
558 parameters.next(),
559 "t=",
560 crate::ErrorContext::PasswordHashTimeCost,
561 )?;
562 let parallelism = parse_pwhash_parameter(
563 parameters.next(),
564 "p=",
565 crate::ErrorContext::PasswordHashParallelism,
566 )?;
567 if parameters.next().is_some() {
568 return Err(Error::invalid_encoding(crate::ErrorContext::PasswordHash));
569 }
570
571 let salt = fields
572 .next()
573 .filter(|field| !field.is_empty())
574 .ok_or(Error::missing_data(crate::ErrorContext::PasswordHashSalt))?;
575 let salt = base64_no_pad_decode(salt).ok_or(Error::invalid_encoding(
576 crate::ErrorContext::PasswordHashSalt,
577 ))?;
578
579 let pwhash = fields
580 .next()
581 .filter(|field| !field.is_empty())
582 .ok_or(Error::missing_data(crate::ErrorContext::PasswordHash))?;
583 let pwhash = base64_no_pad_decode(pwhash)
584 .ok_or(Error::invalid_encoding(crate::ErrorContext::PasswordHash))?;
585
586 if fields.next().is_some() {
587 return Err(Error::invalid_encoding(crate::ErrorContext::PasswordHash));
588 }
589
590 crate::argon2::validate_argon2_pwhash_parameters(
591 pwhash.len(),
592 salt.len(),
593 t_cost,
594 m_cost,
595 parallelism,
596 )?;
597
598 Ok(Self {
599 pwhash: Some(pwhash),
600 salt: Some(salt),
601 type_: Some(algorithm),
602 t_cost: Some(t_cost),
603 m_cost: Some(m_cost),
604 parallelism: Some(parallelism),
605 })
606 }
607}
608
609#[cfg(feature = "base64")]
610fn parse_pwhash_parameter(
611 parameter: Option<&str>,
612 prefix: &str,
613 context: crate::ErrorContext,
614) -> Result<u32, Error> {
615 let value = parameter
616 .ok_or(Error::missing_data(context))?
617 .strip_prefix(prefix)
618 .ok_or(Error::invalid_encoding(context))?;
619 parse_minimal_pwhash_decimal(value, context)
620}
621
622#[cfg(feature = "base64")]
623fn parse_minimal_pwhash_decimal(value: &str, context: crate::ErrorContext) -> Result<u32, Error> {
624 if value.is_empty()
625 || !value.bytes().all(|byte| byte.is_ascii_digit())
626 || (value.len() > 1 && value.starts_with('0'))
627 {
628 return Err(Error::invalid_encoding(context));
629 }
630 value
631 .parse::<u32>()
632 .map_err(|_| Error::invalid_encoding(context))
633}
634
635#[cfg(any(feature = "base64", all(doc, not(doctest))))]
645#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
646pub fn crypto_pwhash_str_verify(hashed_password: &str, password: &[u8]) -> Result<(), Error> {
647 let pwhash = Pwhash::parse_encoded_pwhash(hashed_password)?;
648 let t_cost = pwhash.t_cost.ok_or(Error::missing_data(
649 crate::ErrorContext::PasswordHashTimeCost,
650 ))?;
651 let m_cost = pwhash.m_cost.ok_or(Error::missing_data(
652 crate::ErrorContext::PasswordHashMemoryCost,
653 ))?;
654 let parallelism = pwhash.parallelism.ok_or(Error::missing_data(
655 crate::ErrorContext::PasswordHashParallelism,
656 ))?;
657 let salt = pwhash
658 .salt
659 .ok_or(Error::missing_data(crate::ErrorContext::PasswordHashSalt))?;
660 let algorithm = pwhash.type_.ok_or(Error::missing_data(
661 crate::ErrorContext::PasswordHashAlgorithm,
662 ))?;
663 let expected_hash = pwhash
664 .pwhash
665 .ok_or(Error::missing_data(crate::ErrorContext::PasswordHash))?;
666
667 verify_pwhash_parts(
668 &expected_hash,
669 password,
670 &salt,
671 t_cost,
672 m_cost,
673 parallelism,
674 algorithm,
675 )
676}
677
678pub(crate) fn verify_pwhash_parts(
679 expected_hash: &[u8],
680 password: &[u8],
681 salt: &[u8],
682 t_cost: u32,
683 m_cost: u32,
684 parallelism: u32,
685 algorithm: PasswordHashAlgorithm,
686) -> Result<(), Error> {
687 let mut hash = Zeroizing::new(vec![0u8; expected_hash.len()]);
688 argon2_hash(
689 t_cost,
690 m_cost,
691 parallelism,
692 password,
693 salt,
694 None,
695 None,
696 &mut hash,
697 algorithm.into(),
698 )?;
699
700 if hash.as_slice().ct_eq(expected_hash).unwrap_u8() == 1 {
701 Ok(())
702 } else {
703 Err(Error::AuthenticationFailed)
704 }
705}
706
707#[cfg(any(feature = "base64", all(doc, not(doctest))))]
718#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
719pub fn crypto_pwhash_str_needs_rehash(
720 hashed_password: &str,
721 opslimit: u64,
722 memlimit: usize,
723) -> Result<bool, Error> {
724 let (t_cost, m_cost) = convert_costs_checked(opslimit, memlimit)?;
725 let pwhash = Pwhash::parse_encoded_pwhash(hashed_password)?;
726 let parsed_t_cost = pwhash.t_cost.ok_or(Error::missing_data(
727 crate::ErrorContext::PasswordHashTimeCost,
728 ))?;
729 let parsed_m_cost = pwhash.m_cost.ok_or(Error::missing_data(
730 crate::ErrorContext::PasswordHashMemoryCost,
731 ))?;
732
733 if t_cost != parsed_t_cost || m_cost != parsed_m_cost {
734 Ok(true)
735 } else {
736 Ok(false)
737 }
738}
739
740#[cfg(all(test, dryoc_native_tests))]
741mod tests {
742 use super::*;
743
744 #[test]
745 fn test_crypto_pwhash() {
746 use sodiumoxide::crypto::pwhash;
747
748 use crate::rng::copy_randombytes;
749
750 let mut hash = [0u8; 32];
751 let mut so_hash = [0u8; 32];
752 let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
753
754 copy_randombytes(&mut salt);
755
756 let password = b"donkey kong";
757
758 crypto_pwhash(
759 &mut hash,
760 password,
761 &salt,
762 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
763 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
764 PasswordHashAlgorithm::Argon2id13,
765 )
766 .expect("pwhash failed");
767
768 let _ = pwhash::argon2id13::derive_key(
769 &mut so_hash,
770 password,
771 &pwhash::argon2id13::Salt::from_slice(&salt).expect("salt failed"),
772 pwhash::argon2id13::OPSLIMIT_INTERACTIVE,
773 pwhash::argon2id13::MEMLIMIT_INTERACTIVE,
774 )
775 .expect("so pwhash failed");
776
777 assert_eq!(hash, so_hash);
778 }
779
780 #[cfg(feature = "base64")]
781 #[test]
782 fn test_base64_no_pad_matches_base64_crate() {
783 use base64::Engine as _;
784 use base64::engine::general_purpose;
785
786 for len in 0..128 {
787 let input: Vec<u8> = (0..len).map(|i| (i * 31 + len) as u8).collect();
788 let encoded = base64_no_pad_encode(&input);
789 assert_eq!(encoded, general_purpose::STANDARD_NO_PAD.encode(&input));
790 assert_eq!(
791 base64_no_pad_decode(&encoded).as_deref(),
792 Some(input.as_slice())
793 );
794 }
795
796 assert_eq!(base64_no_pad_decode("A"), None);
797 assert_eq!(base64_no_pad_decode("AA="), None);
798 assert_eq!(base64_no_pad_decode("A/"), None);
799 assert_eq!(base64_no_pad_decode("AA/"), None);
800
801 let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
802 let hash = [0u8; STR_HASHBYTES];
803 let encoded = pwhash_to_string(
804 PasswordHashAlgorithm::Argon2id13,
805 2,
806 65_536,
807 1,
808 &salt,
809 &hash,
810 );
811 assert_eq!(
812 pwhash_string_len(
813 PasswordHashAlgorithm::Argon2id13,
814 2,
815 65_536,
816 1,
817 salt.len(),
818 hash.len(),
819 ),
820 Some(encoded.len())
821 );
822
823 assert_eq!(
824 pwhash_string_len(
825 PasswordHashAlgorithm::Argon2id13,
826 2,
827 65_536,
828 1,
829 usize::MAX,
830 0
831 ),
832 None,
833 );
834 #[cfg(target_pointer_width = "32")]
835 assert_eq!(
836 pwhash_string_len(
837 PasswordHashAlgorithm::Argon2id13,
838 2,
839 65_536,
840 1,
841 3_221_225_471,
842 0,
843 ),
844 None,
845 );
846 }
847
848 #[cfg(feature = "base64")]
849 #[test]
850 fn malformed_password_hash_fields_have_structured_errors() {
851 const SALT: &str = "AAAAAAAAAAA";
852 const HASH: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
853 let cases = [
854 (
855 format!("$argon2wat$v=19$m=65536,t=2,p=1${SALT}${HASH}"),
856 crate::ErrorContext::PasswordHashAlgorithm,
857 ),
858 (
859 format!("$argon2id$v=nope$m=65536,t=2,p=1${SALT}${HASH}"),
860 crate::ErrorContext::PasswordHashVersion,
861 ),
862 (
863 format!("$argon2id$v=19$m=nope,t=2,p=1${SALT}${HASH}"),
864 crate::ErrorContext::PasswordHashMemoryCost,
865 ),
866 (
867 format!("$argon2id$v=19$m=65536,t=nope,p=1${SALT}${HASH}"),
868 crate::ErrorContext::PasswordHashTimeCost,
869 ),
870 (
871 format!("$argon2id$v=19$m=65536,t=2,p=nope${SALT}${HASH}"),
872 crate::ErrorContext::PasswordHashParallelism,
873 ),
874 (
875 format!("$argon2id$v=19$m=65536,t=2,p=1$A${HASH}"),
876 crate::ErrorContext::PasswordHashSalt,
877 ),
878 (
879 format!("$argon2id$v=19$m=65536,t=2,p=1${SALT}$A"),
880 crate::ErrorContext::PasswordHash,
881 ),
882 (
883 format!("$argon2id$v=18$m=65536,t=2,p=1${SALT}${HASH}"),
884 crate::ErrorContext::PasswordHashVersion,
885 ),
886 (
887 format!("$argon2id$v=019$m=65536,t=2,p=1${SALT}${HASH}"),
888 crate::ErrorContext::PasswordHashVersion,
889 ),
890 (
891 format!("$argon2id$v=19$m=065536,t=2,p=1${SALT}${HASH}"),
892 crate::ErrorContext::PasswordHashMemoryCost,
893 ),
894 (
895 format!("$argon2id$v=19$m=65536,t=+2,p=1${SALT}${HASH}"),
896 crate::ErrorContext::PasswordHashTimeCost,
897 ),
898 (
899 format!("$argon2id$v=19$m=65536,t=2,p=01${SALT}${HASH}"),
900 crate::ErrorContext::PasswordHashParallelism,
901 ),
902 ];
903
904 for (encoded, expected_context) in cases {
905 let error = match Pwhash::parse_encoded_pwhash(&encoded) {
906 Ok(_) => panic!("the malformed field should be rejected"),
907 Err(error) => error,
908 };
909 assert!(matches!(
910 error,
911 Error::InvalidEncoding { context } if context == expected_context
912 ));
913 }
914
915 let missing_hash = format!("$argon2id$v=19$m=65536,t=2,p=1${SALT}");
916 assert!(matches!(
917 Pwhash::parse_encoded_pwhash(&missing_hash),
918 Err(Error::MissingData {
919 context: crate::ErrorContext::PasswordHash,
920 })
921 ));
922
923 let missing_algorithm = format!("$v=19$m=65536,t=2,p=1${SALT}${HASH}");
924 assert!(matches!(
925 Pwhash::parse_encoded_pwhash(&missing_algorithm),
926 Err(Error::MissingData {
927 context: crate::ErrorContext::PasswordHashAlgorithm,
928 })
929 ));
930 }
931
932 #[test]
933 fn password_hashing_reports_invalid_resource_limits() {
934 let mut output = [0u8; CRYPTO_PWHASH_BYTES_MIN];
935 let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
936 let password = b"password";
937
938 for (opslimit, memlimit, expected_context) in [
939 (
940 CRYPTO_PWHASH_OPSLIMIT_MIN - 1,
941 CRYPTO_PWHASH_MEMLIMIT_MIN,
942 crate::ErrorContext::OperationsLimit,
943 ),
944 (
945 CRYPTO_PWHASH_OPSLIMIT_MIN,
946 CRYPTO_PWHASH_MEMLIMIT_MIN - 1,
947 crate::ErrorContext::MemoryLimit,
948 ),
949 ] {
950 let error = crypto_pwhash(
951 &mut output,
952 password,
953 &salt,
954 opslimit,
955 memlimit,
956 PasswordHashAlgorithm::Argon2id13,
957 )
958 .expect_err("invalid resource limits should fail");
959 assert!(matches!(
960 error,
961 Error::InvalidValue { context, .. } if context == expected_context
962 ));
963
964 #[cfg(feature = "base64")]
965 {
966 let error = crypto_pwhash_str(password, opslimit, memlimit)
967 .expect_err("invalid resource limits should fail");
968 assert!(matches!(
969 error,
970 Error::InvalidValue { context, .. } if context == expected_context
971 ));
972 }
973 }
974 }
975
976 #[test]
977 fn password_hashing_enforces_classic_parameter_contract() {
978 let mut output = [0u8; CRYPTO_PWHASH_BYTES_MIN];
979 let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
980
981 for opslimit in 1..CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN {
982 assert!(matches!(
983 crypto_pwhash(
984 &mut output,
985 b"password",
986 &salt,
987 opslimit,
988 CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MIN,
989 PasswordHashAlgorithm::Argon2i13,
990 ),
991 Err(Error::InvalidValue {
992 context: crate::ErrorContext::OperationsLimit,
993 ..
994 })
995 ));
996 }
997
998 assert!(matches!(
999 crypto_pwhash(
1000 &mut output,
1001 b"password",
1002 &salt[..CRYPTO_PWHASH_SALTBYTES - 1],
1003 CRYPTO_PWHASH_OPSLIMIT_MIN,
1004 CRYPTO_PWHASH_MEMLIMIT_MIN,
1005 PasswordHashAlgorithm::Argon2id13,
1006 ),
1007 Err(Error::InvalidLength {
1008 context: crate::ErrorContext::PasswordHashSalt,
1009 constraint: crate::LengthConstraint::Exact(CRYPTO_PWHASH_SALTBYTES),
1010 ..
1011 })
1012 ));
1013
1014 assert!(PasswordHashAlgorithm::try_from(0).is_err());
1015 assert_eq!(
1016 PasswordHashAlgorithm::try_from(CRYPTO_PWHASH_ALG_ARGON2ID13 as u32)
1017 .expect("valid algorithm"),
1018 PasswordHashAlgorithm::Argon2id13
1019 );
1020 }
1021
1022 #[cfg(feature = "base64")]
1023 #[test]
1024 fn test_crypto_pwhash_str() {
1025 use sodiumoxide::crypto::pwhash;
1026
1027 let password = b"donkey kong";
1028
1029 let pwhash = crypto_pwhash_str(
1030 password,
1031 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1032 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
1033 )
1034 .expect("pwhash failed");
1035 let pwhash2 = crypto_pwhash_str(
1036 password,
1037 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1038 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
1039 )
1040 .expect("pwhash failed");
1041
1042 let parsed = Pwhash::parse_encoded_pwhash(&pwhash).expect("couldn't parse pwhash");
1043 let parsed2 = Pwhash::parse_encoded_pwhash(&pwhash2).expect("couldn't parse pwhash");
1044
1045 assert_ne!(
1046 parsed.salt.as_ref().expect("missing salt"),
1047 &vec![0u8; CRYPTO_PWHASH_SALTBYTES]
1048 );
1049 assert_ne!(parsed.salt, parsed2.salt);
1050
1051 let mut pwhash_bytes = [0u8; CRYPTO_PWHASH_STRBYTES];
1052 pwhash_bytes[..pwhash.len()].copy_from_slice(pwhash.as_bytes());
1053
1054 assert!(pwhash::argon2id13::pwhash_verify(
1055 &pwhash::argon2id13::HashedPassword::from_slice(&pwhash_bytes)
1056 .expect("hashed password failed"),
1057 password,
1058 ));
1059
1060 let argon2i = crypto_pwhash_str_alg(
1061 password,
1062 CRYPTO_PWHASH_ARGON2I_OPSLIMIT_INTERACTIVE,
1063 CRYPTO_PWHASH_ARGON2I_MEMLIMIT_INTERACTIVE,
1064 PasswordHashAlgorithm::Argon2i13,
1065 )
1066 .expect("argon2i pwhash failed");
1067 assert!(argon2i.starts_with(CRYPTO_PWHASH_ARGON2I_STRPREFIX));
1068 crypto_pwhash_str_verify(&argon2i, password).expect("argon2i verify failed");
1069 }
1070
1071 #[cfg(feature = "base64")]
1072 #[test]
1073 fn test_crypto_pwhash_str_verify() {
1074 use sodiumoxide::crypto::pwhash;
1075
1076 let password = b"donkey kong";
1077
1078 let pwhash = pwhash::argon2id13::pwhash(
1079 password,
1080 pwhash::argon2id13::OPSLIMIT_INTERACTIVE,
1081 pwhash::argon2id13::MEMLIMIT_INTERACTIVE,
1082 )
1083 .expect("so pwhash failed");
1084
1085 let pw_str = std::str::from_utf8(&pwhash.0)
1086 .expect("from ut8 failed")
1087 .trim_end_matches('\x00');
1088
1089 crypto_pwhash_str_verify(pw_str, password).expect("verify failed");
1090 crypto_pwhash_str_verify(pw_str, b"invalid password")
1091 .expect_err("verify should have failed");
1092
1093 for encoded in [
1094 concat!(
1095 "$argon2id$v=19$m=256,t=3,p=1$MDEyMzQ1Njc$",
1096 "G5ajKFCoUzaXRLdz7UJb5wGkb2Xt+X5/GQjUYtS2+TE",
1097 ),
1098 concat!(
1099 "$argon2i$v=19$m=4096,t=3,p=2$b2RpZHVlamRpc29kaXNrdw$",
1100 "TNnWIwlu1061JHrnCqIAmjs3huSxYIU+0jWipu7Kc9M",
1101 ),
1102 ] {
1103 crypto_pwhash_str_verify(encoded, b"password")
1104 .expect("valid libsodium Argon2 vector should verify");
1105 }
1106
1107 assert!(crypto_pwhash_str_verify(&format!("{pw_str}$garbage"), password).is_err());
1108 assert!(crypto_pwhash_str_verify(pw_str.trim_start_matches('$'), password).is_err());
1109 for invalid_parallelism in ["0", "4294967295"] {
1110 let malformed = pw_str.replace(",p=1", &format!(",p={invalid_parallelism}"));
1111 assert!(crypto_pwhash_str_verify(&malformed, password).is_err());
1112 assert!(
1113 crypto_pwhash_str_needs_rehash(
1114 &malformed,
1115 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1116 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
1117 )
1118 .is_err()
1119 );
1120 }
1121
1122 assert!(
1124 !crypto_pwhash_str_needs_rehash(
1125 pw_str,
1126 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1127 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
1128 )
1129 .expect("verify rehash failed")
1130 );
1131
1132 assert!(
1134 crypto_pwhash_str_needs_rehash(
1135 pw_str,
1136 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE + 1,
1137 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
1138 )
1139 .expect("verify rehash failed")
1140 );
1141
1142 assert!(
1143 crypto_pwhash_str_needs_rehash(pw_str, 0, 0,)
1144 .expect("zero costs are a valid rehash comparison")
1145 );
1146
1147 assert!(matches!(
1148 crypto_pwhash_str_needs_rehash(pw_str, u32::MAX as u64 + 1, 0),
1149 Err(Error::InvalidValue {
1150 context: crate::ErrorContext::OperationsLimit,
1151 ..
1152 })
1153 ));
1154 }
1155}