Skip to main content

dryoc/
pwhash.rs

1//! # Password hashing functions
2//!
3//! [`PwHash`] implements libsodium's password hashing functions, based on
4//! Argon2.
5//!
6//! Argon2 is a memory-hard password hashing function. Its work and memory
7//! settings make each password guess more expensive, which slows offline
8//! guessing if a password database is stolen. These settings do not compensate
9//! for weak passwords, so applications should still encourage long, unique
10//! passwords.
11//!
12//! You should use [`PwHash`] when you want to:
13//!
14//! * authenticate with passwords, and store their salted hashes in a database
15//! * derive secret keys based on passphrases
16//!
17//! Use a general-purpose hash such as [`crate::generichash`] or
18//! [`crate::sha256`] for arbitrary data. Password hashing is deliberately much
19//! more expensive.
20//!
21//! If the `serde` feature is enabled, the
22//! [`serde::Deserialize`](https://docs.rs/serde/latest/serde/trait.Deserialize.html) and
23//! [`serde::Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) traits will be
24//! implemented for [`PwHash`].
25//!
26//! ## Rustaceous API example
27//!
28//! ```
29//! use dryoc::pwhash::*;
30//!
31//! // A strong passphrase
32//! let password = b"But, for my own part, it was Greek to me.";
33//!
34//! // Hash the password, generating a random salt
35//! let pwhash = VecPwHash::hash(password, Config::interactive()).expect("unable to hash");
36//!
37//! pwhash.verify(password).expect("verification failed");
38//! pwhash
39//!     .verify(b"invalid password")
40//!     .expect_err("verification should have failed");
41//! ```
42//!
43//! ## Using a custom config, or your own salt
44//!
45//! ```
46//! use dryoc::pwhash::*;
47//!
48//! // Generate a random salt
49//! let mut salt = Salt::default();
50//! salt.resize(dryoc::constants::CRYPTO_PWHASH_SALTBYTES, 0);
51//! dryoc::rng::copy_randombytes(&mut salt);
52//!
53//! // A strong passphrase
54//! let password = b"What's in a name? That which we call a rose\n
55//!                  By any other word would smell as sweet...";
56//!
57//! // Start with a preset, then increase its work factor if your deployment can
58//! // tolerate the extra time. Benchmark the result on the slowest target.
59//! let mut config = Config::interactive()
60//!     .with_opslimit(dryoc::constants::CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE + 1);
61//! # // Keep this doctest fast; these minimums are not a production recommendation.
62//! # config = config
63//! #     .with_opslimit(dryoc::constants::CRYPTO_PWHASH_OPSLIMIT_MIN)
64//! #     .with_memlimit(dryoc::constants::CRYPTO_PWHASH_MEMLIMIT_MIN);
65//!
66//! // With customized configuration parameters, the return type must be explicit.
67//! let pwhash: VecPwHash = PwHash::hash_with_salt(password, salt, config)
68//!     .expect("unable to hash password with salt and custom config");
69//!
70//! pwhash.verify(password).expect("verification failed");
71//! pwhash
72//!     .verify(b"invalid password")
73//!     .expect_err("verification should have failed");
74//! ```
75//!
76//! ## Deriving a keypair from a passphrase and salt
77//!
78//! ```
79//! use dryoc::keypair::StackKeyPair;
80//! use dryoc::pwhash::*;
81//!
82//! // Generate a random salt
83//! let mut salt = Salt::default();
84//! salt.resize(dryoc::constants::CRYPTO_PWHASH_SALTBYTES, 0);
85//! dryoc::rng::copy_randombytes(&mut salt);
86//!
87//! // Use a strong passphrase
88//! let password = b"Is this a dagger which I see before me, the handle toward my hand?";
89//!
90//! let keypair: StackKeyPair = PwHash::derive_keypair(password, salt, Config::interactive())
91//!     .expect("couldn't derive keypair");
92//!
93//! // now you can use `keypair` with DryocBox
94//! ```
95//!
96//! ## String-based encoding
97//!
98//! See [`PwHash::to_encoded_string()`] for an example of using the string-based
99//! encoding API, compatible with `crypto_pwhash_str*` functions.
100//!
101//! ## Additional resources
102//!
103//! * See the [libsodium documentation](https://doc.libsodium.org/password_hashing)
104//!   for more about password hashing
105//! * See the [`protected`] module for examples that keep passwords and keys in
106//!   protected memory
107
108#[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
125/// Heap-allocated salt type alias for password hashing with [`PwHash`].
126///
127/// Newly generated salts contain exactly [`CRYPTO_PWHASH_SALTBYTES`] bytes.
128/// Parsed Argon2 strings may contain other valid Argon2 salt lengths. Each
129/// stored password hash needs a unique, unpredictable salt;
130/// [`PwHash::hash`] generates one automatically.
131pub type Salt = Vec<u8>;
132/// Heap-allocated hash type alias for password hashing with [`PwHash`].
133///
134/// Hashes must contain at least [`CRYPTO_PWHASH_BYTES_MIN`] bytes.
135pub type Hash = Vec<u8>;
136
137#[derive(Zeroize, Clone, Debug)]
138#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
139/// Password hash configuration parameters.
140///
141/// [`Config::interactive`] is the default and is suitable for online
142/// authentication. [`Config::moderate`] and [`Config::sensitive`] spend more
143/// time and memory per password guess. Benchmark the chosen preset on the
144/// slowest supported system, and account for the number of concurrent hashes
145/// when setting memory limits.
146pub struct Config {
147    algorithm: PasswordHashAlgorithm,
148    hash_length: usize,
149    memlimit: usize,
150    opslimit: u64,
151    parallelism: u32,
152}
153
154impl Config {
155    /// Selects the password-hashing algorithm.
156    ///
157    /// The preset resource limits target Argon2id. When selecting Argon2i,
158    /// choose limits that satisfy the corresponding `CRYPTO_PWHASH_ARGON2I_*`
159    /// constants.
160    #[must_use]
161    pub fn with_algorithm(self, algorithm: PasswordHashAlgorithm) -> Self {
162        Self { algorithm, ..self }
163    }
164
165    /// Sets the hash output length in bytes.
166    ///
167    /// The length must be between [`CRYPTO_PWHASH_BYTES_MIN`] and
168    /// [`CRYPTO_PWHASH_BYTES_MAX`], inclusive. Invalid values are reported when
169    /// the config is used to hash a password.
170    #[must_use]
171    pub fn with_hash_length(self, hash_length: usize) -> Self {
172        Self {
173            hash_length,
174            ..self
175        }
176    }
177
178    /// Sets the approximate memory cost in bytes.
179    ///
180    /// More memory makes parallel guessing more expensive, but every
181    /// concurrent hash also consumes that memory. The value must be between
182    /// [`CRYPTO_PWHASH_MEMLIMIT_MIN`] and [`CRYPTO_PWHASH_MEMLIMIT_MAX`],
183    /// inclusive.
184    #[must_use]
185    pub fn with_memlimit(self, memlimit: usize) -> Self {
186        Self { memlimit, ..self }
187    }
188
189    /// Sets the computation cost.
190    ///
191    /// Larger values take longer and make each password guess more expensive.
192    /// The supported range depends on the selected algorithm. See the
193    /// `CRYPTO_PWHASH_ARGON2I_OPSLIMIT_*` and
194    /// `CRYPTO_PWHASH_ARGON2ID_OPSLIMIT_*` constants.
195    #[must_use]
196    pub fn with_opslimit(self, opslimit: u64) -> Self {
197        Self { opslimit, ..self }
198    }
199
200    /// Returns libsodium's interactive password hashing configuration.
201    ///
202    /// This is the default preset for online operations where users wait for
203    /// the result.
204    #[must_use]
205    pub fn interactive() -> Self {
206        Self::preset(
207            CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
208            CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
209        )
210    }
211
212    /// Returns libsodium's moderate password hashing configuration.
213    ///
214    /// This preset uses more time and memory than [`Config::interactive`].
215    #[must_use]
216    pub fn moderate() -> Self {
217        Self::preset(
218            CRYPTO_PWHASH_OPSLIMIT_MODERATE,
219            CRYPTO_PWHASH_MEMLIMIT_MODERATE,
220        )
221    }
222
223    /// Returns libsodium's sensitive password hashing configuration.
224    ///
225    /// This preset has the highest resource requirements. Use it only when the
226    /// deployment can tolerate its latency and memory use.
227    #[must_use]
228    pub fn sensitive() -> Self {
229        Self::preset(
230            CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
231            CRYPTO_PWHASH_MEMLIMIT_SENSITIVE,
232        )
233    }
234
235    /// Returns the Argon2id13 configuration with the given resource limits,
236    /// shared by the three libsodium presets.
237    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
277/// Runs Argon2 over `password` and `salt` into `output` per `config`.
278fn 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))]
296/// Password hash implementation based on Argon2, compatible with libsodium's
297/// `crypto_pwhash_*` functions.
298///
299/// The hash bytes are redacted from [`Debug`] output and wiped when the
300/// instance is dropped. [`PwHash::into_parts`] transfers ownership of the hash
301/// to the caller, who is then responsible for its handling and zeroization.
302pub 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
324/// `Vec<u8>`-based PwHash type alias, provided for convenience.
325pub 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    //! # Protected memory type aliases for [`PwHash`]
334    //!
335    //! Protected-memory aliases for password hashes and salts.
336    //!
337    //! ## Example
338    //!
339    //! ```
340    //! use dryoc::pwhash::protected::*;
341    //! use dryoc::pwhash::{Config, PwHash};
342    //!
343    //! let password = HeapBytes::from_slice_into_locked(
344    //!     b"The robb'd that smiles, steals something from the thief.",
345    //! )
346    //! .expect("couldn't lock password");
347    //!
348    //! let pwhash: LockedPwHash =
349    //!     PwHash::hash(&password, Config::interactive()).expect("unable to hash");
350    //!
351    //! pwhash.verify(&password).expect("verification failed");
352    //! pwhash
353    //!     .verify(b"invalid password")
354    //!     .expect_err("verification should have failed");
355    //! ```
356    use super::*;
357    pub use crate::protected::*;
358
359    /// Heap-allocated, page-aligned salt type alias for protected password
360    /// hashing with [`PwHash`].
361    pub type Salt = HeapBytes;
362    /// Heap-allocated, page-aligned hash type alias for protected password
363    /// hashing with [`PwHash`].
364    pub type Hash = HeapBytes;
365
366    /// Locked [`PwHash`], provided as a type alias for convenience.
367    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    /// Hashes `password` with a random salt and `config`, returning
374    /// the hash, salt, and config upon success.
375    ///
376    /// # Errors
377    ///
378    /// Returns an error if a work limit, memory limit, hash length, or password
379    /// length is outside the supported range, or if the
380    /// underlying Argon2 operation fails.
381    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    /// Hashes `password` with `salt` and `config`, returning
413    /// the hash, salt, and config upon success.
414    ///
415    /// The caller must provide a unique, unpredictable salt for each password.
416    /// Prefer [`PwHash::hash`] unless an existing salt must be reused.
417    ///
418    /// # Errors
419    ///
420    /// Returns an error if a work limit, memory limit, hash length, salt
421    /// length, or password length is outside the supported range, or if the
422    /// underlying Argon2 operation fails.
423    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    /// Creates a new password hash instance by parsing `hashed_password`.
451    /// Compatible with libsodium's `crypto_pwhash_str*` functions, including
452    /// valid Argon2 strings with non-default salt lengths or parallelism.
453    ///
454    /// # Errors
455    ///
456    /// Returns an error if the string is malformed, uses an unsupported
457    /// algorithm or version, omits a required field, or contains an invalid
458    /// encoded value.
459    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    /// Returns a string-encoded representation of this hash, salt, and config,
509    /// suitable for storage in a database.
510    ///
511    /// The string returned is compatible with libsodium's `crypto_pwhash_str`,
512    /// `crypto_pwhash_str_verify`, and `crypto_pwhash_str_needs_rehash`
513    /// functions when the hash length matches libsodium's string format. The
514    /// lower-level hashing API also supports variable-length hash output.
515    ///
516    /// # Errors
517    ///
518    /// Returns an error if the stored parameters are invalid or the resulting
519    /// string would not fit libsodium's password-hash string format.
520    ///
521    /// ## Example
522    ///
523    /// ```
524    /// use dryoc::pwhash::*;
525    ///
526    /// let password = b"Come what come may, time and the hour runs through the roughest day.";
527    ///
528    /// let pwhash = VecPwHash::hash(password, Config::interactive()).expect("unable to hash");
529    /// let pw_string = pwhash.to_encoded_string().expect("unable to encode hash");
530    ///
531    /// let parsed_pwhash = VecPwHash::from_string(&pw_string).expect("couldn't parse hashed password");
532    ///
533    /// parsed_pwhash.verify(password).expect("verification failed");
534    /// parsed_pwhash
535    ///     .verify(b"invalid password")
536    ///     .expect_err("verification should have failed");
537    /// ```
538    #[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    /// Verifies `password` against this hash using its salt and configuration.
582    ///
583    /// # Errors
584    ///
585    /// Returns an error if the password does not match, if the stored salt or
586    /// configuration is invalid, or if the underlying Argon2 operation fails.
587    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    /// Constructs a new instance from `hash`, `salt`, and `config`, consuming
602    /// them.
603    ///
604    /// This function does not validate the parts. Invalid values are reported
605    /// when an operation such as [`PwHash::verify`] or
606    /// [`PwHash::to_encoded_string`] uses them.
607    #[must_use]
608    pub fn from_parts(hash: Hash, salt: Salt, config: Config) -> Self {
609        Self { hash, salt, config }
610    }
611
612    /// Moves the hash, salt, and config out of this instance, returning them as
613    /// a tuple. The returned hash no longer benefits from the instance's
614    /// drop-time zeroization.
615    #[must_use]
616    pub fn into_parts(self) -> (Hash, Salt, Config) {
617        let this = core::mem::ManuallyDrop::new(self);
618        // SAFETY: Each field is read exactly once from `this`; suppressing its
619        // destructor transfers the hash without wiping it before the caller
620        // receives ownership.
621        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    /// Derives a keypair from `password` and `salt`, using `config`.
633    ///
634    /// The same password and salt derive the same keypair. Store the salt, keep
635    /// it unique per derived key, and do not treat it as secret.
636    ///
637    /// # Errors
638    ///
639    /// Returns an error if a work limit, memory limit, salt length, or password
640    /// length is outside the supported range, or if the underlying Argon2
641    /// operation fails.
642    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    /// libsodium `crypto_pwhash` outputs for password `"password"`, salt
678    /// `"0123456789abcdef"`, 32 output bytes, and the minimum cost of each
679    /// algorithm (Argon2id: opslimit 1; Argon2i: opslimit 3; both 8 KiB).
680    const PASSWORD: &[u8; 8] = b"password";
681    const SALT: &[u8; CRYPTO_PWHASH_SALTBYTES] = b"0123456789abcdef";
682    const ARGON2ID_MIN_HASH: &str =
683        "771338d819573c67116b39e1788ae8e04b0eb0cf9dfbbfe2e6d746cf3e464fc7";
684    /// `crypto_scalarmult_base` of `ARGON2ID_MIN_HASH`.
685    const ARGON2ID_MIN_PUBLIC_KEY: &str =
686        "4a4673d64ae26efee17c8432ffd40f6358ef58f533bc318a5968555d19d1d66f";
687    const ARGON2I_MIN_HASH: &str =
688        "edb3a9e12a39f7528d38ddcc001fd6dfa0c2858bdf8f7910c8c2c74889ab902b";
689    /// libsodium `test/default/pwhash_argon2id.c` string vector for
690    /// `"password"`.
691    #[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        // A different salt or cost is a different hash.
818        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        // Argon2i has a higher minimum opslimit than Argon2id.
845        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        // The classic composition: `crypto_pwhash` into the secret key, then
886        // `crypto_scalarmult_base` (i.e. `KeyPair::from_secret_key`).
887        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        // Same password with a different salt derives a different keypair, and
907        // the salt length is validated.
908        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        // The direct hashing API requires libsodium's fixed salt length, so
943        // the parsed 8-byte salt only works through `verify`.
944        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        // The unmodified vector still parses, so the rejections above are not
996        // an artifact of the reconstruction.
997        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        // A wider hash still fits libsodium's string format and round-trips
1028        // with its length intact.
1029        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        // Dropping the algorithm field changes the result: an Argon2id config
1062        // with the same limits does not verify the Argon2i hash.
1063        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        // Production-cost Argon2 is too expensive to interpret. Salt
1105        // generation and verification exercise the same path at minimum cost.
1106        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        // Miri checks multi-lane Argon2i with the smaller RFC vector in
1190        // argon2::tests; retain parsing and re-encoding this 4 MiB vector.
1191        #[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}