dryoc/
pwhash.rs

1//! # Password hashing functions
2//!
3//! [`PwHash`] implements libsodium's password hashing functions, based on
4//! Argon2.
5//!
6//! Argon2 provides a configurable memory-hard arbitrary-length hashing function
7//! that is well suited for password hashing. You may tune the function
8//! according to your preferences to either provide stronger collision
9//! resistance, or shorter computation times.
10//!
11//! You should use [`PwHash`] when you want to:
12//!
13//! * authenticate with passwords, and store their salted hashes in a database
14//! * derive secret keys based on passphrases
15//! * hash arbitrary data in a manner that's strongly resistant to collisions
16//!
17//! If the `serde` feature is enabled, the [`serde::Deserialize`] and
18//! [`serde::Serialize`] traits will be implemented for [`PwHash`].
19//!
20//! ## Rustaceous API example
21//!
22//! ```
23//! use dryoc::pwhash::*;
24//!
25//! // A strong passphrase
26//! let password = b"But, for my own part, it was Greek to me.";
27//!
28//! // Hash the password, generating a random salt
29//! let pwhash = PwHash::hash_with_defaults(password).expect("unable to hash");
30//!
31//! pwhash.verify(password).expect("verification failed");
32//! pwhash
33//!     .verify(b"invalid password")
34//!     .expect_err("verification should have failed");
35//! ```
36//!
37//! ## Using a custom config, or your own salt
38//!
39//! ```
40//! use dryoc::pwhash::*;
41//!
42//! // Generate a random salt
43//! let mut salt = Salt::default();
44//! salt.resize(dryoc::constants::CRYPTO_PWHASH_SALTBYTES, 0);
45//! dryoc::rng::copy_randombytes(&mut salt);
46//!
47//! // A strong passphrase
48//! let password = b"What's in a name? That which we call a rose\n
49//!                  By any other word would smell as sweet...";
50//!
51//! // With customized configuration parameters, return type must be explicit
52//! let pwhash: VecPwHash = PwHash::hash_with_salt(
53//!     password,
54//!     salt,
55//!     Config::interactive().with_opslimit(1).with_memlimit(8192),
56//! )
57//! .expect("unable to hash password with salt and custom config");
58//!
59//! pwhash.verify(password).expect("verification failed");
60//! pwhash
61//!     .verify(b"invalid password")
62//!     .expect_err("verification should have failed");
63//! ```
64//!
65//! ## Deriving a keypair from a passphrase and salt
66//!
67//! ```
68//! use dryoc::keypair::StackKeyPair;
69//! use dryoc::pwhash::*;
70//!
71//! // Generate a random salt
72//! let mut salt = Salt::default();
73//! salt.resize(dryoc::constants::CRYPTO_PWHASH_SALTBYTES, 0);
74//! dryoc::rng::copy_randombytes(&mut salt);
75//!
76//! // Use a strong passphrase
77//! let password = b"Is this a dagger which I see before me, the handle toward my hand?";
78//!
79//! let keypair: StackKeyPair = PwHash::derive_keypair(password, salt, Config::interactive())
80//!     .expect("couldn't derive keypair");
81//!
82//! // now you can use `keypair` with DryocBox
83//! ```
84//!
85//! ## String-based encoding
86//!
87//! See [`PwHash::to_string()`] for an example of using the string-based
88//! encoding API, compatible with `crypto_pwhash_str*` functions.
89//!
90//! ## Additional resources
91//!
92//! * See <https://libsodium.gitbook.io/doc/password_hashing> for additional
93//!   details on password hashing
94//! * Refer to the [protected] module for details on usage with protected
95//!   memory.
96
97#[cfg(feature = "serde")]
98use serde::{Deserialize, Serialize};
99use subtle::ConstantTimeEq;
100use zeroize::Zeroize;
101
102use crate::classic::crypto_pwhash;
103use crate::constants::*;
104use crate::error::Error;
105use crate::keypair;
106use crate::rng::copy_randombytes;
107use crate::types::*;
108
109/// Heap-allocated salt type alias for password hashing with [`PwHash`]. Salts
110/// can be of arbitrary length, but they should be at least
111/// [`CRYPTO_PWHASH_SALTBYTES_MIN`] bytes.
112pub type Salt = Vec<u8>;
113/// Heap-allocated hash type alias for password hashing with [`PwHash`]. Hashes
114/// can be of arbitrary length, but they should be at least
115/// [`CRYPTO_PWHASH_BYTES_MIN`] bytes.
116pub type Hash = Vec<u8>;
117
118#[cfg_attr(
119    feature = "serde",
120    derive(Zeroize, Clone, Debug, Serialize, Deserialize)
121)]
122#[cfg_attr(not(feature = "serde"), derive(Zeroize, Clone, Debug))]
123/// Password hash configuration parameters. Provides reasonable default
124/// values with [`Config::default()`], [`Config::interactive()`],
125/// [`Config::moderate()`], and [`Config::sensitive()`].
126pub struct Config {
127    algorithm: crypto_pwhash::PasswordHashAlgorithm,
128    hash_length: usize,
129    memlimit: usize,
130    opslimit: u64,
131    salt_length: usize,
132}
133
134impl Config {
135    /// Returns this config with `salt_length`.
136    #[must_use]
137    pub fn with_salt_length(self, salt_length: usize) -> Self {
138        Self {
139            salt_length,
140            ..self
141        }
142    }
143
144    /// Returns this config with `hash_length`.
145    #[must_use]
146    pub fn with_hash_length(self, hash_length: usize) -> Self {
147        Self {
148            hash_length,
149            ..self
150        }
151    }
152
153    /// Returns this config with `memlimit`.
154    #[must_use]
155    pub fn with_memlimit(self, memlimit: usize) -> Self {
156        Self { memlimit, ..self }
157    }
158
159    /// Returns this config with `opslimit`.
160    #[must_use]
161    pub fn with_opslimit(self, opslimit: u64) -> Self {
162        Self { opslimit, ..self }
163    }
164
165    /// Provides a password hash configuration for interactive hashing.
166    pub fn interactive() -> Self {
167        Self {
168            algorithm: crypto_pwhash::PasswordHashAlgorithm::Argon2id13,
169            opslimit: CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
170            memlimit: CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
171            salt_length: CRYPTO_PWHASH_SALTBYTES,
172            hash_length: crypto_pwhash::STR_HASHBYTES,
173        }
174    }
175
176    /// Provides a password hash configuration for moderate hashing.
177    pub fn moderate() -> Self {
178        Self {
179            algorithm: crypto_pwhash::PasswordHashAlgorithm::Argon2id13,
180            opslimit: CRYPTO_PWHASH_OPSLIMIT_MODERATE,
181            memlimit: CRYPTO_PWHASH_MEMLIMIT_MODERATE,
182            salt_length: CRYPTO_PWHASH_SALTBYTES,
183            hash_length: crypto_pwhash::STR_HASHBYTES,
184        }
185    }
186
187    /// Provides a password hash configuration for sensitive hashing.
188    pub fn sensitive() -> Self {
189        Self {
190            algorithm: crypto_pwhash::PasswordHashAlgorithm::Argon2id13,
191            opslimit: CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
192            memlimit: CRYPTO_PWHASH_MEMLIMIT_SENSITIVE,
193            salt_length: CRYPTO_PWHASH_SALTBYTES,
194            hash_length: crypto_pwhash::STR_HASHBYTES,
195        }
196    }
197}
198
199impl Default for Config {
200    fn default() -> Self {
201        Self::interactive()
202    }
203}
204
205#[cfg_attr(
206    feature = "serde",
207    derive(Zeroize, Clone, Debug, Serialize, Deserialize)
208)]
209#[cfg_attr(not(feature = "serde"), derive(Zeroize, Clone, Debug))]
210/// Password hash implementation based on Argon2, compatible with libsodium's
211/// `crypto_pwhash_*` functions.
212pub struct PwHash<Hash: Bytes + Zeroize, Salt: Bytes + Zeroize> {
213    hash: Hash,
214    salt: Salt,
215    config: Config,
216}
217
218/// `Vec<u8>`-based PwHash type alias, provided for convenience.
219pub type VecPwHash = PwHash<Hash, Salt>;
220
221#[cfg(any(feature = "nightly", all(doc, not(doctest))))]
222#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "nightly")))]
223pub mod protected {
224    //! #  Protected memory type aliases for [`PwHash`]
225    //!
226    //! This mod provides re-exports of type aliases for protected memory usage
227    //! with [`PwHash`]. These type aliases are provided for
228    //! convenience.
229    //!
230    //! ## Example
231    //!
232    //! ```
233    //! use dryoc::pwhash::protected::*;
234    //! use dryoc::pwhash::{Config, PwHash};
235    //!
236    //! let password = HeapBytes::from_slice_into_locked(
237    //!     b"The robb'd that smiles, steals something from the thief.",
238    //! )
239    //! .expect("couldn't lock password");
240    //!
241    //! let pwhash: LockedPwHash =
242    //!     PwHash::hash(&password, Config::interactive()).expect("unable to hash");
243    //!
244    //! pwhash.verify(&password).expect("verification failed");
245    //! pwhash
246    //!     .verify(b"invalid password")
247    //!     .expect_err("verification should have failed");
248    //! ```
249    use super::*;
250    pub use crate::protected::*;
251
252    /// Heap-allocated, page-aligned salt type alias for protected password
253    /// hashing with [`PwHash`].
254    pub type Salt = HeapBytes;
255    /// Heap-allocated, page-aligned hash type alias for protected password
256    /// hashing with [`PwHash`].
257    pub type Hash = HeapBytes;
258
259    /// Locked [`PwHash`], provided as a type alias for convenience.
260    pub type LockedPwHash = PwHash<Locked<Hash>, Locked<Salt>>;
261}
262
263impl<Hash: NewBytes + ResizableBytes + Zeroize, Salt: NewBytes + ResizableBytes + Zeroize>
264    PwHash<Hash, Salt>
265{
266    /// Hashes `password` with a random salt and `config`, returning
267    /// the hash, salt, and config upon success.
268    pub fn hash<Password: Bytes>(password: &Password, config: Config) -> Result<Self, Error> {
269        let mut hash = Hash::new_bytes();
270        let mut salt = Salt::new_bytes();
271
272        hash.resize(config.hash_length, 0);
273
274        salt.resize(config.salt_length, 0);
275        copy_randombytes(salt.as_mut_slice());
276
277        crypto_pwhash::crypto_pwhash(
278            hash.as_mut_slice(),
279            password.as_slice(),
280            salt.as_slice(),
281            config.opslimit,
282            config.memlimit,
283            config.algorithm.clone(),
284        )?;
285
286        Ok(Self { hash, salt, config })
287    }
288
289    /// Hashes `password` with a random salt and a default configuration
290    /// suitable for interactive hashing, returning the hash, salt, and config
291    /// upon success.
292    pub fn hash_interactive<Password: Bytes>(password: &Password) -> Result<Self, Error> {
293        Self::hash(password, Config::interactive())
294    }
295
296    /// Hashes `password` with a random salt and a default configuration
297    /// suitable for moderate hashing, returning the hash, salt, and config upon
298    /// success.
299    pub fn hash_moderate<Password: Bytes>(password: &Password) -> Result<Self, Error> {
300        Self::hash(password, Config::moderate())
301    }
302
303    /// Hashes `password` with a random salt and a default configuration
304    /// suitable for sensitive hashing, returning the hash, salt, and config
305    /// upon success.
306    pub fn hash_sensitive<Password: Bytes>(password: &Password) -> Result<Self, Error> {
307        Self::hash(password, Config::sensitive())
308    }
309
310    /// Returns a string-encoded representation of this hash, salt, and config,
311    /// suitable for storage in a database.
312    ///
313    /// It's recommended that you use the Serde support instead of this
314    /// function, however this function is provided for compatiblity reasons.
315    ///
316    /// The string returned is compatible with libsodium's `crypto_pwhash_str`,
317    /// `crypto_pwhash_str_verify`, and `crypto_pwhash_str_needs_rehash`
318    /// functions, but _only_ when the hash and salt length values match those
319    /// supported by libsodium. This implementation supports variable-length
320    /// salts and hashes, but libsodium's does not.
321    ///
322    /// ## Example
323    ///
324    /// ```
325    /// use dryoc::pwhash::*;
326    ///
327    /// let password = b"Come what come may, time and the hour runs through the roughest day.";
328    ///
329    /// let pwhash = PwHash::hash_with_defaults(password).expect("unable to hash");
330    /// let pw_string = pwhash.to_string();
331    ///
332    /// let parsed_pwhash =
333    ///     PwHash::from_string_with_defaults(&pw_string).expect("couldn't parse hashed password");
334    ///
335    /// parsed_pwhash.verify(password).expect("verification failed");
336    /// parsed_pwhash
337    ///     .verify(b"invalid password")
338    ///     .expect_err("verification should have failed");
339    /// ```
340    #[cfg(any(feature = "base64", all(doc, not(doctest))))]
341    #[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
342    #[allow(clippy::inherent_to_string)]
343    pub fn to_string(&self) -> String {
344        let (t_cost, m_cost) =
345            crypto_pwhash::convert_costs(self.config.opslimit, self.config.memlimit);
346
347        crypto_pwhash::pwhash_to_string(t_cost, m_cost, self.salt.as_slice(), self.hash.as_slice())
348    }
349}
350
351impl<Hash: NewBytes + ResizableBytes + Zeroize, Salt: Bytes + Clone + Zeroize> PwHash<Hash, Salt> {
352    /// Verifies that this hash, salt, and config is valid for `password`.
353    pub fn verify<Password: Bytes>(&self, password: &Password) -> Result<(), Error> {
354        let computed = Self::hash_with_salt(password, self.salt.clone(), self.config.clone())?;
355
356        if self
357            .hash
358            .as_slice()
359            .ct_eq(computed.hash.as_slice())
360            .unwrap_u8()
361            == 1
362        {
363            Ok(())
364        } else {
365            Err(dryoc_error!("hashes do not match"))
366        }
367    }
368
369    /// Hashes `password` with `salt` and `config`, returning
370    /// the hash, salt, and config upon success.
371    pub fn hash_with_salt<Password: Bytes>(
372        password: &Password,
373        salt: Salt,
374        config: Config,
375    ) -> Result<Self, Error> {
376        let mut hash = Hash::new_bytes();
377
378        hash.resize(config.hash_length, 0);
379
380        crypto_pwhash::crypto_pwhash(
381            hash.as_mut_slice(),
382            password.as_slice(),
383            salt.as_slice(),
384            config.opslimit,
385            config.memlimit,
386            config.algorithm.clone(),
387        )?;
388
389        Ok(Self { hash, salt, config })
390    }
391}
392
393#[cfg(any(feature = "base64", all(doc, not(doctest))))]
394#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
395impl<Hash: Bytes + From<Vec<u8>> + Zeroize, Salt: Bytes + From<Vec<u8>> + Zeroize>
396    PwHash<Hash, Salt>
397{
398    /// Creates a new password hash instance by parsing `hashed_password`.
399    /// Compatible with libsodium's `crypto_pwhash_str*` functions, and supports
400    /// variable-length encoding for the hash and salt.
401    ///
402    /// It's recommended that you use the Serde support instead of this
403    /// function, however this function is provided for compatiblity reasons.
404    pub fn from_string(hashed_password: &str) -> Result<Self, Error> {
405        let parsed_pwhash = crypto_pwhash::Pwhash::parse_encoded_pwhash(hashed_password)?;
406
407        let opslimit = parsed_pwhash.t_cost.unwrap() as u64;
408        let memlimit = 1024 * (parsed_pwhash.m_cost.unwrap() as usize);
409        let hash_length = parsed_pwhash.pwhash.as_ref().unwrap().len();
410        let salt_length = parsed_pwhash.salt.as_ref().unwrap().len();
411        let algorithm = parsed_pwhash.type_.unwrap();
412
413        Ok(Self {
414            hash: parsed_pwhash.pwhash.unwrap().into(),
415            salt: parsed_pwhash.salt.unwrap().into(),
416            config: Config {
417                algorithm,
418                hash_length,
419                memlimit,
420                opslimit,
421                salt_length,
422            },
423        })
424    }
425}
426
427impl<Hash: Bytes + Zeroize, Salt: Bytes + Zeroize> PwHash<Hash, Salt> {
428    /// Constructs a new instance from `hash`, `salt`, and `config`, consuming
429    /// them.
430    pub fn from_parts(hash: Hash, salt: Salt, config: Config) -> Self {
431        Self { hash, salt, config }
432    }
433
434    /// Moves the hash, salt, and config out of this instance, returning them as
435    /// a tuple.
436    pub fn into_parts(self) -> (Hash, Salt, Config) {
437        (self.hash, self.salt, self.config)
438    }
439}
440
441impl<Salt: Bytes + Zeroize> PwHash<Hash, Salt> {
442    /// Derives a keypair from `password` and `salt`, using `config`.
443    pub fn derive_keypair<
444        Password: Bytes + Zeroize,
445        PublicKey: NewByteArray<CRYPTO_BOX_PUBLICKEYBYTES> + Zeroize,
446        SecretKey: NewByteArray<CRYPTO_BOX_SECRETKEYBYTES> + Zeroize,
447    >(
448        password: &Password,
449        salt: Salt,
450        config: Config,
451    ) -> Result<keypair::KeyPair<PublicKey, SecretKey>, Error> {
452        let mut secret_key = SecretKey::new_byte_array();
453
454        crypto_pwhash::crypto_pwhash(
455            secret_key.as_mut_slice(),
456            password.as_slice(),
457            salt.as_slice(),
458            config.opslimit,
459            config.memlimit,
460            config.algorithm,
461        )?;
462
463        Ok(keypair::KeyPair::<PublicKey, SecretKey>::from_secret_key(
464            secret_key,
465        ))
466    }
467}
468
469impl PwHash<Hash, Salt> {
470    /// Hashes `password` using default (interactive) config parameters,
471    /// returning the `Vec<u8>`-based hash and salt, with config, upon success.
472    ///
473    /// This function provides reasonable defaults, and is provided for
474    /// convenience.
475    pub fn hash_with_defaults<Password: Bytes>(password: &Password) -> Result<Self, Error> {
476        Self::hash_interactive(password)
477    }
478
479    #[cfg(any(feature = "base64", all(doc, not(doctest))))]
480    #[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
481    /// Parses the `hashed_password` string, returning a new hash instance upon
482    /// success. Wraps [`PwHash::from_string`], provided for convenience.
483    pub fn from_string_with_defaults(hashed_password: &str) -> Result<Self, Error> {
484        Self::from_string(hashed_password)
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    #[test]
493    fn test_pwhash() {
494        let password = b"super secrit password";
495
496        let pwhash = PwHash::hash_with_defaults(password).expect("unable to hash");
497
498        pwhash.verify(password).expect("verification failed");
499        pwhash
500            .verify(b"invalid password")
501            .expect_err("verification should have failed");
502    }
503
504    #[cfg(feature = "base64")]
505    #[test]
506    fn test_pwhash_str() {
507        let password = b"super secrit password";
508
509        let pwhash = PwHash::hash_with_defaults(password).expect("unable to hash");
510        let pw_string = pwhash.to_string();
511
512        let parsed_pwhash =
513            PwHash::from_string_with_defaults(&pw_string).expect("couldn't parse hashed password");
514
515        parsed_pwhash.verify(password).expect("verification failed");
516        parsed_pwhash
517            .verify(b"invalid password")
518            .expect_err("verification should have failed");
519    }
520
521    #[test]
522    #[cfg(feature = "nightly")]
523    fn test_protected() {
524        use crate::pwhash::protected::*;
525
526        let password =
527            HeapBytes::from_slice_into_locked(b"juicy password").expect("couldn't lock password");
528
529        let pwhash: LockedPwHash =
530            PwHash::hash(&password, Config::interactive()).expect("unable to hash");
531
532        pwhash.verify(&password).expect("verification failed");
533        pwhash
534            .verify(b"invalid password")
535            .expect_err("verification should have failed");
536    }
537}