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
370fn validate_stored_password_hash(
371 parsed: &PasswordHash,
372 limits: PasswordHashVerificationLimits,
373) -> Result<Params> {
374 if parsed.algorithm.as_str() != "argon2id" {
375 return Err(CryptoError::password_hash(format_args!(
376 "unsupported password hash algorithm {}",
377 parsed.algorithm
378 )));
379 }
380 if parsed.version != Some(ARGON2_VERSION) {
381 return Err(CryptoError::password_hash(
382 "unsupported or missing Argon2 version",
383 ));
384 }
385 if parsed.salt.is_none() {
386 return Err(CryptoError::password_hash(
387 "password hash is missing a salt",
388 ));
389 }
390 if parsed.hash.is_none() {
391 return Err(CryptoError::password_hash(
392 "password hash is missing an output",
393 ));
394 }
395
396 let params = Params::try_from(parsed).map_err(CryptoError::password_hash)?;
397 if !params.keyid().is_empty() || !params.data().is_empty() {
398 return Err(CryptoError::password_hash(
399 "Argon2 keyid and associated data are not supported",
400 ));
401 }
402
403 ensure_within_limit(
404 "m",
405 u64::from(params.m_cost()),
406 u64::from(limits.max_memory_kib),
407 )?;
408 ensure_within_limit(
409 "t",
410 u64::from(params.t_cost()),
411 u64::from(limits.max_iterations),
412 )?;
413 ensure_within_limit(
414 "p",
415 u64::from(params.p_cost()),
416 u64::from(limits.max_parallelism),
417 )?;
418 ensure_within_limit(
419 "output_length",
420 usize_to_u64(
421 params
422 .output_len()
423 .unwrap_or(DEFAULT_PASSWORD_HASH_OUTPUT_LENGTH),
424 ),
425 usize_to_u64(limits.max_output_length),
426 )?;
427
428 Ok(params)
429}
430
431fn password_hash_needs_rehash(
432 parsed: &PasswordHash,
433 params: &Params,
434 current: PasswordHashWorkFactor,
435) -> Result<bool> {
436 let salt = parsed
437 .salt
438 .ok_or_else(|| CryptoError::password_hash("password hash is missing a salt"))?;
439 let salt_length = salt.as_ref().len();
440 let output_length = params
441 .output_len()
442 .unwrap_or(DEFAULT_PASSWORD_HASH_OUTPUT_LENGTH);
443
444 Ok(params.m_cost() < current.memory_kib
445 || params.t_cost() < current.iterations
446 || params.p_cost() < current.parallelism
447 || output_length < current.output_length
448 || salt_length < PASSWORD_SALT_LENGTH)
449}
450
451fn ensure_within_limit(parameter: &'static str, actual: u64, maximum: u64) -> Result<()> {
452 if actual > maximum {
453 Err(CryptoError::PasswordHashVerificationLimit {
454 parameter,
455 actual,
456 maximum,
457 })
458 } else {
459 Ok(())
460 }
461}
462
463fn ensure_policy_value_within_limit(
464 parameter: &'static str,
465 actual: u64,
466 maximum: u64,
467) -> Result<()> {
468 if actual > maximum {
469 Err(CryptoError::password_hash_policy(format_args!(
470 "work factor {parameter}={actual} exceeds verification limit {maximum}"
471 )))
472 } else {
473 Ok(())
474 }
475}
476
477fn usize_to_u64(value: usize) -> u64 {
478 u64::try_from(value).unwrap_or(u64::MAX)
479}
480
481pub fn hmac_sha256_hex(key: &[u8], data: &[u8]) -> Result<String> {
491 let mut mac = <Hmac<Sha256> as KeyInit>::new_from_slice(key)
492 .map_err(CryptoError::message_authentication)?;
493 mac.update(data);
494 Ok(bytes_to_hex(&mac.finalize().into_bytes()))
495}
496
497#[must_use]
499pub fn sha256_hex(data: &[u8]) -> String {
500 let mut hasher = Sha256::new();
501 hasher.update(data);
502 bytes_to_hex(&hasher.finalize())
503}
504
505#[must_use]
507pub fn bytes_to_hex(bytes: &[u8]) -> String {
508 let mut hex = String::with_capacity(bytes.len() * 2);
509 for byte in bytes {
510 let _ = write!(&mut hex, "{byte:02x}");
511 }
512 hex
513}
514
515#[must_use]
517pub fn sha256_digest_to_hex(digest: &[u8]) -> String {
518 bytes_to_hex(digest)
519}
520
521#[must_use]
523pub fn new_sha256() -> Sha256 {
524 Sha256::new()
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530 use sha2::Digest;
531
532 fn lightweight_policy() -> PasswordHashPolicy {
533 let work_factor = PasswordHashWorkFactor::new(8, 1, 1, 32).unwrap();
534 let limits = PasswordHashVerificationLimits::new(8, 1, 1, 32).unwrap();
535 PasswordHashPolicy::new(work_factor, limits).unwrap()
536 }
537
538 fn hash_with_work_factor(password: &str, work_factor: PasswordHashWorkFactor) -> String {
539 hash_with_work_factor_and_salt(password, work_factor, &[7_u8; PASSWORD_SALT_LENGTH])
540 }
541
542 fn hash_with_work_factor_and_salt(
543 password: &str,
544 work_factor: PasswordHashWorkFactor,
545 salt: &[u8],
546 ) -> String {
547 password_hasher(work_factor)
548 .unwrap()
549 .hash_password_with_salt(password.as_bytes(), salt)
550 .unwrap()
551 .to_string()
552 }
553
554 #[test]
555 fn default_policy_uses_rfc_9106_second_recommended_profile() {
556 let policy = PasswordHashPolicy::default();
557 let work_factor = policy.work_factor();
558 let limits = policy.verification_limits();
559
560 assert_eq!(work_factor.memory_kib(), 64 * 1024);
561 assert_eq!(work_factor.iterations(), 3);
562 assert_eq!(work_factor.parallelism(), 4);
563 assert_eq!(work_factor.output_length(), 32);
564 assert_eq!(limits.max_memory_kib(), work_factor.memory_kib());
565 assert_eq!(limits.max_iterations(), work_factor.iterations());
566 assert_eq!(limits.max_parallelism(), work_factor.parallelism());
567 assert_eq!(limits.max_output_length(), work_factor.output_length());
568 }
569
570 #[test]
571 fn default_hash_encodes_rfc_9106_second_recommended_profile() {
572 let hash = hash_password("default policy password").unwrap();
573
574 assert!(hash.starts_with("$argon2id$v=19$m=65536,t=3,p=4$"));
575 assert!(verify_password("default policy password", &hash).unwrap());
576 }
577
578 #[test]
579 fn sha256_hex_matches_known_vectors_and_binary_input() {
580 assert_eq!(
581 sha256_hex(b""),
582 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
583 );
584 assert_eq!(
585 sha256_hex(b"abc"),
586 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
587 );
588 assert_eq!(
589 sha256_hex(&[0x00, 0xff, 0x10]),
590 "2da45f2cd1f9c8e69a67abf7a6b26c282533d0a7686787a9533265418680d4d2"
591 );
592 }
593
594 #[test]
595 fn hmac_sha256_matches_rfc_4231_vector_and_separates_keys() {
596 let data = b"Hi There";
597 let first = hmac_sha256_hex(&[0x0b; 20], data).unwrap();
598 let second = hmac_sha256_hex(&[0x0c; 20], data).unwrap();
599
600 assert_eq!(
601 first,
602 "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
603 );
604 assert_ne!(first, second);
605 assert_ne!(first, hmac_sha256_hex(&[0x0b; 20], b"Hi There!").unwrap());
606 assert_eq!(
607 hmac_sha256_hex(b"Jefe", b"what do ya want for nothing?").unwrap(),
608 "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
609 );
610 assert_eq!(
611 hmac_sha256_hex(b"", b"").unwrap(),
612 "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"
613 );
614 }
615
616 #[test]
617 fn bytes_to_hex_encodes_lowercase_and_preserves_leading_zeroes() {
618 assert_eq!(bytes_to_hex(&[]), "");
619 assert_eq!(bytes_to_hex(&[0x00, 0x0f, 0x10, 0xab, 0xff]), "000f10abff");
620 }
621
622 #[test]
623 fn sha256_digest_to_hex_matches_incremental_hasher_output() {
624 let mut hasher = new_sha256();
625 hasher.update(b"a");
626 hasher.update(b"bc");
627 assert_eq!(sha256_digest_to_hex(&hasher.finalize()), sha256_hex(b"abc"));
628 }
629
630 #[test]
631 fn password_hash_verifies_matching_password_and_rejects_wrong_password() {
632 let policy = lightweight_policy();
633 let hash = hash_password_with_policy("correct horse battery staple", &policy).unwrap();
634
635 assert!(
636 verify_password_with_policy("correct horse battery staple", &hash, &policy)
637 .unwrap()
638 .is_valid
639 );
640 assert!(
641 !verify_password_with_policy("wrong password", &hash, &policy)
642 .unwrap()
643 .is_valid
644 );
645 assert!(hash.starts_with("$argon2id$v=19$m=8,t=1,p=1$"));
646 }
647
648 #[test]
649 fn password_hash_uses_fresh_salt() {
650 let policy = lightweight_policy();
651 let first = hash_password_with_policy("same password", &policy).unwrap();
652 let second = hash_password_with_policy("same password", &policy).unwrap();
653
654 assert_ne!(first, second);
655 assert!(
656 verify_password_with_policy("same password", &first, &policy)
657 .unwrap()
658 .is_valid
659 );
660 assert!(
661 verify_password_with_policy("same password", &second, &policy)
662 .unwrap()
663 .is_valid
664 );
665 }
666
667 #[test]
668 fn malformed_and_unsupported_password_hashes_return_errors() {
669 assert!(matches!(
670 verify_password("password", "not-a-password-hash"),
671 Err(CryptoError::PasswordHash(_))
672 ));
673
674 let policy = lightweight_policy();
675 let hash = hash_password_with_policy("password", &policy).unwrap();
676 let unsupported = hash.replacen("$argon2id$", "$notargon$", 1);
677 assert!(matches!(
678 verify_password_with_policy("password", &unsupported, &policy),
679 Err(CryptoError::PasswordHash(_))
680 ));
681
682 let unsupported_version = hash.replacen("v=19", "v=16", 1);
683 assert!(matches!(
684 verify_password_with_policy("password", &unsupported_version, &policy),
685 Err(CryptoError::PasswordHash(_))
686 ));
687
688 let missing_version = hash.replacen("$v=19$", "$", 1);
689 assert!(matches!(
690 verify_password_with_policy("password", &missing_version, &policy),
691 Err(CryptoError::PasswordHash(_))
692 ));
693
694 assert!(matches!(
695 verify_password_with_policy("password", "$argon2id$v=19$m=8,t=1,p=1", &policy,),
696 Err(CryptoError::PasswordHash(_))
697 ));
698 assert!(matches!(
699 verify_password_with_policy(
700 "password",
701 "$argon2id$v=19$m=8,t=1,p=1$c2FsdHNhbHQ",
702 &policy,
703 ),
704 Err(CryptoError::PasswordHash(_))
705 ));
706
707 let unknown_parameter = hash.replacen("p=1", "p=1,x=1", 1);
708 assert!(matches!(
709 verify_password_with_policy("password", &unknown_parameter, &policy),
710 Err(CryptoError::PasswordHash(_))
711 ));
712 }
713
714 #[test]
715 fn verification_rejects_over_budget_work_factors_before_argon2_runs() {
716 let policy = lightweight_policy();
717 let hash = hash_password_with_policy("password", &policy).unwrap();
718 let over_memory = hash.replacen("m=8", "m=9", 1);
719 let over_iterations = hash.replacen("t=1", "t=2", 1);
720 let over_output = hash_with_work_factor(
721 "password",
722 PasswordHashWorkFactor::new(8, 1, 1, 33).unwrap(),
723 );
724 let parallel_work_factor = PasswordHashWorkFactor::new(16, 1, 1, 32).unwrap();
725 let parallel_limits = PasswordHashVerificationLimits::new(16, 1, 1, 32).unwrap();
726 let parallel_policy =
727 PasswordHashPolicy::new(parallel_work_factor, parallel_limits).unwrap();
728 let over_parallelism =
729 hash_with_work_factor("password", parallel_work_factor).replacen("p=1", "p=2", 1);
730
731 assert!(matches!(
732 verify_password_with_policy("password", &over_memory, &policy),
733 Err(CryptoError::PasswordHashVerificationLimit {
734 parameter: "m",
735 actual: 9,
736 maximum: 8,
737 })
738 ));
739 assert!(matches!(
740 verify_password_with_policy("password", &over_iterations, &policy),
741 Err(CryptoError::PasswordHashVerificationLimit {
742 parameter: "t",
743 actual: 2,
744 maximum: 1,
745 })
746 ));
747 assert!(matches!(
748 verify_password_with_policy("password", &over_parallelism, ¶llel_policy),
749 Err(CryptoError::PasswordHashVerificationLimit {
750 parameter: "p",
751 actual: 2,
752 maximum: 1,
753 })
754 ));
755 assert!(matches!(
756 verify_password_with_policy("password", &over_output, &policy),
757 Err(CryptoError::PasswordHashVerificationLimit {
758 parameter: "output_length",
759 actual: 33,
760 maximum: 32,
761 })
762 ));
763
764 let extreme_memory = hash.replacen("m=8", "m=4294967295", 1);
765 assert!(matches!(
766 verify_password_with_policy("password", &extreme_memory, &policy),
767 Err(CryptoError::PasswordHashVerificationLimit {
768 parameter: "m",
769 actual: 4_294_967_295,
770 maximum: 8,
771 })
772 ));
773 }
774
775 #[test]
776 fn legacy_work_factor_verifies_and_requests_rehash() {
777 let legacy = PasswordHashWorkFactor::new(19 * 1024, 2, 1, 32).unwrap();
778 let hash = hash_with_work_factor("password", legacy);
779 let verification =
780 verify_password_with_policy("password", &hash, &PasswordHashPolicy::default()).unwrap();
781
782 assert!(verification.is_valid);
783 assert!(verification.needs_rehash);
784
785 let wrong =
786 verify_password_with_policy("wrong password", &hash, &PasswordHashPolicy::default())
787 .unwrap();
788 assert!(!wrong.is_valid);
789 assert!(!wrong.needs_rehash);
790 }
791
792 #[test]
793 fn current_work_factor_does_not_request_rehash() {
794 let policy = lightweight_policy();
795 let hash = hash_password_with_policy("password", &policy).unwrap();
796 let verification = verify_password_with_policy("password", &hash, &policy).unwrap();
797
798 assert!(verification.is_valid);
799 assert!(!verification.needs_rehash);
800 }
801
802 #[test]
803 fn short_legacy_salt_verifies_and_requests_rehash() {
804 let policy = lightweight_policy();
805 let hash = hash_with_work_factor_and_salt("password", policy.work_factor(), &[5_u8; 8]);
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 custom_limits_accept_stronger_hash_without_rehashing_down() {
814 let current = PasswordHashWorkFactor::new(8, 1, 1, 32).unwrap();
815 let limits = PasswordHashVerificationLimits::new(16, 2, 2, 32).unwrap();
816 let policy = PasswordHashPolicy::new(current, limits).unwrap();
817 let stronger = PasswordHashWorkFactor::new(16, 2, 2, 32).unwrap();
818 let hash = hash_with_work_factor("password", stronger);
819 let verification = verify_password_with_policy("password", &hash, &policy).unwrap();
820
821 assert!(verification.is_valid);
822 assert!(!verification.needs_rehash);
823 }
824
825 #[test]
826 fn policy_rejects_work_factor_above_its_verification_limits() {
827 let work_factor = PasswordHashWorkFactor::new(64, 2, 1, 32).unwrap();
828 let limits = PasswordHashVerificationLimits::new(32, 2, 1, 32).unwrap();
829
830 assert!(matches!(
831 PasswordHashPolicy::new(work_factor, limits),
832 Err(CryptoError::PasswordHashPolicy(_))
833 ));
834
835 assert!(matches!(
836 PasswordHashWorkFactor::new(8, 1, 1, 4),
837 Err(CryptoError::PasswordHashPolicy(_))
838 ));
839 }
840
841 #[test]
842 fn work_factor_and_limit_constructors_reject_every_invalid_boundary() {
843 for result in [
844 PasswordHashWorkFactor::new(7, 1, 1, 32),
845 PasswordHashWorkFactor::new(15, 1, 2, 32),
846 PasswordHashWorkFactor::new(8, 0, 1, 32),
847 PasswordHashWorkFactor::new(8, 1, 0, 32),
848 PasswordHashWorkFactor::new(8, 1, 1, Output::MIN_LENGTH - 1),
849 PasswordHashWorkFactor::new(8, 1, 1, Output::MAX_LENGTH + 1),
850 ] {
851 assert!(matches!(result, Err(CryptoError::PasswordHashPolicy(_))));
852 }
853
854 for result in [
855 PasswordHashVerificationLimits::new(Params::MIN_M_COST - 1, 1, 1, 32),
856 PasswordHashVerificationLimits::new(8, Params::MIN_T_COST - 1, 1, 32),
857 PasswordHashVerificationLimits::new(8, 1, Params::MIN_P_COST - 1, 32),
858 PasswordHashVerificationLimits::new(8, 1, 1, Output::MIN_LENGTH - 1),
859 PasswordHashVerificationLimits::new(8, 1, 1, Output::MAX_LENGTH + 1),
860 ] {
861 assert!(matches!(result, Err(CryptoError::PasswordHashPolicy(_))));
862 }
863 }
864
865 #[test]
866 fn policy_rejects_each_work_factor_dimension_above_its_limit() {
867 let cases = [
868 (
869 PasswordHashWorkFactor::new(16, 1, 1, 32).unwrap(),
870 PasswordHashVerificationLimits::new(8, 1, 1, 32).unwrap(),
871 ),
872 (
873 PasswordHashWorkFactor::new(8, 2, 1, 32).unwrap(),
874 PasswordHashVerificationLimits::new(8, 1, 1, 32).unwrap(),
875 ),
876 (
877 PasswordHashWorkFactor::new(16, 1, 2, 32).unwrap(),
878 PasswordHashVerificationLimits::new(16, 1, 1, 32).unwrap(),
879 ),
880 (
881 PasswordHashWorkFactor::new(8, 1, 1, 33).unwrap(),
882 PasswordHashVerificationLimits::new(8, 1, 1, 32).unwrap(),
883 ),
884 ];
885
886 for (work_factor, limits) in cases {
887 assert!(matches!(
888 PasswordHashPolicy::new(work_factor, limits),
889 Err(CryptoError::PasswordHashPolicy(_))
890 ));
891 }
892 }
893}