1#[cfg(any(feature = "base64", all(doc, not(doctest))))]
109use alloc::string::String;
110use alloc::vec::Vec;
111use core::fmt;
112
113#[cfg(feature = "serde")]
114use serde::{Deserialize, Serialize};
115use zeroize::Zeroize;
116
117use crate::classic::crypto_pwhash;
118pub use crate::classic::crypto_pwhash::PasswordHashAlgorithm;
119use crate::constants::*;
120use crate::error::Error;
121use crate::keypair;
122use crate::rng::copy_randombytes;
123use crate::types::*;
124
125pub type Salt = Vec<u8>;
132pub type Hash = Vec<u8>;
136
137#[derive(Zeroize, Clone, Debug)]
138#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
139pub struct Config {
147 algorithm: PasswordHashAlgorithm,
148 hash_length: usize,
149 memlimit: usize,
150 opslimit: u64,
151 parallelism: u32,
152}
153
154impl Config {
155 #[must_use]
161 pub fn with_algorithm(self, algorithm: PasswordHashAlgorithm) -> Self {
162 Self { algorithm, ..self }
163 }
164
165 #[must_use]
171 pub fn with_hash_length(self, hash_length: usize) -> Self {
172 Self {
173 hash_length,
174 ..self
175 }
176 }
177
178 #[must_use]
185 pub fn with_memlimit(self, memlimit: usize) -> Self {
186 Self { memlimit, ..self }
187 }
188
189 #[must_use]
196 pub fn with_opslimit(self, opslimit: u64) -> Self {
197 Self { opslimit, ..self }
198 }
199
200 #[must_use]
205 pub fn interactive() -> Self {
206 Self::preset(
207 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
208 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
209 )
210 }
211
212 #[must_use]
216 pub fn moderate() -> Self {
217 Self::preset(
218 CRYPTO_PWHASH_OPSLIMIT_MODERATE,
219 CRYPTO_PWHASH_MEMLIMIT_MODERATE,
220 )
221 }
222
223 #[must_use]
228 pub fn sensitive() -> Self {
229 Self::preset(
230 CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
231 CRYPTO_PWHASH_MEMLIMIT_SENSITIVE,
232 )
233 }
234
235 const fn preset(opslimit: u64, memlimit: usize) -> Self {
238 Self {
239 algorithm: PasswordHashAlgorithm::Argon2id13,
240 opslimit,
241 memlimit,
242 parallelism: 1,
243 hash_length: crypto_pwhash::STR_HASHBYTES,
244 }
245 }
246}
247
248impl Default for Config {
249 fn default() -> Self {
250 Self::interactive()
251 }
252}
253
254fn validate_direct_config(
255 config: &Config,
256 output_len: usize,
257 password_len: usize,
258 salt_len: usize,
259) -> Result<(), Error> {
260 if config.parallelism != 1 {
261 return Err(Error::InvalidValue {
262 context: crate::ErrorContext::PasswordHashParallelism,
263 actual: config.parallelism as u64,
264 constraint: crate::ValueConstraint::Between { min: 1, max: 1 },
265 });
266 }
267 crypto_pwhash::validate_pwhash_parameters(
268 output_len,
269 password_len,
270 salt_len,
271 config.opslimit,
272 config.memlimit,
273 config.algorithm,
274 )
275}
276
277fn argon2_into(
279 output: &mut [u8],
280 password: &[u8],
281 salt: &[u8],
282 config: &Config,
283) -> Result<(), Error> {
284 crypto_pwhash::crypto_pwhash(
285 output,
286 password,
287 salt,
288 config.opslimit,
289 config.memlimit,
290 config.algorithm,
291 )
292}
293
294#[derive(Zeroize, Clone)]
295#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
296pub struct PwHash<Hash: Bytes + Zeroize, Salt: Bytes + Zeroize> {
303 hash: Hash,
304 salt: Salt,
305 config: Config,
306}
307
308impl<Hash: Bytes + Zeroize, Salt: Bytes + Zeroize> Drop for PwHash<Hash, Salt> {
309 fn drop(&mut self) {
310 self.hash.zeroize();
311 }
312}
313
314impl<Hash: Bytes + Zeroize, Salt: Bytes + Zeroize> fmt::Debug for PwHash<Hash, Salt> {
315 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316 f.debug_struct("PwHash")
317 .field("hash", &"[REDACTED]")
318 .field("salt", &self.salt.as_slice())
319 .field("config", &self.config)
320 .finish()
321 }
322}
323
324pub type VecPwHash = PwHash<Hash, Salt>;
326
327#[cfg(any(
328 all(feature = "protected", any(unix, windows)),
329 all(doc, not(doctest), feature = "std")
330))]
331#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "protected")))]
332pub mod protected {
333 use super::*;
357 pub use crate::protected::*;
358
359 pub type Salt = HeapBytes;
362 pub type Hash = HeapBytes;
365
366 pub type LockedPwHash = PwHash<Locked<Hash>, Locked<Salt>>;
368}
369
370impl<Hash: NewBytes + ResizableBytes + Zeroize, Salt: NewBytes + ResizableBytes + Zeroize>
371 PwHash<Hash, Salt>
372{
373 pub fn hash<Password: Bytes + ?Sized>(
382 password: &Password,
383 config: Config,
384 ) -> Result<Self, Error> {
385 validate_direct_config(
386 &config,
387 config.hash_length,
388 password.len(),
389 CRYPTO_PWHASH_SALTBYTES,
390 )?;
391
392 let mut hash = Hash::new_bytes();
393 let mut salt = Salt::new_bytes();
394
395 hash.resize(config.hash_length, 0);
396
397 salt.resize(CRYPTO_PWHASH_SALTBYTES, 0);
398 copy_randombytes(salt.as_mut_slice());
399
400 argon2_into(
401 hash.as_mut_slice(),
402 password.as_slice(),
403 salt.as_slice(),
404 &config,
405 )?;
406
407 Ok(Self { hash, salt, config })
408 }
409}
410
411impl<Hash: NewBytes + ResizableBytes + Zeroize, Salt: Bytes + Zeroize> PwHash<Hash, Salt> {
412 pub fn hash_with_salt<Password: Bytes + ?Sized>(
424 password: &Password,
425 salt: Salt,
426 config: Config,
427 ) -> Result<Self, Error> {
428 validate_direct_config(&config, config.hash_length, password.len(), salt.len())?;
429
430 let mut hash = Hash::new_bytes();
431
432 hash.resize(config.hash_length, 0);
433
434 argon2_into(
435 hash.as_mut_slice(),
436 password.as_slice(),
437 salt.as_slice(),
438 &config,
439 )?;
440
441 Ok(Self { hash, salt, config })
442 }
443}
444
445#[cfg(any(feature = "base64", all(doc, not(doctest))))]
446#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
447impl<Hash: Bytes + From<Vec<u8>> + Zeroize, Salt: Bytes + From<Vec<u8>> + Zeroize>
448 PwHash<Hash, Salt>
449{
450 pub fn from_string(hashed_password: &str) -> Result<Self, Error> {
460 let parsed_pwhash = crypto_pwhash::Pwhash::parse_encoded_pwhash(hashed_password)?;
461
462 let opslimit = parsed_pwhash.t_cost.ok_or(Error::missing_data(
463 crate::ErrorContext::PasswordHashTimeCost,
464 ))? as u64;
465 let encoded_memlimit = parsed_pwhash.m_cost.ok_or(Error::missing_data(
466 crate::ErrorContext::PasswordHashMemoryCost,
467 ))?;
468 let memlimit =
469 1024usize
470 .checked_mul(encoded_memlimit as usize)
471 .ok_or(Error::InvalidValue {
472 context: crate::ErrorContext::PasswordHashMemoryCost,
473 actual: encoded_memlimit as u64,
474 constraint: crate::ValueConstraint::Between {
475 min: 0,
476 max: (usize::MAX / 1024) as u64,
477 },
478 })?;
479 let hash = parsed_pwhash
480 .pwhash
481 .ok_or(Error::missing_data(crate::ErrorContext::PasswordHash))?;
482 let salt = parsed_pwhash
483 .salt
484 .ok_or(Error::missing_data(crate::ErrorContext::PasswordHashSalt))?;
485 let algorithm = parsed_pwhash.type_.ok_or(Error::missing_data(
486 crate::ErrorContext::PasswordHashAlgorithm,
487 ))?;
488 let parallelism = parsed_pwhash.parallelism.ok_or(Error::missing_data(
489 crate::ErrorContext::PasswordHashParallelism,
490 ))?;
491 let hash_length = hash.len();
492
493 Ok(Self {
494 hash: hash.into(),
495 salt: salt.into(),
496 config: Config {
497 algorithm,
498 hash_length,
499 memlimit,
500 opslimit,
501 parallelism,
502 },
503 })
504 }
505}
506
507impl<Hash: Bytes + Zeroize, Salt: Bytes + Zeroize> PwHash<Hash, Salt> {
508 #[cfg(any(feature = "base64", all(doc, not(doctest))))]
539 #[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
540 pub fn to_encoded_string(&self) -> Result<String, Error> {
541 let (t_cost, m_cost) =
542 crypto_pwhash::convert_costs_checked(self.config.opslimit, self.config.memlimit)?;
543 crate::argon2::validate_argon2_pwhash_parameters(
544 self.hash.len(),
545 self.salt.len(),
546 t_cost,
547 m_cost,
548 self.config.parallelism,
549 )?;
550
551 let encoded_len = crypto_pwhash::pwhash_string_len(
552 self.config.algorithm,
553 t_cost,
554 m_cost,
555 self.config.parallelism,
556 self.salt.len(),
557 self.hash.len(),
558 )
559 .ok_or(Error::arithmetic_overflow(
560 crate::ErrorContext::PasswordHash,
561 ))?;
562 if encoded_len >= CRYPTO_PWHASH_STRBYTES {
563 return Err(length_error!(
564 crate::ErrorContext::PasswordHash,
565 encoded_len,
566 max CRYPTO_PWHASH_STRBYTES - 1
567 ));
568 }
569 let encoded = crypto_pwhash::pwhash_to_string(
570 self.config.algorithm,
571 t_cost,
572 m_cost,
573 self.config.parallelism,
574 self.salt.as_slice(),
575 self.hash.as_slice(),
576 );
577 debug_assert_eq!(encoded.len(), encoded_len);
578 Ok(encoded)
579 }
580
581 pub fn verify<Password: Bytes + ?Sized>(&self, password: &Password) -> Result<(), Error> {
588 let (t_cost, m_cost) =
589 crypto_pwhash::convert_costs_checked(self.config.opslimit, self.config.memlimit)?;
590 crypto_pwhash::verify_pwhash_parts(
591 self.hash.as_slice(),
592 password.as_slice(),
593 self.salt.as_slice(),
594 t_cost,
595 m_cost,
596 self.config.parallelism,
597 self.config.algorithm,
598 )
599 }
600
601 #[must_use]
608 pub fn from_parts(hash: Hash, salt: Salt, config: Config) -> Self {
609 Self { hash, salt, config }
610 }
611
612 #[must_use]
616 pub fn into_parts(self) -> (Hash, Salt, Config) {
617 let this = core::mem::ManuallyDrop::new(self);
618 unsafe {
622 (
623 core::ptr::read(&this.hash),
624 core::ptr::read(&this.salt),
625 core::ptr::read(&this.config),
626 )
627 }
628 }
629}
630
631impl<Salt: Bytes + Zeroize> PwHash<Hash, Salt> {
632 pub fn derive_keypair<
643 PublicKey: NewByteArray<CRYPTO_BOX_PUBLICKEYBYTES> + Zeroize,
644 SecretKey: NewByteArray<CRYPTO_BOX_SECRETKEYBYTES> + Zeroize,
645 Password: Bytes + Zeroize + ?Sized,
646 >(
647 password: &Password,
648 salt: Salt,
649 config: Config,
650 ) -> Result<keypair::KeyPair<PublicKey, SecretKey>, Error> {
651 validate_direct_config(
652 &config,
653 CRYPTO_BOX_SECRETKEYBYTES,
654 password.len(),
655 salt.len(),
656 )?;
657 let mut secret_key = SecretKey::new_byte_array();
658
659 argon2_into(
660 secret_key.as_mut_slice(),
661 password.as_slice(),
662 salt.as_slice(),
663 &config,
664 )?;
665
666 Ok(keypair::KeyPair::<PublicKey, SecretKey>::from_secret_key(
667 secret_key,
668 ))
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675 use crate::test_prelude::*;
676
677 const PASSWORD: &[u8; 8] = b"password";
681 const SALT: &[u8; CRYPTO_PWHASH_SALTBYTES] = b"0123456789abcdef";
682 const ARGON2ID_MIN_HASH: &str =
683 "771338d819573c67116b39e1788ae8e04b0eb0cf9dfbbfe2e6d746cf3e464fc7";
684 const ARGON2ID_MIN_PUBLIC_KEY: &str =
686 "4a4673d64ae26efee17c8432ffd40f6358ef58f533bc318a5968555d19d1d66f";
687 const ARGON2I_MIN_HASH: &str =
688 "edb3a9e12a39f7528d38ddcc001fd6dfa0c2858bdf8f7910c8c2c74889ab902b";
689 #[cfg(feature = "base64")]
692 const LIBSODIUM_ARGON2ID_STR: &str = concat!(
693 "$argon2id$v=19$m=256,t=3,p=1$MDEyMzQ1Njc$",
694 "G5ajKFCoUzaXRLdz7UJb5wGkb2Xt+X5/GQjUYtS2+TE",
695 );
696
697 fn argon2id_min() -> Config {
698 Config::interactive()
699 .with_opslimit(CRYPTO_PWHASH_OPSLIMIT_MIN)
700 .with_memlimit(CRYPTO_PWHASH_MEMLIMIT_MIN)
701 }
702
703 fn argon2i_min() -> Config {
704 Config::interactive()
705 .with_algorithm(PasswordHashAlgorithm::Argon2i13)
706 .with_opslimit(CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN)
707 .with_memlimit(CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MIN)
708 }
709
710 fn classic_hash(config: &Config) -> Vec<u8> {
711 let mut output = vec![0u8; config.hash_length];
712 crypto_pwhash::crypto_pwhash(
713 &mut output,
714 PASSWORD,
715 SALT,
716 config.opslimit,
717 config.memlimit,
718 config.algorithm,
719 )
720 .expect("classic pwhash");
721 output
722 }
723
724 #[test]
725 fn debug_redacts_hash_bytes() {
726 let pwhash = VecPwHash::from_parts(vec![0xabu8; 32], SALT.to_vec(), argon2id_min());
727 let debug = format!("{pwhash:?}");
728
729 assert!(!debug.contains("171"));
730 }
731
732 #[test]
733 fn dropping_pwhash_zeroizes_its_hash() {
734 struct DropCheckingHash(Vec<u8>);
735
736 impl crate::types::Bytes for DropCheckingHash {
737 fn as_slice(&self) -> &[u8] {
738 &self.0
739 }
740
741 fn len(&self) -> usize {
742 self.0.len()
743 }
744
745 fn is_empty(&self) -> bool {
746 self.0.is_empty()
747 }
748 }
749
750 impl Zeroize for DropCheckingHash {
751 fn zeroize(&mut self) {
752 self.0.zeroize();
753 }
754 }
755
756 impl Drop for DropCheckingHash {
757 fn drop(&mut self) {
758 assert!(self.0.iter().all(|byte| *byte == 0));
759 }
760 }
761
762 drop(PwHash::from_parts(
763 DropCheckingHash(vec![0xabu8; 32]),
764 SALT.to_vec(),
765 argon2id_min(),
766 ));
767 }
768
769 #[cfg(feature = "base64")]
770 #[test]
771 fn from_string_accepts_long_valid_argon2_hash() {
772 let mut hash = [0u8; 64];
773 crate::argon2::argon2_hash(
774 1,
775 8,
776 1,
777 PASSWORD,
778 SALT,
779 None,
780 None,
781 &mut hash,
782 crate::argon2::Argon2Type::Argon2id,
783 )
784 .expect("derive long hash");
785 let encoded = crypto_pwhash::pwhash_to_string(
786 PasswordHashAlgorithm::Argon2id13,
787 1,
788 8,
789 1,
790 SALT,
791 &hash,
792 );
793 assert_eq!(encoded.len(), 136);
794
795 let pwhash = VecPwHash::from_string(&encoded).expect("long hash parses");
796 assert_eq!(pwhash.hash, hash);
797 pwhash.verify(PASSWORD).expect("long hash verifies");
798 }
799 #[test]
800 fn hash_with_salt_matches_libsodium_argon2id_and_classic() {
801 let expected = hex::decode(ARGON2ID_MIN_HASH).expect("hex");
802 let pwhash: VecPwHash =
803 PwHash::hash_with_salt(PASSWORD, SALT.to_vec(), argon2id_min()).expect("hash");
804 assert_eq!(pwhash.hash, expected);
805 assert_eq!(pwhash.salt, SALT);
806 assert_eq!(classic_hash(&argon2id_min()), expected);
807
808 pwhash.verify(PASSWORD).expect("verify failed");
809 assert!(pwhash.verify(b"Password").is_err());
810 assert!(pwhash.verify(b"").is_err());
811
812 let (hash, salt, config) = pwhash.into_parts();
813 assert_eq!(hash, expected);
814 let rebuilt = VecPwHash::from_parts(hash, salt, config);
815 rebuilt.verify(PASSWORD).expect("verify failed");
816
817 let mut other_salt = *SALT;
819 other_salt[0] ^= 1;
820 let other: VecPwHash =
821 PwHash::hash_with_salt(PASSWORD, other_salt.to_vec(), argon2id_min()).expect("hash");
822 assert_ne!(other.hash, expected);
823 let costlier: VecPwHash = PwHash::hash_with_salt(
824 PASSWORD,
825 SALT.to_vec(),
826 argon2id_min().with_opslimit(CRYPTO_PWHASH_OPSLIMIT_MIN + 1),
827 )
828 .expect("hash");
829 assert_ne!(costlier.hash, expected);
830 assert!(costlier.verify(PASSWORD).is_ok());
831 }
832
833 #[test]
834 fn argon2i_configuration_matches_libsodium_and_classic() {
835 let expected = hex::decode(ARGON2I_MIN_HASH).expect("hex");
836 let pwhash: VecPwHash =
837 PwHash::hash_with_salt(PASSWORD, SALT.to_vec(), argon2i_min()).expect("hash");
838 assert_eq!(pwhash.hash, expected);
839 assert_eq!(classic_hash(&argon2i_min()), expected);
840 assert_ne!(pwhash.hash, hex::decode(ARGON2ID_MIN_HASH).expect("hex"));
841 pwhash.verify(PASSWORD).expect("verify failed");
842 assert!(pwhash.verify(b"password ").is_err());
843
844 let below_min = argon2i_min().with_opslimit(CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN - 1);
846 assert!(matches!(
847 VecPwHash::hash_with_salt(PASSWORD, SALT.to_vec(), below_min),
848 Err(Error::InvalidValue {
849 context: crate::ErrorContext::OperationsLimit,
850 ..
851 })
852 ));
853 let derived: Result<keypair::StackKeyPair, Error> = PwHash::derive_keypair(
854 PASSWORD,
855 SALT.to_vec(),
856 argon2i_min().with_opslimit(CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN - 1),
857 );
858 assert!(derived.is_err());
859
860 #[cfg(feature = "base64")]
861 {
862 let encoded = pwhash.to_encoded_string().expect("encode");
863 assert!(encoded.starts_with("$argon2i$v=19$m=8,t=3,p=1$"));
864 let parsed = VecPwHash::from_string(&encoded).expect("parse");
865 assert_eq!(parsed.hash, expected);
866 assert_eq!(parsed.salt, SALT);
867 parsed.verify(PASSWORD).expect("verify failed");
868 assert_eq!(parsed.to_encoded_string().expect("encode"), encoded);
869 crypto_pwhash::crypto_pwhash_str_verify(&encoded, PASSWORD).expect("classic verify");
870 }
871 }
872
873 #[test]
874 fn derive_keypair_is_the_scalar_base_multiple_of_the_argon2_output() {
875 use crate::classic::crypto_core::crypto_scalarmult_base;
876
877 let secret_key = hex::decode(ARGON2ID_MIN_HASH).expect("hex");
878 let public_key = hex::decode(ARGON2ID_MIN_PUBLIC_KEY).expect("hex");
879
880 let keypair: keypair::StackKeyPair =
881 PwHash::derive_keypair(PASSWORD, SALT.to_vec(), argon2id_min()).expect("derive");
882 assert_eq!(keypair.secret_key.as_slice(), secret_key.as_slice());
883 assert_eq!(keypair.public_key.as_slice(), public_key.as_slice());
884
885 let mut classic_secret = [0u8; CRYPTO_BOX_SECRETKEYBYTES];
888 crypto_pwhash::crypto_pwhash(
889 &mut classic_secret,
890 PASSWORD,
891 SALT,
892 CRYPTO_PWHASH_OPSLIMIT_MIN,
893 CRYPTO_PWHASH_MEMLIMIT_MIN,
894 PasswordHashAlgorithm::Argon2id13,
895 )
896 .expect("classic pwhash");
897 let mut classic_public = [0u8; CRYPTO_BOX_PUBLICKEYBYTES];
898 crypto_scalarmult_base(&mut classic_public, &classic_secret);
899 assert_eq!(keypair.secret_key.as_array(), &classic_secret);
900 assert_eq!(keypair.public_key.as_array(), &classic_public);
901 assert_eq!(
902 keypair::StackKeyPair::from_secret_key(keypair.secret_key.clone()),
903 keypair
904 );
905
906 let mut other_salt = *SALT;
909 other_salt[15] ^= 1;
910 let other: keypair::StackKeyPair =
911 PwHash::derive_keypair(PASSWORD, other_salt.to_vec(), argon2id_min()).expect("derive");
912 assert_ne!(other, keypair);
913 let short_salt: Result<keypair::StackKeyPair, Error> =
914 PwHash::derive_keypair(PASSWORD, SALT[..8].to_vec(), argon2id_min());
915 assert!(matches!(
916 short_salt,
917 Err(Error::InvalidLength {
918 context: crate::ErrorContext::PasswordHashSalt,
919 actual: 8,
920 ..
921 })
922 ));
923 }
924
925 #[cfg(feature = "base64")]
926 #[test]
927 fn libsodium_string_vector_parses_verifies_and_reencodes() {
928 let parsed = VecPwHash::from_string(LIBSODIUM_ARGON2ID_STR).expect("parse");
929 assert_eq!(parsed.salt, b"01234567");
930 assert_eq!(parsed.hash.len(), 32);
931 assert_eq!(parsed.config.opslimit, 3);
932 assert_eq!(parsed.config.memlimit, 256 * 1024);
933 assert_eq!(parsed.config.parallelism, 1);
934 assert_eq!(parsed.config.algorithm, PasswordHashAlgorithm::Argon2id13);
935 parsed.verify(PASSWORD).expect("verify failed");
936 assert!(parsed.verify(b"passwore").is_err());
937 assert_eq!(
938 parsed.to_encoded_string().expect("encode"),
939 LIBSODIUM_ARGON2ID_STR
940 );
941
942 let (hash, salt, config) = parsed.into_parts();
945 assert_eq!(salt.len(), 8);
946 assert!(matches!(
947 VecPwHash::hash_with_salt(PASSWORD, salt.clone(), config.clone()),
948 Err(Error::InvalidLength {
949 context: crate::ErrorContext::PasswordHashSalt,
950 actual: 8,
951 ..
952 })
953 ));
954 VecPwHash::from_parts(hash, salt, config)
955 .verify(PASSWORD)
956 .expect("verify failed");
957 }
958
959 #[cfg(feature = "base64")]
960 #[test]
961 fn from_string_rejects_malformed_encodings() {
962 let (prefix, rest) = LIBSODIUM_ARGON2ID_STR.split_at("$argon2id$v=19$".len());
963 let (params, salt_and_hash) = rest.split_at("m=256,t=3,p=1".len());
964 let (salt, hash) = salt_and_hash[1..].split_once('$').expect("salt and hash");
965
966 let malformed = [
967 String::new(),
968 "$".to_string(),
969 "$argon2id$".to_string(),
970 LIBSODIUM_ARGON2ID_STR.trim_start_matches('$').to_string(),
971 format!("{prefix}{params}${salt}"),
972 format!("{prefix}{params}$${hash}"),
973 format!("{prefix}{params}${salt}$"),
974 format!("{prefix}{params}${salt}${hash}$extra"),
975 format!("{prefix}{params}${salt}$!{}", &hash[1..]),
976 format!("{prefix}{params}$!{}${hash}", &salt[1..]),
977 format!("{prefix}t=3,p=1${salt}${hash}"),
978 format!("{prefix}m=256,p=1${salt}${hash}"),
979 format!("{prefix}m=256,t=3${salt}${hash}"),
980 format!("{prefix}m=256,t=3,p=0${salt}${hash}"),
981 format!("{prefix}m=256,t=0,p=1${salt}${hash}"),
982 format!("{prefix}m=0256,t=3,p=1${salt}${hash}"),
983 format!("$argon2id$v=18${params}${salt}${hash}"),
984 format!("$argon2id${params}${salt}${hash}"),
985 format!("$argon2d$v=19${params}${salt}${hash}"),
986 format!("$scrypt$v=19${params}${salt}${hash}"),
987 ];
988 for input in &malformed {
989 assert!(
990 VecPwHash::from_string(input).is_err(),
991 "accepted malformed string {input:?}"
992 );
993 }
994
995 assert_eq!(
998 format!("{prefix}{params}${salt}${hash}"),
999 LIBSODIUM_ARGON2ID_STR
1000 );
1001 VecPwHash::from_string(LIBSODIUM_ARGON2ID_STR).expect("valid string");
1002 }
1003
1004 #[cfg(feature = "base64")]
1005 #[test]
1006 fn encoded_string_reports_rehash_only_when_the_limits_change() {
1007 use crate::classic::crypto_pwhash::crypto_pwhash_str_needs_rehash;
1008
1009 let config = argon2id_min();
1010 let pwhash: VecPwHash =
1011 PwHash::hash_with_salt(PASSWORD, SALT.to_vec(), config.clone()).expect("hash");
1012 let encoded = pwhash.to_encoded_string().expect("encode");
1013
1014 assert!(
1015 !crypto_pwhash_str_needs_rehash(&encoded, config.opslimit, config.memlimit)
1016 .expect("rehash check")
1017 );
1018 assert!(
1019 crypto_pwhash_str_needs_rehash(&encoded, config.opslimit + 1, config.memlimit)
1020 .expect("rehash check")
1021 );
1022 assert!(
1023 crypto_pwhash_str_needs_rehash(&encoded, config.opslimit, config.memlimit * 2)
1024 .expect("rehash check")
1025 );
1026
1027 let wide: VecPwHash =
1030 PwHash::hash_with_salt(PASSWORD, SALT.to_vec(), config.with_hash_length(48))
1031 .expect("hash");
1032 assert_eq!(wide.hash.len(), 48);
1033 let encoded = wide.to_encoded_string().expect("encode");
1034 assert_ne!(encoded, pwhash.to_encoded_string().expect("encode"));
1035 let parsed = VecPwHash::from_string(&encoded).expect("parse");
1036 assert_eq!(parsed.hash, wide.hash);
1037 parsed.verify(PASSWORD).expect("verify failed");
1038 crypto_pwhash::crypto_pwhash_str_verify(&encoded, PASSWORD).expect("classic verify");
1039 }
1040
1041 #[cfg(feature = "serde")]
1042 #[test]
1043 fn config_serde_round_trip_reproduces_the_hash() {
1044 let expected = hex::decode(ARGON2I_MIN_HASH).expect("hex");
1045 let config = argon2i_min();
1046 let json = serde_json::to_string(&config).expect("serialize");
1047 let decoded: Config = serde_json::from_str(&json).expect("deserialize");
1048 assert_eq!(decoded.algorithm, PasswordHashAlgorithm::Argon2i13);
1049 assert_eq!(decoded.opslimit, config.opslimit);
1050 assert_eq!(decoded.memlimit, config.memlimit);
1051 assert_eq!(decoded.parallelism, config.parallelism);
1052 assert_eq!(decoded.hash_length, config.hash_length);
1053
1054 let pwhash: VecPwHash =
1055 PwHash::hash_with_salt(PASSWORD, SALT.to_vec(), decoded.clone()).expect("hash");
1056 assert_eq!(pwhash.hash, expected);
1057 VecPwHash::from_parts(expected, SALT.to_vec(), decoded)
1058 .verify(PASSWORD)
1059 .expect("verify failed");
1060
1061 let argon2id = argon2i_min().with_algorithm(PasswordHashAlgorithm::Argon2id13);
1064 assert!(
1065 VecPwHash::from_parts(
1066 hex::decode(ARGON2I_MIN_HASH).expect("hex"),
1067 SALT.to_vec(),
1068 argon2id
1069 )
1070 .verify(PASSWORD)
1071 .is_err()
1072 );
1073 }
1074
1075 #[cfg(dryoc_native_tests)]
1076 #[test]
1077 fn hash_with_salt_matches_libsodium_for_both_algorithms() {
1078 crate::native_test_util::init();
1079 for config in [argon2id_min(), argon2i_min()] {
1080 let pwhash: VecPwHash =
1081 PwHash::hash_with_salt(PASSWORD, SALT.to_vec(), config.clone()).expect("hash");
1082 let mut sodium = vec![0u8; config.hash_length];
1083 let rc = unsafe {
1084 libsodium_sys::crypto_pwhash(
1085 sodium.as_mut_ptr(),
1086 config.hash_length as u64,
1087 PASSWORD.as_ptr().cast(),
1088 PASSWORD.len() as u64,
1089 SALT.as_ptr(),
1090 config.opslimit,
1091 config.memlimit,
1092 config.algorithm as i32,
1093 )
1094 };
1095 assert_eq!(rc, 0);
1096 assert_eq!(pwhash.hash, sodium);
1097 }
1098 }
1099
1100 #[test]
1101 fn test_pwhash_uses_random_salt() {
1102 let password = b"super secrit password";
1103
1104 let hash = || {
1107 if cfg!(miri) {
1108 VecPwHash::hash(password, argon2id_min())
1109 } else {
1110 VecPwHash::hash(password, Config::interactive())
1111 }
1112 .expect("unable to hash")
1113 };
1114 let pwhash1 = hash();
1115 let pwhash2 = hash();
1116
1117 assert_ne!(pwhash1.salt.as_slice(), pwhash2.salt.as_slice());
1118
1119 pwhash1.verify(password).expect("verification failed");
1120 pwhash2.verify(password).expect("verification failed");
1121 }
1122
1123 #[test]
1124 fn test_pwhash_validates_output_length_before_allocation() {
1125 let config = Config::interactive().with_hash_length(usize::MAX);
1126 assert!(matches!(
1127 VecPwHash::hash(b"password", config),
1128 Err(Error::InvalidLength {
1129 context: crate::ErrorContext::Output,
1130 actual: usize::MAX,
1131 ..
1132 })
1133 ));
1134 }
1135
1136 #[cfg(feature = "serde")]
1137 #[test]
1138 fn test_pwhash_serde_roundtrip_preserves_verification() {
1139 let password = b"serde password";
1140 let config = Config::interactive()
1141 .with_opslimit(CRYPTO_PWHASH_OPSLIMIT_MIN)
1142 .with_memlimit(CRYPTO_PWHASH_MEMLIMIT_MIN);
1143 let pwhash = VecPwHash::hash(password, config).expect("unable to hash");
1144
1145 let json = serde_json::to_string(&pwhash).expect("unable to serialize password hash");
1146 let decoded: VecPwHash =
1147 serde_json::from_str(&json).expect("unable to deserialize password hash");
1148
1149 decoded.verify(password).expect("verification failed");
1150 decoded
1151 .verify(b"wrong password")
1152 .expect_err("wrong password should not verify");
1153
1154 #[cfg(feature = "base64")]
1155 decoded
1156 .to_encoded_string()
1157 .expect("unable to encode deserialized password hash");
1158 }
1159
1160 #[cfg(feature = "base64")]
1161 #[test]
1162 fn test_pwhash_str() {
1163 let password = b"super secrit password";
1164
1165 let config = if cfg!(miri) {
1166 argon2id_min()
1167 } else {
1168 Config::interactive()
1169 };
1170 let pwhash = VecPwHash::hash(password, config).expect("unable to hash");
1171 let pw_string = pwhash
1172 .to_encoded_string()
1173 .expect("couldn't encode password hash");
1174
1175 let parsed_pwhash =
1176 VecPwHash::from_string(&pw_string).expect("couldn't parse hashed password");
1177
1178 parsed_pwhash.verify(password).expect("verification failed");
1179 parsed_pwhash
1180 .verify(b"invalid password")
1181 .expect_err("verification should have failed");
1182
1183 let argon2i = concat!(
1184 "$argon2i$v=19$m=4096,t=3,p=2$b2RpZHVlamRpc29kaXNrdw$",
1185 "TNnWIwlu1061JHrnCqIAmjs3huSxYIU+0jWipu7Kc9M",
1186 );
1187 let parsed_argon2i =
1188 VecPwHash::from_string(argon2i).expect("valid Argon2i string should parse");
1189 #[cfg(not(miri))]
1192 parsed_argon2i
1193 .verify(b"password")
1194 .expect("valid Argon2i string should verify");
1195 assert_eq!(
1196 parsed_argon2i
1197 .to_encoded_string()
1198 .expect("couldn't re-encode hash"),
1199 argon2i
1200 );
1201
1202 let oversized_encoding = VecPwHash::from_parts(
1203 vec![0u8; 64],
1204 vec![0u8; CRYPTO_PWHASH_SALTBYTES],
1205 Config::interactive().with_hash_length(64),
1206 );
1207 assert!(oversized_encoding.to_encoded_string().is_err());
1208 }
1209
1210 #[test]
1211 #[cfg(all(feature = "protected", any(unix, windows)))]
1212 fn test_protected() {
1213 use crate::pwhash::protected::*;
1214
1215 let password =
1216 HeapBytes::from_slice_into_locked(b"juicy password").expect("couldn't lock password");
1217
1218 let pwhash: LockedPwHash =
1219 PwHash::hash(&password, Config::interactive()).expect("unable to hash");
1220
1221 pwhash.verify(&password).expect("verification failed");
1222 pwhash
1223 .verify(b"invalid password")
1224 .expect_err("verification should have failed");
1225 }
1226}