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}