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}