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 = PwHash::hash_with_defaults(password).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 <https://libsodium.gitbook.io/doc/password_hashing> for additional
104//!   details on password hashing
105//! * Refer to the [protected] module for details on usage with protected
106//!   memory.
107
108#[cfg(feature = "serde")]
109use serde::{Deserialize, Serialize};
110use zeroize::Zeroize;
111
112use crate::classic::crypto_pwhash;
113pub use crate::classic::crypto_pwhash::PasswordHashAlgorithm;
114use crate::constants::*;
115use crate::error::Error;
116use crate::keypair;
117use crate::rng::copy_randombytes;
118use crate::types::*;
119
120/// Heap-allocated salt type alias for password hashing with [`PwHash`].
121///
122/// Newly generated salts contain exactly [`CRYPTO_PWHASH_SALTBYTES`] bytes.
123/// Parsed Argon2 strings may contain other valid Argon2 salt lengths. Each
124/// stored password hash needs a unique, unpredictable salt;
125/// [`PwHash::hash`] generates one automatically.
126pub type Salt = Vec<u8>;
127/// Heap-allocated hash type alias for password hashing with [`PwHash`].
128///
129/// Hashes must contain at least [`CRYPTO_PWHASH_BYTES_MIN`] bytes.
130pub type Hash = Vec<u8>;
131
132#[cfg_attr(
133    feature = "serde",
134    derive(Zeroize, Clone, Debug, Serialize, Deserialize)
135)]
136#[cfg_attr(not(feature = "serde"), derive(Zeroize, Clone, Debug))]
137/// Password hash configuration parameters.
138///
139/// [`Config::interactive`] is the default and is suitable for online
140/// authentication. [`Config::moderate`] and [`Config::sensitive`] spend more
141/// time and memory per password guess. Benchmark the chosen preset on the
142/// slowest supported system, and account for the number of concurrent hashes
143/// when setting memory limits.
144pub struct Config {
145    algorithm: PasswordHashAlgorithm,
146    hash_length: usize,
147    memlimit: usize,
148    opslimit: u64,
149    parallelism: u32,
150}
151
152impl Config {
153    /// Selects the password-hashing algorithm.
154    ///
155    /// The preset resource limits target Argon2id. When selecting Argon2i,
156    /// choose limits that satisfy the corresponding `CRYPTO_PWHASH_ARGON2I_*`
157    /// constants.
158    #[must_use]
159    pub fn with_algorithm(self, algorithm: PasswordHashAlgorithm) -> Self {
160        Self { algorithm, ..self }
161    }
162
163    /// Sets the hash output length in bytes.
164    ///
165    /// The length must be between [`CRYPTO_PWHASH_BYTES_MIN`] and
166    /// [`CRYPTO_PWHASH_BYTES_MAX`], inclusive. Invalid values are reported when
167    /// the config is used to hash a password.
168    #[must_use]
169    pub fn with_hash_length(self, hash_length: usize) -> Self {
170        Self {
171            hash_length,
172            ..self
173        }
174    }
175
176    /// Sets the approximate memory cost in bytes.
177    ///
178    /// More memory makes parallel guessing more expensive, but every
179    /// concurrent hash also consumes that memory. The value must be between
180    /// [`CRYPTO_PWHASH_MEMLIMIT_MIN`] and [`CRYPTO_PWHASH_MEMLIMIT_MAX`],
181    /// inclusive.
182    #[must_use]
183    pub fn with_memlimit(self, memlimit: usize) -> Self {
184        Self { memlimit, ..self }
185    }
186
187    /// Sets the computation cost.
188    ///
189    /// Larger values take longer and make each password guess more expensive.
190    /// The supported range depends on the selected algorithm. See the
191    /// `CRYPTO_PWHASH_ARGON2I_OPSLIMIT_*` and
192    /// `CRYPTO_PWHASH_ARGON2ID_OPSLIMIT_*` constants.
193    #[must_use]
194    pub fn with_opslimit(self, opslimit: u64) -> Self {
195        Self { opslimit, ..self }
196    }
197
198    /// Returns libsodium's interactive password hashing configuration.
199    ///
200    /// This is the default preset for online operations where users wait for
201    /// the result.
202    pub fn interactive() -> Self {
203        Self {
204            algorithm: PasswordHashAlgorithm::Argon2id13,
205            opslimit: CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
206            memlimit: CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
207            parallelism: 1,
208            hash_length: crypto_pwhash::STR_HASHBYTES,
209        }
210    }
211
212    /// Returns libsodium's moderate password hashing configuration.
213    ///
214    /// This preset uses more time and memory than [`Config::interactive`].
215    pub fn moderate() -> Self {
216        Self {
217            algorithm: PasswordHashAlgorithm::Argon2id13,
218            opslimit: CRYPTO_PWHASH_OPSLIMIT_MODERATE,
219            memlimit: CRYPTO_PWHASH_MEMLIMIT_MODERATE,
220            parallelism: 1,
221            hash_length: crypto_pwhash::STR_HASHBYTES,
222        }
223    }
224
225    /// Returns libsodium's sensitive password hashing configuration.
226    ///
227    /// This preset has the highest resource requirements. Use it only when the
228    /// deployment can tolerate its latency and memory use.
229    pub fn sensitive() -> Self {
230        Self {
231            algorithm: PasswordHashAlgorithm::Argon2id13,
232            opslimit: CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
233            memlimit: CRYPTO_PWHASH_MEMLIMIT_SENSITIVE,
234            parallelism: 1,
235            hash_length: crypto_pwhash::STR_HASHBYTES,
236        }
237    }
238}
239
240impl Default for Config {
241    fn default() -> Self {
242        Self::interactive()
243    }
244}
245
246fn validate_direct_config(
247    config: &Config,
248    output_len: usize,
249    password_len: usize,
250    salt_len: usize,
251) -> Result<(), Error> {
252    if config.parallelism != 1 {
253        return Err(Error::InvalidValue {
254            context: crate::ErrorContext::PasswordHashParallelism,
255            actual: config.parallelism as u64,
256            constraint: crate::ValueConstraint::Between { min: 1, max: 1 },
257        });
258    }
259    crypto_pwhash::validate_pwhash_parameters(
260        output_len,
261        password_len,
262        salt_len,
263        config.opslimit,
264        config.memlimit,
265        config.algorithm,
266    )
267}
268
269#[cfg_attr(
270    feature = "serde",
271    derive(Zeroize, Clone, Debug, Serialize, Deserialize)
272)]
273#[cfg_attr(not(feature = "serde"), derive(Zeroize, Clone, Debug))]
274/// Password hash implementation based on Argon2, compatible with libsodium's
275/// `crypto_pwhash_*` functions.
276pub struct PwHash<Hash: Bytes + Zeroize, Salt: Bytes + Zeroize> {
277    hash: Hash,
278    salt: Salt,
279    config: Config,
280}
281
282/// `Vec<u8>`-based PwHash type alias, provided for convenience.
283pub type VecPwHash = PwHash<Hash, Salt>;
284
285#[cfg(any(all(feature = "protected", any(unix, windows)), all(doc, not(doctest))))]
286#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "protected")))]
287pub mod protected {
288    //! # Protected memory type aliases for [`PwHash`]
289    //!
290    //! Protected-memory aliases for password hashes and salts.
291    //!
292    //! ## Example
293    //!
294    //! ```
295    //! use dryoc::pwhash::protected::*;
296    //! use dryoc::pwhash::{Config, PwHash};
297    //!
298    //! let password = HeapBytes::from_slice_into_locked(
299    //!     b"The robb'd that smiles, steals something from the thief.",
300    //! )
301    //! .expect("couldn't lock password");
302    //!
303    //! let pwhash: LockedPwHash =
304    //!     PwHash::hash(&password, Config::interactive()).expect("unable to hash");
305    //!
306    //! pwhash.verify(&password).expect("verification failed");
307    //! pwhash
308    //!     .verify(b"invalid password")
309    //!     .expect_err("verification should have failed");
310    //! ```
311    use super::*;
312    pub use crate::protected::*;
313
314    /// Heap-allocated, page-aligned salt type alias for protected password
315    /// hashing with [`PwHash`].
316    pub type Salt = HeapBytes;
317    /// Heap-allocated, page-aligned hash type alias for protected password
318    /// hashing with [`PwHash`].
319    pub type Hash = HeapBytes;
320
321    /// Locked [`PwHash`], provided as a type alias for convenience.
322    pub type LockedPwHash = PwHash<Locked<Hash>, Locked<Salt>>;
323}
324
325impl<Hash: NewBytes + ResizableBytes + Zeroize, Salt: NewBytes + ResizableBytes + Zeroize>
326    PwHash<Hash, Salt>
327{
328    /// Hashes `password` with a random salt and `config`, returning
329    /// the hash, salt, and config upon success.
330    ///
331    /// # Errors
332    ///
333    /// Returns an error if a work limit, memory limit, hash length, or password
334    /// length is outside the supported range, or if the
335    /// underlying Argon2 operation fails.
336    pub fn hash<Password: Bytes>(password: &Password, config: Config) -> Result<Self, Error> {
337        validate_direct_config(
338            &config,
339            config.hash_length,
340            password.len(),
341            CRYPTO_PWHASH_SALTBYTES,
342        )?;
343
344        let mut hash = Hash::new_bytes();
345        let mut salt = Salt::new_bytes();
346
347        hash.resize(config.hash_length, 0);
348
349        salt.resize(CRYPTO_PWHASH_SALTBYTES, 0);
350        copy_randombytes(salt.as_mut_slice());
351
352        crypto_pwhash::crypto_pwhash(
353            hash.as_mut_slice(),
354            password.as_slice(),
355            salt.as_slice(),
356            config.opslimit,
357            config.memlimit,
358            config.algorithm,
359        )?;
360
361        Ok(Self { hash, salt, config })
362    }
363
364    /// Hashes `password` with a random salt and a default configuration
365    /// suitable for interactive hashing, returning the hash, salt, and config
366    /// upon success.
367    ///
368    /// # Errors
369    ///
370    /// Returns the same errors as [`PwHash::hash`].
371    pub fn hash_interactive<Password: Bytes>(password: &Password) -> Result<Self, Error> {
372        Self::hash(password, Config::interactive())
373    }
374
375    /// Hashes `password` with a random salt and a default configuration
376    /// suitable for moderate hashing, returning the hash, salt, and config upon
377    /// success.
378    ///
379    /// # Errors
380    ///
381    /// Returns the same errors as [`PwHash::hash`].
382    pub fn hash_moderate<Password: Bytes>(password: &Password) -> Result<Self, Error> {
383        Self::hash(password, Config::moderate())
384    }
385
386    /// Hashes `password` with a random salt and a default configuration
387    /// suitable for sensitive hashing, returning the hash, salt, and config
388    /// upon success.
389    ///
390    /// # Errors
391    ///
392    /// Returns the same errors as [`PwHash::hash`].
393    pub fn hash_sensitive<Password: Bytes>(password: &Password) -> Result<Self, Error> {
394        Self::hash(password, Config::sensitive())
395    }
396}
397
398impl<Hash: NewBytes + ResizableBytes + Zeroize, Salt: Bytes + Zeroize> PwHash<Hash, Salt> {
399    /// Hashes `password` with `salt` and `config`, returning
400    /// the hash, salt, and config upon success.
401    ///
402    /// The caller must provide a unique, unpredictable salt for each password.
403    /// Prefer [`PwHash::hash`] unless an existing salt must be reused.
404    ///
405    /// # Errors
406    ///
407    /// Returns an error if a work limit, memory limit, hash length, salt
408    /// length, or password length is outside the supported range, or if the
409    /// underlying Argon2 operation fails.
410    pub fn hash_with_salt<Password: Bytes>(
411        password: &Password,
412        salt: Salt,
413        config: Config,
414    ) -> Result<Self, Error> {
415        validate_direct_config(&config, config.hash_length, password.len(), salt.len())?;
416
417        let mut hash = Hash::new_bytes();
418
419        hash.resize(config.hash_length, 0);
420
421        crypto_pwhash::crypto_pwhash(
422            hash.as_mut_slice(),
423            password.as_slice(),
424            salt.as_slice(),
425            config.opslimit,
426            config.memlimit,
427            config.algorithm,
428        )?;
429
430        Ok(Self { hash, salt, config })
431    }
432}
433
434#[cfg(any(feature = "base64", all(doc, not(doctest))))]
435#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
436impl<Hash: Bytes + From<Vec<u8>> + Zeroize, Salt: Bytes + From<Vec<u8>> + Zeroize>
437    PwHash<Hash, Salt>
438{
439    /// Creates a new password hash instance by parsing `hashed_password`.
440    /// Compatible with libsodium's `crypto_pwhash_str*` functions, including
441    /// valid Argon2 strings with non-default salt lengths or parallelism.
442    ///
443    /// # Errors
444    ///
445    /// Returns an error if the string is malformed, uses an unsupported
446    /// algorithm or version, omits a required field, or contains an invalid
447    /// encoded value.
448    pub fn from_string(hashed_password: &str) -> Result<Self, Error> {
449        let parsed_pwhash = crypto_pwhash::Pwhash::parse_encoded_pwhash(hashed_password)?;
450
451        let opslimit = parsed_pwhash.t_cost.ok_or(Error::missing_data(
452            crate::ErrorContext::PasswordHashTimeCost,
453        ))? as u64;
454        let encoded_memlimit = parsed_pwhash.m_cost.ok_or(Error::missing_data(
455            crate::ErrorContext::PasswordHashMemoryCost,
456        ))?;
457        let memlimit =
458            1024usize
459                .checked_mul(encoded_memlimit as usize)
460                .ok_or(Error::InvalidValue {
461                    context: crate::ErrorContext::PasswordHashMemoryCost,
462                    actual: encoded_memlimit as u64,
463                    constraint: crate::ValueConstraint::Between {
464                        min: 0,
465                        max: (usize::MAX / 1024) as u64,
466                    },
467                })?;
468        let hash = parsed_pwhash
469            .pwhash
470            .ok_or(Error::missing_data(crate::ErrorContext::PasswordHash))?;
471        let salt = parsed_pwhash
472            .salt
473            .ok_or(Error::missing_data(crate::ErrorContext::PasswordHashSalt))?;
474        let algorithm = parsed_pwhash.type_.ok_or(Error::missing_data(
475            crate::ErrorContext::PasswordHashAlgorithm,
476        ))?;
477        let parallelism = parsed_pwhash.parallelism.ok_or(Error::missing_data(
478            crate::ErrorContext::PasswordHashParallelism,
479        ))?;
480        let hash_length = hash.len();
481
482        Ok(Self {
483            hash: hash.into(),
484            salt: salt.into(),
485            config: Config {
486                algorithm,
487                hash_length,
488                memlimit,
489                opslimit,
490                parallelism,
491            },
492        })
493    }
494}
495
496impl<Hash: Bytes + Zeroize, Salt: Bytes + Zeroize> PwHash<Hash, Salt> {
497    /// Returns a string-encoded representation of this hash, salt, and config,
498    /// suitable for storage in a database.
499    ///
500    /// The string returned is compatible with libsodium's `crypto_pwhash_str`,
501    /// `crypto_pwhash_str_verify`, and `crypto_pwhash_str_needs_rehash`
502    /// functions when the hash length matches libsodium's string format. The
503    /// lower-level hashing API also supports variable-length hash output.
504    ///
505    /// # Errors
506    ///
507    /// Returns an error if the stored parameters are invalid or the resulting
508    /// string would not fit libsodium's password-hash string format.
509    ///
510    /// ## Example
511    ///
512    /// ```
513    /// use dryoc::pwhash::*;
514    ///
515    /// let password = b"Come what come may, time and the hour runs through the roughest day.";
516    ///
517    /// let pwhash = PwHash::hash_with_defaults(password).expect("unable to hash");
518    /// let pw_string = pwhash.to_encoded_string().expect("unable to encode hash");
519    ///
520    /// let parsed_pwhash =
521    ///     PwHash::from_string_with_defaults(&pw_string).expect("couldn't parse hashed password");
522    ///
523    /// parsed_pwhash.verify(password).expect("verification failed");
524    /// parsed_pwhash
525    ///     .verify(b"invalid password")
526    ///     .expect_err("verification should have failed");
527    /// ```
528    #[cfg(any(feature = "base64", all(doc, not(doctest))))]
529    #[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
530    pub fn to_encoded_string(&self) -> Result<String, Error> {
531        let (t_cost, m_cost) =
532            crypto_pwhash::convert_costs_checked(self.config.opslimit, self.config.memlimit)?;
533        crate::argon2::validate_argon2_pwhash_parameters(
534            self.hash.len(),
535            self.salt.len(),
536            t_cost,
537            m_cost,
538            self.config.parallelism,
539        )?;
540
541        let encoded_len = crypto_pwhash::pwhash_string_len(
542            self.config.algorithm,
543            t_cost,
544            m_cost,
545            self.config.parallelism,
546            self.salt.len(),
547            self.hash.len(),
548        )
549        .ok_or(Error::arithmetic_overflow(
550            crate::ErrorContext::PasswordHash,
551        ))?;
552        if encoded_len >= CRYPTO_PWHASH_STRBYTES {
553            return Err(length_error!(
554                crate::ErrorContext::PasswordHash,
555                encoded_len,
556                max CRYPTO_PWHASH_STRBYTES - 1
557            ));
558        }
559        let encoded = crypto_pwhash::pwhash_to_string(
560            self.config.algorithm,
561            t_cost,
562            m_cost,
563            self.config.parallelism,
564            self.salt.as_slice(),
565            self.hash.as_slice(),
566        );
567        debug_assert_eq!(encoded.len(), encoded_len);
568        Ok(encoded)
569    }
570
571    /// Verifies `password` against this hash using its salt and configuration.
572    ///
573    /// # Errors
574    ///
575    /// Returns an error if the password does not match, if the stored salt or
576    /// configuration is invalid, or if the underlying Argon2 operation fails.
577    pub fn verify<Password: Bytes>(&self, password: &Password) -> Result<(), Error> {
578        let (t_cost, m_cost) =
579            crypto_pwhash::convert_costs_checked(self.config.opslimit, self.config.memlimit)?;
580        crypto_pwhash::verify_pwhash_parts(
581            self.hash.as_slice(),
582            password.as_slice(),
583            self.salt.as_slice(),
584            t_cost,
585            m_cost,
586            self.config.parallelism,
587            self.config.algorithm,
588        )
589    }
590
591    /// Constructs a new instance from `hash`, `salt`, and `config`, consuming
592    /// them.
593    ///
594    /// This function does not validate the parts. Invalid values are reported
595    /// when an operation such as [`PwHash::verify`] or
596    /// [`PwHash::to_encoded_string`] uses them.
597    pub fn from_parts(hash: Hash, salt: Salt, config: Config) -> Self {
598        Self { hash, salt, config }
599    }
600
601    /// Moves the hash, salt, and config out of this instance, returning them as
602    /// a tuple.
603    pub fn into_parts(self) -> (Hash, Salt, Config) {
604        (self.hash, self.salt, self.config)
605    }
606}
607
608impl<Salt: Bytes + Zeroize> PwHash<Hash, Salt> {
609    /// Derives a keypair from `password` and `salt`, using `config`.
610    ///
611    /// The same password and salt derive the same keypair. Store the salt, keep
612    /// it unique per derived key, and do not treat it as secret.
613    ///
614    /// # Errors
615    ///
616    /// Returns an error if a work limit, memory limit, salt length, or password
617    /// length is outside the supported range, or if the underlying Argon2
618    /// operation fails.
619    pub fn derive_keypair<
620        Password: Bytes + Zeroize,
621        PublicKey: NewByteArray<CRYPTO_BOX_PUBLICKEYBYTES> + Zeroize,
622        SecretKey: NewByteArray<CRYPTO_BOX_SECRETKEYBYTES> + Zeroize,
623    >(
624        password: &Password,
625        salt: Salt,
626        config: Config,
627    ) -> Result<keypair::KeyPair<PublicKey, SecretKey>, Error> {
628        validate_direct_config(
629            &config,
630            CRYPTO_BOX_SECRETKEYBYTES,
631            password.len(),
632            salt.len(),
633        )?;
634        let mut secret_key = SecretKey::new_byte_array();
635
636        crypto_pwhash::crypto_pwhash(
637            secret_key.as_mut_slice(),
638            password.as_slice(),
639            salt.as_slice(),
640            config.opslimit,
641            config.memlimit,
642            config.algorithm,
643        )?;
644
645        Ok(keypair::KeyPair::<PublicKey, SecretKey>::from_secret_key(
646            secret_key,
647        ))
648    }
649}
650
651impl PwHash<Hash, Salt> {
652    /// Hashes `password` using default (interactive) config parameters,
653    /// returning the `Vec<u8>`-based hash and salt, with config, upon success.
654    ///
655    /// This function provides reasonable defaults, and is provided for
656    /// convenience.
657    ///
658    /// # Errors
659    ///
660    /// Returns an error if the password length is unsupported or the
661    /// underlying Argon2 operation fails.
662    pub fn hash_with_defaults<Password: Bytes>(password: &Password) -> Result<Self, Error> {
663        Self::hash_interactive(password)
664    }
665
666    #[cfg(any(feature = "base64", all(doc, not(doctest))))]
667    #[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
668    /// Parses the `hashed_password` string, returning a new hash instance upon
669    /// success. Wraps [`PwHash::from_string`], provided for convenience.
670    ///
671    /// # Errors
672    ///
673    /// Returns an error if the string is malformed, uses an unsupported
674    /// algorithm or version, omits a required field, or contains an invalid
675    /// encoded value.
676    pub fn from_string_with_defaults(hashed_password: &str) -> Result<Self, Error> {
677        Self::from_string(hashed_password)
678    }
679}
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684
685    #[test]
686    fn test_pwhash() {
687        let password = b"super secrit password";
688
689        let pwhash = PwHash::hash_with_defaults(password).expect("unable to hash");
690
691        pwhash.verify(password).expect("verification failed");
692        pwhash
693            .verify(b"invalid password")
694            .expect_err("verification should have failed");
695    }
696
697    #[test]
698    fn test_pwhash_uses_random_salt() {
699        let password = b"super secrit password";
700
701        let pwhash1 = PwHash::hash_with_defaults(password).expect("unable to hash");
702        let pwhash2 = PwHash::hash_with_defaults(password).expect("unable to hash");
703
704        assert_ne!(pwhash1.salt.as_slice(), pwhash2.salt.as_slice());
705
706        pwhash1.verify(password).expect("verification failed");
707        pwhash2.verify(password).expect("verification failed");
708    }
709
710    #[test]
711    fn test_pwhash_validates_output_length_before_allocation() {
712        let config = Config::interactive().with_hash_length(usize::MAX);
713        assert!(matches!(
714            VecPwHash::hash(b"password", config),
715            Err(Error::InvalidLength {
716                context: crate::ErrorContext::Output,
717                actual: usize::MAX,
718                ..
719            })
720        ));
721    }
722
723    #[cfg(feature = "serde")]
724    #[test]
725    fn test_pwhash_serde_roundtrip_preserves_verification() {
726        let password = b"serde password";
727        let config = Config::interactive()
728            .with_opslimit(CRYPTO_PWHASH_OPSLIMIT_MIN)
729            .with_memlimit(CRYPTO_PWHASH_MEMLIMIT_MIN);
730        let pwhash = VecPwHash::hash(password, config).expect("unable to hash");
731
732        let json = serde_json::to_string(&pwhash).expect("unable to serialize password hash");
733        let decoded: VecPwHash =
734            serde_json::from_str(&json).expect("unable to deserialize password hash");
735
736        decoded.verify(password).expect("verification failed");
737        decoded
738            .verify(b"wrong password")
739            .expect_err("wrong password should not verify");
740
741        #[cfg(feature = "base64")]
742        decoded
743            .to_encoded_string()
744            .expect("unable to encode deserialized password hash");
745    }
746
747    #[cfg(feature = "base64")]
748    #[test]
749    fn test_pwhash_str() {
750        let password = b"super secrit password";
751
752        let pwhash = PwHash::hash_with_defaults(password).expect("unable to hash");
753        let pw_string = pwhash
754            .to_encoded_string()
755            .expect("couldn't encode password hash");
756
757        let parsed_pwhash =
758            PwHash::from_string_with_defaults(&pw_string).expect("couldn't parse hashed password");
759
760        parsed_pwhash.verify(password).expect("verification failed");
761        parsed_pwhash
762            .verify(b"invalid password")
763            .expect_err("verification should have failed");
764
765        let argon2i = concat!(
766            "$argon2i$v=19$m=4096,t=3,p=2$b2RpZHVlamRpc29kaXNrdw$",
767            "TNnWIwlu1061JHrnCqIAmjs3huSxYIU+0jWipu7Kc9M",
768        );
769        let parsed_argon2i =
770            VecPwHash::from_string(argon2i).expect("valid Argon2i string should parse");
771        parsed_argon2i
772            .verify(b"password")
773            .expect("valid Argon2i string should verify");
774        assert_eq!(
775            parsed_argon2i
776                .to_encoded_string()
777                .expect("couldn't re-encode hash"),
778            argon2i
779        );
780
781        let oversized_encoding = VecPwHash::from_parts(
782            vec![0u8; 64],
783            vec![0u8; CRYPTO_PWHASH_SALTBYTES],
784            Config::interactive().with_hash_length(64),
785        );
786        assert!(oversized_encoding.to_encoded_string().is_err());
787
788        let _argon2i_config = Config::interactive()
789            .with_algorithm(PasswordHashAlgorithm::Argon2i13)
790            .with_opslimit(CRYPTO_PWHASH_ARGON2I_OPSLIMIT_INTERACTIVE)
791            .with_memlimit(CRYPTO_PWHASH_ARGON2I_MEMLIMIT_INTERACTIVE);
792    }
793
794    #[test]
795    #[cfg(all(feature = "protected", any(unix, windows)))]
796    fn test_protected() {
797        use crate::pwhash::protected::*;
798
799        let password =
800            HeapBytes::from_slice_into_locked(b"juicy password").expect("couldn't lock password");
801
802        let pwhash: LockedPwHash =
803            PwHash::hash(&password, Config::interactive()).expect("unable to hash");
804
805        pwhash.verify(&password).expect("verification failed");
806        pwhash
807            .verify(b"invalid password")
808            .expect_err("verification should have failed");
809    }
810}