1use crate::{CryptoError, Result};
9use argon2::{
10 Algorithm, Argon2, Params, Version,
11 password_hash::{
12 Error as PasswordHashError, PasswordHasher, PasswordVerifier,
13 phc::{Output, PasswordHash},
14 },
15};
16use hmac::{Hmac, KeyInit, Mac};
17use sha2::{Digest, Sha256};
18use std::fmt::Write;
19
20const ARGON2_VERSION: u32 = 19;
21const PASSWORD_SALT_LENGTH: usize = 16;
22const RFC_9106_SECOND_MEMORY_KIB: u32 = 64 * 1024;
23const RFC_9106_SECOND_ITERATIONS: u32 = 3;
24const RFC_9106_SECOND_PARALLELISM: u32 = 4;
25const DEFAULT_PASSWORD_HASH_OUTPUT_LENGTH: usize = 32;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct PasswordHashWorkFactor {
30 memory_kib: u32,
31 iterations: u32,
32 parallelism: u32,
33 output_length: usize,
34}
35
36impl PasswordHashWorkFactor {
37 #[must_use]
41 pub const fn rfc_9106_second_recommended() -> Self {
42 Self {
43 memory_kib: RFC_9106_SECOND_MEMORY_KIB,
44 iterations: RFC_9106_SECOND_ITERATIONS,
45 parallelism: RFC_9106_SECOND_PARALLELISM,
46 output_length: DEFAULT_PASSWORD_HASH_OUTPUT_LENGTH,
47 }
48 }
49
50 pub fn new(
57 memory_kib: u32,
58 iterations: u32,
59 parallelism: u32,
60 output_length: usize,
61 ) -> Result<Self> {
62 let work_factor = Self {
63 memory_kib,
64 iterations,
65 parallelism,
66 output_length,
67 };
68 if !(Output::MIN_LENGTH..=Output::MAX_LENGTH).contains(&output_length) {
69 return Err(CryptoError::password_hash_policy(format_args!(
70 "output length must be between {} and {} bytes",
71 Output::MIN_LENGTH,
72 Output::MAX_LENGTH
73 )));
74 }
75 work_factor.params()?;
76 Ok(work_factor)
77 }
78
79 #[must_use]
81 pub const fn memory_kib(self) -> u32 {
82 self.memory_kib
83 }
84
85 #[must_use]
87 pub const fn iterations(self) -> u32 {
88 self.iterations
89 }
90
91 #[must_use]
93 pub const fn parallelism(self) -> u32 {
94 self.parallelism
95 }
96
97 #[must_use]
99 pub const fn output_length(self) -> usize {
100 self.output_length
101 }
102
103 fn params(self) -> Result<Params> {
104 Params::new(
105 self.memory_kib,
106 self.iterations,
107 self.parallelism,
108 Some(self.output_length),
109 )
110 .map_err(CryptoError::password_hash_policy)
111 }
112}
113
114impl Default for PasswordHashWorkFactor {
115 fn default() -> Self {
116 Self::rfc_9106_second_recommended()
117 }
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126#[expect(
127 clippy::struct_field_names,
128 reason = "The max prefix distinguishes verification ceilings from password hashing work factors."
129)]
130pub struct PasswordHashVerificationLimits {
131 max_memory_kib: u32,
132 max_iterations: u32,
133 max_parallelism: u32,
134 max_output_length: usize,
135}
136
137impl PasswordHashVerificationLimits {
138 pub fn new(
145 max_memory_kib: u32,
146 max_iterations: u32,
147 max_parallelism: u32,
148 max_output_length: usize,
149 ) -> Result<Self> {
150 if max_memory_kib < Params::MIN_M_COST
151 || max_iterations < Params::MIN_T_COST
152 || max_parallelism < Params::MIN_P_COST
153 {
154 return Err(CryptoError::password_hash_policy(
155 "verification limits are below the Argon2 minimum",
156 ));
157 }
158 if !(Output::MIN_LENGTH..=Output::MAX_LENGTH).contains(&max_output_length) {
159 return Err(CryptoError::password_hash_policy(format_args!(
160 "maximum output length must be between {} and {} bytes",
161 Output::MIN_LENGTH,
162 Output::MAX_LENGTH
163 )));
164 }
165
166 Ok(Self {
167 max_memory_kib,
168 max_iterations,
169 max_parallelism,
170 max_output_length,
171 })
172 }
173
174 #[must_use]
176 pub const fn max_memory_kib(self) -> u32 {
177 self.max_memory_kib
178 }
179
180 #[must_use]
182 pub const fn max_iterations(self) -> u32 {
183 self.max_iterations
184 }
185
186 #[must_use]
188 pub const fn max_parallelism(self) -> u32 {
189 self.max_parallelism
190 }
191
192 #[must_use]
194 pub const fn max_output_length(self) -> usize {
195 self.max_output_length
196 }
197}
198
199impl Default for PasswordHashVerificationLimits {
200 fn default() -> Self {
201 let work_factor = PasswordHashWorkFactor::rfc_9106_second_recommended();
202 Self {
203 max_memory_kib: work_factor.memory_kib,
204 max_iterations: work_factor.iterations,
205 max_parallelism: work_factor.parallelism,
206 max_output_length: work_factor.output_length,
207 }
208 }
209}
210
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
217pub struct PasswordHashPolicy {
218 work_factor: PasswordHashWorkFactor,
219 verification_limits: PasswordHashVerificationLimits,
220}
221
222impl PasswordHashPolicy {
223 pub fn new(
230 work_factor: PasswordHashWorkFactor,
231 verification_limits: PasswordHashVerificationLimits,
232 ) -> Result<Self> {
233 work_factor.params()?;
234 ensure_policy_value_within_limit(
235 "m",
236 u64::from(work_factor.memory_kib),
237 u64::from(verification_limits.max_memory_kib),
238 )?;
239 ensure_policy_value_within_limit(
240 "t",
241 u64::from(work_factor.iterations),
242 u64::from(verification_limits.max_iterations),
243 )?;
244 ensure_policy_value_within_limit(
245 "p",
246 u64::from(work_factor.parallelism),
247 u64::from(verification_limits.max_parallelism),
248 )?;
249 ensure_policy_value_within_limit(
250 "output_length",
251 usize_to_u64(work_factor.output_length),
252 usize_to_u64(verification_limits.max_output_length),
253 )?;
254
255 Ok(Self {
256 work_factor,
257 verification_limits,
258 })
259 }
260
261 #[must_use]
263 pub const fn work_factor(self) -> PasswordHashWorkFactor {
264 self.work_factor
265 }
266
267 #[must_use]
269 pub const fn verification_limits(self) -> PasswordHashVerificationLimits {
270 self.verification_limits
271 }
272}
273
274impl Default for PasswordHashPolicy {
275 fn default() -> Self {
276 Self {
277 work_factor: PasswordHashWorkFactor::rfc_9106_second_recommended(),
278 verification_limits: PasswordHashVerificationLimits {
279 max_memory_kib: RFC_9106_SECOND_MEMORY_KIB,
280 max_iterations: RFC_9106_SECOND_ITERATIONS,
281 max_parallelism: RFC_9106_SECOND_PARALLELISM,
282 max_output_length: DEFAULT_PASSWORD_HASH_OUTPUT_LENGTH,
283 },
284 }
285 }
286}
287
288#[derive(Clone, Copy, Debug, PartialEq, Eq)]
290pub struct PasswordHashVerification {
291 pub is_valid: bool,
293 pub needs_rehash: bool,
295}
296
297pub fn hash_password(password: &str) -> Result<String> {
303 hash_password_with_policy(password, &PasswordHashPolicy::default())
304}
305
306pub fn hash_password_with_policy(password: &str, policy: &PasswordHashPolicy) -> Result<String> {
313 password_hasher(policy.work_factor)?
314 .hash_password(password.as_bytes())
315 .map(|hash| hash.to_string())
316 .map_err(CryptoError::password_hash)
317}
318
319pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
330 verify_password_with_policy(password, hash, &PasswordHashPolicy::default())
331 .map(|verification| verification.is_valid)
332}
333
334pub fn verify_password_with_policy(
341 password: &str,
342 hash: &str,
343 policy: &PasswordHashPolicy,
344) -> Result<PasswordHashVerification> {
345 let parsed = PasswordHash::new(hash).map_err(CryptoError::password_hash)?;
346 let params = validate_stored_password_hash(&parsed, policy.verification_limits)?;
347 let needs_rehash = password_hash_needs_rehash(&parsed, ¶ms, policy.work_factor)?;
348
349 match password_hasher(policy.work_factor)?.verify_password(password.as_bytes(), &parsed) {
350 Ok(()) => Ok(PasswordHashVerification {
351 is_valid: true,
352 needs_rehash,
353 }),
354 Err(PasswordHashError::PasswordInvalid) => Ok(PasswordHashVerification {
355 is_valid: false,
356 needs_rehash: false,
357 }),
358 Err(error) => Err(CryptoError::password_hash(error)),
359 }
360}
361
362fn password_hasher(work_factor: PasswordHashWorkFactor) -> Result<Argon2<'static>> {
363 Ok(Argon2::new(
364 Algorithm::Argon2id,
365 Version::V0x13,
366 work_factor.params()?,
367 ))
368}
369
370pub fn validate_password_hash(hash: &str, limits: PasswordHashVerificationLimits) -> Result<()> {
375 let parsed =
376 PasswordHash::new(hash).map_err(|_| CryptoError::password_hash("invalid PHC encoding"))?;
377 validate_stored_password_hash(&parsed, limits).map(|_| ())
378}
379
380fn validate_stored_password_hash(
381 parsed: &PasswordHash,
382 limits: PasswordHashVerificationLimits,
383) -> Result<Params> {
384 if parsed.algorithm.as_str() != "argon2id" {
385 return Err(CryptoError::password_hash(format_args!(
386 "unsupported password hash algorithm {}",
387 parsed.algorithm
388 )));
389 }
390 if parsed.version != Some(ARGON2_VERSION) {
391 return Err(CryptoError::password_hash(
392 "unsupported or missing Argon2 version",
393 ));
394 }
395 if parsed.salt.is_none() {
396 return Err(CryptoError::password_hash(
397 "password hash is missing a salt",
398 ));
399 }
400 if parsed.hash.is_none() {
401 return Err(CryptoError::password_hash(
402 "password hash is missing an output",
403 ));
404 }
405
406 let params = Params::try_from(parsed).map_err(CryptoError::password_hash)?;
407 if !params.keyid().is_empty() || !params.data().is_empty() {
408 return Err(CryptoError::password_hash(
409 "Argon2 keyid and associated data are not supported",
410 ));
411 }
412
413 ensure_within_limit(
414 "m",
415 u64::from(params.m_cost()),
416 u64::from(limits.max_memory_kib),
417 )?;
418 ensure_within_limit(
419 "t",
420 u64::from(params.t_cost()),
421 u64::from(limits.max_iterations),
422 )?;
423 ensure_within_limit(
424 "p",
425 u64::from(params.p_cost()),
426 u64::from(limits.max_parallelism),
427 )?;
428 ensure_within_limit(
429 "output_length",
430 usize_to_u64(
431 params
432 .output_len()
433 .unwrap_or(DEFAULT_PASSWORD_HASH_OUTPUT_LENGTH),
434 ),
435 usize_to_u64(limits.max_output_length),
436 )?;
437
438 Ok(params)
439}
440
441fn password_hash_needs_rehash(
442 parsed: &PasswordHash,
443 params: &Params,
444 current: PasswordHashWorkFactor,
445) -> Result<bool> {
446 let salt = parsed
447 .salt
448 .ok_or_else(|| CryptoError::password_hash("password hash is missing a salt"))?;
449 let salt_length = salt.as_ref().len();
450 let output_length = params
451 .output_len()
452 .unwrap_or(DEFAULT_PASSWORD_HASH_OUTPUT_LENGTH);
453
454 Ok(params.m_cost() < current.memory_kib
455 || params.t_cost() < current.iterations
456 || params.p_cost() < current.parallelism
457 || output_length < current.output_length
458 || salt_length < PASSWORD_SALT_LENGTH)
459}
460
461fn ensure_within_limit(parameter: &'static str, actual: u64, maximum: u64) -> Result<()> {
462 if actual > maximum {
463 Err(CryptoError::PasswordHashVerificationLimit {
464 parameter,
465 actual,
466 maximum,
467 })
468 } else {
469 Ok(())
470 }
471}
472
473fn ensure_policy_value_within_limit(
474 parameter: &'static str,
475 actual: u64,
476 maximum: u64,
477) -> Result<()> {
478 if actual > maximum {
479 Err(CryptoError::password_hash_policy(format_args!(
480 "work factor {parameter}={actual} exceeds verification limit {maximum}"
481 )))
482 } else {
483 Ok(())
484 }
485}
486
487fn usize_to_u64(value: usize) -> u64 {
488 u64::try_from(value).unwrap_or(u64::MAX)
489}
490
491pub fn hmac_sha256_hex(key: &[u8], data: &[u8]) -> Result<String> {
501 let mut mac = <Hmac<Sha256> as KeyInit>::new_from_slice(key)
502 .map_err(CryptoError::message_authentication)?;
503 mac.update(data);
504 Ok(bytes_to_hex(&mac.finalize().into_bytes()))
505}
506
507#[must_use]
509pub fn sha256_hex(data: &[u8]) -> String {
510 let mut hasher = Sha256::new();
511 hasher.update(data);
512 bytes_to_hex(&hasher.finalize())
513}
514
515#[must_use]
517pub fn bytes_to_hex(bytes: &[u8]) -> String {
518 let mut hex = String::with_capacity(bytes.len() * 2);
519 for byte in bytes {
520 let _ = write!(&mut hex, "{byte:02x}");
521 }
522 hex
523}
524
525#[must_use]
527pub fn sha256_digest_to_hex(digest: &[u8]) -> String {
528 bytes_to_hex(digest)
529}
530
531#[must_use]
533pub fn new_sha256() -> Sha256 {
534 Sha256::new()
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use sha2::Digest;
541
542 fn lightweight_policy() -> PasswordHashPolicy {
543 let work_factor = PasswordHashWorkFactor::new(8, 1, 1, 32).unwrap();
544 let limits = PasswordHashVerificationLimits::new(8, 1, 1, 32).unwrap();
545 PasswordHashPolicy::new(work_factor, limits).unwrap()
546 }
547
548 fn hash_with_work_factor(password: &str, work_factor: PasswordHashWorkFactor) -> String {
549 hash_with_work_factor_and_salt(password, work_factor, &[7_u8; PASSWORD_SALT_LENGTH])
550 }
551
552 fn hash_with_work_factor_and_salt(
553 password: &str,
554 work_factor: PasswordHashWorkFactor,
555 salt: &[u8],
556 ) -> String {
557 password_hasher(work_factor)
558 .unwrap()
559 .hash_password_with_salt(password.as_bytes(), salt)
560 .unwrap()
561 .to_string()
562 }
563
564 #[test]
565 fn default_policy_uses_rfc_9106_second_recommended_profile() {
566 let policy = PasswordHashPolicy::default();
567 let work_factor = policy.work_factor();
568 let limits = policy.verification_limits();
569
570 assert_eq!(work_factor.memory_kib(), 64 * 1024);
571 assert_eq!(work_factor.iterations(), 3);
572 assert_eq!(work_factor.parallelism(), 4);
573 assert_eq!(work_factor.output_length(), 32);
574 assert_eq!(limits.max_memory_kib(), work_factor.memory_kib());
575 assert_eq!(limits.max_iterations(), work_factor.iterations());
576 assert_eq!(limits.max_parallelism(), work_factor.parallelism());
577 assert_eq!(limits.max_output_length(), work_factor.output_length());
578 }
579
580 #[test]
581 fn default_hash_encodes_rfc_9106_second_recommended_profile() {
582 let hash = hash_password("default policy password").unwrap();
583
584 assert!(hash.starts_with("$argon2id$v=19$m=65536,t=3,p=4$"));
585 assert!(verify_password("default policy password", &hash).unwrap());
586 }
587
588 #[test]
589 fn sha256_hex_matches_known_vectors_and_binary_input() {
590 assert_eq!(
591 sha256_hex(b""),
592 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
593 );
594 assert_eq!(
595 sha256_hex(b"abc"),
596 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
597 );
598 assert_eq!(
599 sha256_hex(&[0x00, 0xff, 0x10]),
600 "2da45f2cd1f9c8e69a67abf7a6b26c282533d0a7686787a9533265418680d4d2"
601 );
602 }
603
604 #[test]
605 fn hmac_sha256_matches_rfc_4231_vector_and_separates_keys() {
606 let data = b"Hi There";
607 let first = hmac_sha256_hex(&[0x0b; 20], data).unwrap();
608 let second = hmac_sha256_hex(&[0x0c; 20], data).unwrap();
609
610 assert_eq!(
611 first,
612 "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
613 );
614 assert_ne!(first, second);
615 assert_ne!(first, hmac_sha256_hex(&[0x0b; 20], b"Hi There!").unwrap());
616 assert_eq!(
617 hmac_sha256_hex(b"Jefe", b"what do ya want for nothing?").unwrap(),
618 "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
619 );
620 assert_eq!(
621 hmac_sha256_hex(b"", b"").unwrap(),
622 "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"
623 );
624 }
625
626 #[test]
627 fn bytes_to_hex_encodes_lowercase_and_preserves_leading_zeroes() {
628 assert_eq!(bytes_to_hex(&[]), "");
629 assert_eq!(bytes_to_hex(&[0x00, 0x0f, 0x10, 0xab, 0xff]), "000f10abff");
630 }
631
632 #[test]
633 fn sha256_digest_to_hex_matches_incremental_hasher_output() {
634 let mut hasher = new_sha256();
635 hasher.update(b"a");
636 hasher.update(b"bc");
637 assert_eq!(sha256_digest_to_hex(&hasher.finalize()), sha256_hex(b"abc"));
638 }
639
640 #[test]
641 fn password_hash_verifies_matching_password_and_rejects_wrong_password() {
642 let policy = lightweight_policy();
643 let hash = hash_password_with_policy("correct horse battery staple", &policy).unwrap();
644
645 assert!(
646 verify_password_with_policy("correct horse battery staple", &hash, &policy)
647 .unwrap()
648 .is_valid
649 );
650 assert!(
651 !verify_password_with_policy("wrong password", &hash, &policy)
652 .unwrap()
653 .is_valid
654 );
655 assert!(hash.starts_with("$argon2id$v=19$m=8,t=1,p=1$"));
656 }
657
658 #[test]
659 fn password_hash_uses_fresh_salt() {
660 let policy = lightweight_policy();
661 let first = hash_password_with_policy("same password", &policy).unwrap();
662 let second = hash_password_with_policy("same password", &policy).unwrap();
663
664 assert_ne!(first, second);
665 assert!(
666 verify_password_with_policy("same password", &first, &policy)
667 .unwrap()
668 .is_valid
669 );
670 assert!(
671 verify_password_with_policy("same password", &second, &policy)
672 .unwrap()
673 .is_valid
674 );
675 }
676
677 #[test]
678 fn malformed_and_unsupported_password_hashes_return_errors() {
679 assert!(matches!(
680 verify_password("password", "not-a-password-hash"),
681 Err(CryptoError::PasswordHash(_))
682 ));
683
684 let policy = lightweight_policy();
685 let hash = hash_password_with_policy("password", &policy).unwrap();
686 let unsupported = hash.replacen("$argon2id$", "$notargon$", 1);
687 assert!(matches!(
688 verify_password_with_policy("password", &unsupported, &policy),
689 Err(CryptoError::PasswordHash(_))
690 ));
691
692 let unsupported_version = hash.replacen("v=19", "v=16", 1);
693 assert!(matches!(
694 verify_password_with_policy("password", &unsupported_version, &policy),
695 Err(CryptoError::PasswordHash(_))
696 ));
697
698 let missing_version = hash.replacen("$v=19$", "$", 1);
699 assert!(matches!(
700 verify_password_with_policy("password", &missing_version, &policy),
701 Err(CryptoError::PasswordHash(_))
702 ));
703
704 assert!(matches!(
705 verify_password_with_policy("password", "$argon2id$v=19$m=8,t=1,p=1", &policy,),
706 Err(CryptoError::PasswordHash(_))
707 ));
708 assert!(matches!(
709 verify_password_with_policy(
710 "password",
711 "$argon2id$v=19$m=8,t=1,p=1$c2FsdHNhbHQ",
712 &policy,
713 ),
714 Err(CryptoError::PasswordHash(_))
715 ));
716
717 let unknown_parameter = hash.replacen("p=1", "p=1,x=1", 1);
718 assert!(matches!(
719 verify_password_with_policy("password", &unknown_parameter, &policy),
720 Err(CryptoError::PasswordHash(_))
721 ));
722 }
723
724 #[test]
725 fn verification_rejects_over_budget_work_factors_before_argon2_runs() {
726 let policy = lightweight_policy();
727 let hash = hash_password_with_policy("password", &policy).unwrap();
728 let over_memory = hash.replacen("m=8", "m=9", 1);
729 let over_iterations = hash.replacen("t=1", "t=2", 1);
730 let over_output = hash_with_work_factor(
731 "password",
732 PasswordHashWorkFactor::new(8, 1, 1, 33).unwrap(),
733 );
734 let parallel_work_factor = PasswordHashWorkFactor::new(16, 1, 1, 32).unwrap();
735 let parallel_limits = PasswordHashVerificationLimits::new(16, 1, 1, 32).unwrap();
736 let parallel_policy =
737 PasswordHashPolicy::new(parallel_work_factor, parallel_limits).unwrap();
738 let over_parallelism =
739 hash_with_work_factor("password", parallel_work_factor).replacen("p=1", "p=2", 1);
740
741 assert!(matches!(
742 verify_password_with_policy("password", &over_memory, &policy),
743 Err(CryptoError::PasswordHashVerificationLimit {
744 parameter: "m",
745 actual: 9,
746 maximum: 8,
747 })
748 ));
749 assert!(matches!(
750 verify_password_with_policy("password", &over_iterations, &policy),
751 Err(CryptoError::PasswordHashVerificationLimit {
752 parameter: "t",
753 actual: 2,
754 maximum: 1,
755 })
756 ));
757 assert!(matches!(
758 verify_password_with_policy("password", &over_parallelism, ¶llel_policy),
759 Err(CryptoError::PasswordHashVerificationLimit {
760 parameter: "p",
761 actual: 2,
762 maximum: 1,
763 })
764 ));
765 assert!(matches!(
766 verify_password_with_policy("password", &over_output, &policy),
767 Err(CryptoError::PasswordHashVerificationLimit {
768 parameter: "output_length",
769 actual: 33,
770 maximum: 32,
771 })
772 ));
773
774 let extreme_memory = hash.replacen("m=8", "m=4294967295", 1);
775 assert!(matches!(
776 verify_password_with_policy("password", &extreme_memory, &policy),
777 Err(CryptoError::PasswordHashVerificationLimit {
778 parameter: "m",
779 actual: 4_294_967_295,
780 maximum: 8,
781 })
782 ));
783 }
784
785 #[test]
786 fn legacy_work_factor_verifies_and_requests_rehash() {
787 let legacy = PasswordHashWorkFactor::new(19 * 1024, 2, 1, 32).unwrap();
788 let hash = hash_with_work_factor("password", legacy);
789 let verification =
790 verify_password_with_policy("password", &hash, &PasswordHashPolicy::default()).unwrap();
791
792 assert!(verification.is_valid);
793 assert!(verification.needs_rehash);
794
795 let wrong =
796 verify_password_with_policy("wrong password", &hash, &PasswordHashPolicy::default())
797 .unwrap();
798 assert!(!wrong.is_valid);
799 assert!(!wrong.needs_rehash);
800 }
801
802 #[test]
803 fn current_work_factor_does_not_request_rehash() {
804 let policy = lightweight_policy();
805 let hash = hash_password_with_policy("password", &policy).unwrap();
806 let verification = verify_password_with_policy("password", &hash, &policy).unwrap();
807
808 assert!(verification.is_valid);
809 assert!(!verification.needs_rehash);
810 }
811
812 #[test]
813 fn short_legacy_salt_verifies_and_requests_rehash() {
814 let policy = lightweight_policy();
815 let hash = hash_with_work_factor_and_salt("password", policy.work_factor(), &[5_u8; 8]);
816 let verification = verify_password_with_policy("password", &hash, &policy).unwrap();
817
818 assert!(verification.is_valid);
819 assert!(verification.needs_rehash);
820 }
821
822 #[test]
823 fn custom_limits_accept_stronger_hash_without_rehashing_down() {
824 let current = PasswordHashWorkFactor::new(8, 1, 1, 32).unwrap();
825 let limits = PasswordHashVerificationLimits::new(16, 2, 2, 32).unwrap();
826 let policy = PasswordHashPolicy::new(current, limits).unwrap();
827 let stronger = PasswordHashWorkFactor::new(16, 2, 2, 32).unwrap();
828 let hash = hash_with_work_factor("password", stronger);
829 let verification = verify_password_with_policy("password", &hash, &policy).unwrap();
830
831 assert!(verification.is_valid);
832 assert!(!verification.needs_rehash);
833 }
834
835 #[test]
836 fn policy_rejects_work_factor_above_its_verification_limits() {
837 let work_factor = PasswordHashWorkFactor::new(64, 2, 1, 32).unwrap();
838 let limits = PasswordHashVerificationLimits::new(32, 2, 1, 32).unwrap();
839
840 assert!(matches!(
841 PasswordHashPolicy::new(work_factor, limits),
842 Err(CryptoError::PasswordHashPolicy(_))
843 ));
844
845 assert!(matches!(
846 PasswordHashWorkFactor::new(8, 1, 1, 4),
847 Err(CryptoError::PasswordHashPolicy(_))
848 ));
849 }
850
851 #[test]
852 fn work_factor_and_limit_constructors_reject_every_invalid_boundary() {
853 for result in [
854 PasswordHashWorkFactor::new(7, 1, 1, 32),
855 PasswordHashWorkFactor::new(15, 1, 2, 32),
856 PasswordHashWorkFactor::new(8, 0, 1, 32),
857 PasswordHashWorkFactor::new(8, 1, 0, 32),
858 PasswordHashWorkFactor::new(8, 1, 1, Output::MIN_LENGTH - 1),
859 PasswordHashWorkFactor::new(8, 1, 1, Output::MAX_LENGTH + 1),
860 ] {
861 assert!(matches!(result, Err(CryptoError::PasswordHashPolicy(_))));
862 }
863
864 for result in [
865 PasswordHashVerificationLimits::new(Params::MIN_M_COST - 1, 1, 1, 32),
866 PasswordHashVerificationLimits::new(8, Params::MIN_T_COST - 1, 1, 32),
867 PasswordHashVerificationLimits::new(8, 1, Params::MIN_P_COST - 1, 32),
868 PasswordHashVerificationLimits::new(8, 1, 1, Output::MIN_LENGTH - 1),
869 PasswordHashVerificationLimits::new(8, 1, 1, Output::MAX_LENGTH + 1),
870 ] {
871 assert!(matches!(result, Err(CryptoError::PasswordHashPolicy(_))));
872 }
873 }
874
875 #[test]
876 fn policy_rejects_each_work_factor_dimension_above_its_limit() {
877 let cases = [
878 (
879 PasswordHashWorkFactor::new(16, 1, 1, 32).unwrap(),
880 PasswordHashVerificationLimits::new(8, 1, 1, 32).unwrap(),
881 ),
882 (
883 PasswordHashWorkFactor::new(8, 2, 1, 32).unwrap(),
884 PasswordHashVerificationLimits::new(8, 1, 1, 32).unwrap(),
885 ),
886 (
887 PasswordHashWorkFactor::new(16, 1, 2, 32).unwrap(),
888 PasswordHashVerificationLimits::new(16, 1, 1, 32).unwrap(),
889 ),
890 (
891 PasswordHashWorkFactor::new(8, 1, 1, 33).unwrap(),
892 PasswordHashVerificationLimits::new(8, 1, 1, 32).unwrap(),
893 ),
894 ];
895
896 for (work_factor, limits) in cases {
897 assert!(matches!(
898 PasswordHashPolicy::new(work_factor, limits),
899 Err(CryptoError::PasswordHashPolicy(_))
900 ));
901 }
902 }
903}