Skip to main content

dryoc/classic/
crypto_pwhash.rs

1//! # Password hashing
2//!
3//! Implements libsodium's `crypto_pwhash_*` functions. This implementation
4//! currently only supports Argon2i and Argon2id algorithms, and does not
5//! support scrypt.
6//!
7//! String-based functions are enabled by default. They can be disabled by
8//! building without default features, and re-enabled with the `base64` feature.
9//!
10//! For details, refer to [libsodium docs](https://libsodium.gitbook.io/doc/password_hashing/default_phf).
11//!
12//! ## Classic API example, key derivation
13//!
14//! ```
15//! use base64::{Engine as _, engine::general_purpose};
16//! use dryoc::classic::crypto_pwhash::*;
17//! use dryoc::rng::copy_randombytes;
18//! use dryoc::constants::{CRYPTO_SECRETBOX_KEYBYTES, CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
19//!     CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, CRYPTO_PWHASH_SALTBYTES};
20//!
21//! let mut key = [0u8; CRYPTO_SECRETBOX_KEYBYTES];
22//!
23//! // Randomly generate a salt
24//! let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
25//! copy_randombytes(&mut salt);
26//!
27//! // Create a really good password
28//! let password = b"It is by riding a bicycle that you learn the contours of a country best, since you have to sweat up the hills and coast down them.";
29//!
30//! crypto_pwhash(
31//!     &mut key,
32//!     password,
33//!     &salt,
34//!     CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
35//!     CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
36//!     PasswordHashAlgorithm::Argon2id13,
37//! )
38//! .expect("pwhash failed");
39//!
40//! // now `key` can be used as a secret key
41//! println!("key = {}", general_purpose::STANDARD_NO_PAD.encode(&key));
42//! ```
43
44#[cfg(feature = "serde")]
45use serde::{Deserialize, Serialize};
46use subtle::ConstantTimeEq;
47use zeroize::{Zeroize, Zeroizing};
48
49#[cfg(feature = "base64")]
50use crate::argon2::ARGON2_VERSION_NUMBER;
51use crate::argon2::{self, argon2_hash};
52use crate::constants::*;
53use crate::error::Error;
54
55pub(crate) const STR_HASHBYTES: usize = 32;
56
57#[cfg_attr(
58    feature = "serde",
59    derive(Zeroize, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)
60)]
61#[cfg_attr(
62    not(feature = "serde"),
63    derive(Zeroize, Clone, Copy, Debug, Eq, PartialEq)
64)]
65/// Password hash algorithm implementations.
66pub enum PasswordHashAlgorithm {
67    /// Argon2i version 0x13 (v19)
68    Argon2i13  = 1,
69    /// Argon2id version 0x13 (v19)
70    Argon2id13 = 2,
71}
72
73impl TryFrom<u32> for PasswordHashAlgorithm {
74    type Error = Error;
75
76    fn try_from(num: u32) -> Result<Self, Self::Error> {
77        match num {
78            num if num == PasswordHashAlgorithm::Argon2i13 as u32 => {
79                Ok(PasswordHashAlgorithm::Argon2i13)
80            }
81            num if num == PasswordHashAlgorithm::Argon2id13 as u32 => {
82                Ok(PasswordHashAlgorithm::Argon2id13)
83            }
84            _ => Err(Error::InvalidValue {
85                context: crate::ErrorContext::PasswordHashAlgorithm,
86                actual: num as u64,
87                constraint: crate::ValueConstraint::Between {
88                    min: PasswordHashAlgorithm::Argon2i13 as u64,
89                    max: PasswordHashAlgorithm::Argon2id13 as u64,
90                },
91            }),
92        }
93    }
94}
95
96impl From<PasswordHashAlgorithm> for argon2::Argon2Type {
97    fn from(algo: PasswordHashAlgorithm) -> Self {
98        match algo {
99            PasswordHashAlgorithm::Argon2i13 => argon2::Argon2Type::Argon2i,
100            PasswordHashAlgorithm::Argon2id13 => argon2::Argon2Type::Argon2id,
101        }
102    }
103}
104
105/// Hashes `password` with `salt`, placing the resulting hash into `output`.
106///
107/// * `opslimit` specifies the number of iterations to use in the underlying
108///   algorithm
109/// * `memlimit` specifies the maximum amount of memory to use, in bytes
110///
111/// Generally speaking, you want to set `opslimit` and `memlimit` sufficiently
112/// large such that it's hard for someone to brute-force a password.
113///
114/// For your convenience, the following constants are defined which can be used
115/// with `opslimit` and `memlimit`:
116/// * [`CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE`] and
117///   [`CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE`] for interactive operations
118/// * [`CRYPTO_PWHASH_OPSLIMIT_MODERATE`] and
119///   [`CRYPTO_PWHASH_MEMLIMIT_MODERATE`] for typical operations, such as
120///   server-side password hashing
121/// * [`CRYPTO_PWHASH_OPSLIMIT_SENSITIVE`] and
122///   [`CRYPTO_PWHASH_MEMLIMIT_SENSITIVE`] for sensitive operations
123///
124/// Compatible with libsodium's `crypto_pwhash`.
125///
126/// # Errors
127///
128/// Returns an error if the cost parameters, salt length, or output length are
129/// invalid, or if Argon2 cannot hash the password with the requested settings.
130pub fn crypto_pwhash(
131    output: &mut [u8],
132    password: &[u8],
133    salt: &[u8],
134    opslimit: u64,
135    memlimit: usize,
136    algorithm: PasswordHashAlgorithm,
137) -> Result<(), Error> {
138    validate_pwhash_parameters(
139        output.len(),
140        password.len(),
141        salt.len(),
142        opslimit,
143        memlimit,
144        algorithm,
145    )?;
146
147    let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
148
149    argon2_hash(
150        t_cost,
151        m_cost,
152        1,
153        password,
154        salt,
155        None,
156        None,
157        output,
158        algorithm.into(),
159    )
160}
161
162pub(crate) fn validate_pwhash_parameters(
163    output_len: usize,
164    password_len: usize,
165    salt_len: usize,
166    opslimit: u64,
167    memlimit: usize,
168    algorithm: PasswordHashAlgorithm,
169) -> Result<(), Error> {
170    let (
171        bytes_min,
172        bytes_max,
173        password_max,
174        opslimit_min,
175        opslimit_max,
176        memlimit_min,
177        memlimit_max,
178    ) = match algorithm {
179        PasswordHashAlgorithm::Argon2i13 => (
180            CRYPTO_PWHASH_ARGON2I_BYTES_MIN,
181            CRYPTO_PWHASH_ARGON2I_BYTES_MAX,
182            CRYPTO_PWHASH_ARGON2I_PASSWD_MAX,
183            CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN,
184            CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MAX,
185            CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MIN,
186            CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MAX,
187        ),
188        PasswordHashAlgorithm::Argon2id13 => (
189            CRYPTO_PWHASH_ARGON2ID_BYTES_MIN,
190            CRYPTO_PWHASH_ARGON2ID_BYTES_MAX,
191            CRYPTO_PWHASH_ARGON2ID_PASSWD_MAX,
192            CRYPTO_PWHASH_ARGON2ID_OPSLIMIT_MIN,
193            CRYPTO_PWHASH_ARGON2ID_OPSLIMIT_MAX,
194            CRYPTO_PWHASH_ARGON2ID_MEMLIMIT_MIN,
195            CRYPTO_PWHASH_ARGON2ID_MEMLIMIT_MAX,
196        ),
197    };
198
199    validate_length!(
200        bytes_min,
201        bytes_max,
202        output_len,
203        crate::ErrorContext::Output
204    );
205    validate_length!(
206        CRYPTO_PWHASH_PASSWD_MIN,
207        password_max,
208        password_len,
209        crate::ErrorContext::Password
210    );
211    validate_length!(
212        exact CRYPTO_PWHASH_SALTBYTES,
213        salt_len,
214        crate::ErrorContext::PasswordHashSalt
215    );
216    validate_value!(
217        opslimit_min,
218        opslimit_max,
219        opslimit,
220        crate::ErrorContext::OperationsLimit
221    );
222    validate_value!(
223        memlimit_min,
224        memlimit_max,
225        memlimit,
226        crate::ErrorContext::MemoryLimit
227    );
228
229    Ok(())
230}
231
232#[cfg(any(feature = "base64", all(doc, not(doctest))))]
233#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
234pub(crate) fn pwhash_to_string(
235    algorithm: PasswordHashAlgorithm,
236    t_cost: u32,
237    m_cost: u32,
238    parallelism: u32,
239    salt: &[u8],
240    hash: &[u8],
241) -> String {
242    let algorithm_name = pwhash_algorithm_name(algorithm);
243    format!(
244        "${algorithm_name}$v={}$m={},t={},p={parallelism}${}${}",
245        argon2::ARGON2_VERSION_NUMBER,
246        m_cost,
247        t_cost,
248        base64_no_pad_encode(salt),
249        base64_no_pad_encode(hash),
250    )
251}
252
253#[cfg(any(feature = "base64", all(doc, not(doctest))))]
254pub(crate) fn pwhash_string_len(
255    algorithm: PasswordHashAlgorithm,
256    t_cost: u32,
257    m_cost: u32,
258    parallelism: u32,
259    salt_len: usize,
260    hash_len: usize,
261) -> Option<usize> {
262    let salt_len = base64_no_pad_encoded_len(salt_len)?;
263    let hash_len = base64_no_pad_encoded_len(hash_len)?;
264    1usize
265        .checked_add(pwhash_algorithm_name(algorithm).len())?
266        .checked_add(3 + decimal_len(ARGON2_VERSION_NUMBER))?
267        .checked_add(3 + decimal_len(m_cost))?
268        .checked_add(3 + decimal_len(t_cost))?
269        .checked_add(3 + decimal_len(parallelism))?
270        .checked_add(1)?
271        .checked_add(salt_len)?
272        .checked_add(1)?
273        .checked_add(hash_len)
274}
275
276#[cfg(any(feature = "base64", all(doc, not(doctest))))]
277const fn pwhash_algorithm_name(algorithm: PasswordHashAlgorithm) -> &'static str {
278    match algorithm {
279        PasswordHashAlgorithm::Argon2i13 => "argon2i",
280        PasswordHashAlgorithm::Argon2id13 => "argon2id",
281    }
282}
283
284#[cfg(any(feature = "base64", all(doc, not(doctest))))]
285const fn decimal_len(value: u32) -> usize {
286    if value == 0 {
287        1
288    } else {
289        value.ilog10() as usize + 1
290    }
291}
292
293#[cfg(any(feature = "base64", all(doc, not(doctest))))]
294const fn base64_no_pad_encoded_len(input_len: usize) -> Option<usize> {
295    let remainder_len = match input_len % 3 {
296        0 => 0,
297        1 => 2,
298        _ => 3,
299    };
300    match (input_len / 3).checked_mul(4) {
301        Some(full_len) => full_len.checked_add(remainder_len),
302        None => None,
303    }
304}
305
306#[cfg(any(feature = "base64", all(doc, not(doctest))))]
307fn base64_no_pad_encode(input: &[u8]) -> String {
308    const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
309
310    let mut output = String::with_capacity(input.len().div_ceil(3) * 4);
311
312    let (chunks, rem) = input.as_chunks::<3>();
313
314    for chunk in chunks {
315        let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
316        output.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
317        output.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
318        output.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
319        output.push(ALPHABET[(n & 0x3f) as usize] as char);
320    }
321
322    if rem.len() == 1 {
323        let n = (rem[0] as u32) << 16;
324        output.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
325        output.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
326    } else if rem.len() == 2 {
327        let n = ((rem[0] as u32) << 16) | ((rem[1] as u32) << 8);
328        output.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
329        output.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
330        output.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
331    }
332
333    output
334}
335
336#[cfg(feature = "base64")]
337fn base64_no_pad_decode(input: &str) -> Option<Vec<u8>> {
338    fn decode_byte(byte: u8) -> Option<u8> {
339        match byte {
340            b'A'..=b'Z' => Some(byte - b'A'),
341            b'a'..=b'z' => Some(byte - b'a' + 26),
342            b'0'..=b'9' => Some(byte - b'0' + 52),
343            b'+' => Some(62),
344            b'/' => Some(63),
345            _ => None,
346        }
347    }
348
349    let input = input.as_bytes();
350    if input.len() % 4 == 1 || input.contains(&b'=') {
351        return None;
352    }
353
354    let mut output = Vec::with_capacity(input.len() / 4 * 3 + 2);
355    let (chunks, rem) = input.as_chunks::<4>();
356
357    for chunk in chunks {
358        let n = ((decode_byte(chunk[0])? as u32) << 18)
359            | ((decode_byte(chunk[1])? as u32) << 12)
360            | ((decode_byte(chunk[2])? as u32) << 6)
361            | decode_byte(chunk[3])? as u32;
362        output.push((n >> 16) as u8);
363        output.push((n >> 8) as u8);
364        output.push(n as u8);
365    }
366
367    if rem.len() == 2 {
368        let second = decode_byte(rem[1])?;
369        if second & 0x0f != 0 {
370            return None;
371        }
372        let n = ((decode_byte(rem[0])? as u32) << 18) | ((second as u32) << 12);
373        output.push((n >> 16) as u8);
374    } else if rem.len() == 3 {
375        let third = decode_byte(rem[2])?;
376        if third & 0x03 != 0 {
377            return None;
378        }
379        let n = ((decode_byte(rem[0])? as u32) << 18)
380            | ((decode_byte(rem[1])? as u32) << 12)
381            | ((third as u32) << 6);
382        output.push((n >> 16) as u8);
383        output.push((n >> 8) as u8);
384    }
385
386    Some(output)
387}
388
389pub(crate) fn convert_costs(opslimit: u64, memlimit: usize) -> (u32, u32) {
390    (opslimit as u32, (memlimit / 1024) as u32)
391}
392
393pub(crate) fn convert_costs_checked(opslimit: u64, memlimit: usize) -> Result<(u32, u32), Error> {
394    let t_cost = u32::try_from(opslimit).map_err(|_| Error::InvalidValue {
395        context: crate::ErrorContext::OperationsLimit,
396        actual: opslimit,
397        constraint: crate::ValueConstraint::Between {
398            min: 0,
399            max: u32::MAX as u64,
400        },
401    })?;
402    let m_cost = u32::try_from(memlimit / 1024).map_err(|_| Error::InvalidValue {
403        context: crate::ErrorContext::MemoryLimit,
404        actual: memlimit as u64,
405        constraint: crate::ValueConstraint::Between {
406            min: 0,
407            max: (u32::MAX as u64) * 1024 + 1023,
408        },
409    })?;
410    Ok((t_cost, m_cost))
411}
412
413/// Hash a password string with a random salt.
414///
415/// This function provides a wrapper for [`crypto_pwhash`] that returns a string
416/// encoding of a hashed password with a random salt, suitable for use with
417/// password hash storage (i.e., in a database). Can be used to verify a
418/// password using [`crypto_pwhash_str_verify`].
419///
420/// Compatible with libsodium's `crypto_pwhash_str`.
421///
422/// # Errors
423///
424/// Returns an error if the password or resource limits are unsupported, or if
425/// Argon2 cannot hash the password.
426///
427/// # Panics
428///
429/// Panics if the operating system's random number generator fails.
430#[cfg(any(feature = "base64", all(doc, not(doctest))))]
431#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
432pub fn crypto_pwhash_str(password: &[u8], opslimit: u64, memlimit: usize) -> Result<String, Error> {
433    crypto_pwhash_str_alg(
434        password,
435        opslimit,
436        memlimit,
437        PasswordHashAlgorithm::Argon2id13,
438    )
439}
440
441/// Hashes a password with a random salt and the selected algorithm, returning
442/// a database-safe encoded string.
443///
444/// Compatible with libsodium's `crypto_pwhash_str_alg`.
445///
446/// # Errors
447///
448/// Returns an error if the password or resource limits are unsupported, or if
449/// Argon2 cannot hash the password.
450///
451/// # Panics
452///
453/// Panics if the operating system's random number generator fails.
454#[cfg(any(feature = "base64", all(doc, not(doctest))))]
455#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
456pub fn crypto_pwhash_str_alg(
457    password: &[u8],
458    opslimit: u64,
459    memlimit: usize,
460    algorithm: PasswordHashAlgorithm,
461) -> Result<String, Error> {
462    validate_pwhash_parameters(
463        STR_HASHBYTES,
464        password.len(),
465        CRYPTO_PWHASH_SALTBYTES,
466        opslimit,
467        memlimit,
468        algorithm,
469    )?;
470
471    let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
472    let mut hash = [0u8; STR_HASHBYTES];
473    crate::rng::copy_randombytes(&mut salt);
474
475    let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
476
477    crypto_pwhash(&mut hash, password, &salt, opslimit, memlimit, algorithm)?;
478
479    let pw = pwhash_to_string(algorithm, t_cost, m_cost, 1, &salt, &hash);
480
481    Ok(pw)
482}
483
484#[cfg(feature = "base64")]
485#[derive(Default)]
486pub(crate) struct Pwhash {
487    pub(crate) pwhash: Option<Vec<u8>>,
488    pub(crate) salt: Option<Vec<u8>>,
489    pub(crate) type_: Option<PasswordHashAlgorithm>,
490    pub(crate) t_cost: Option<u32>,
491    pub(crate) m_cost: Option<u32>,
492    pub(crate) parallelism: Option<u32>,
493}
494
495#[cfg(feature = "base64")]
496impl Pwhash {
497    pub(crate) fn parse_encoded_pwhash(hashed_password: &str) -> Result<Self, Error> {
498        if hashed_password.len() >= CRYPTO_PWHASH_STRBYTES {
499            return Err(length_error!(
500                crate::ErrorContext::PasswordHash,
501                hashed_password.len(),
502                max CRYPTO_PWHASH_STRBYTES - 1
503            ));
504        }
505
506        let encoded = hashed_password
507            .strip_prefix('$')
508            .ok_or_else(|| Error::invalid_encoding(crate::ErrorContext::PasswordHash))?;
509        let mut fields = encoded.split('$');
510
511        let algorithm = match fields.next().filter(|field| !field.is_empty()) {
512            Some("argon2i") => PasswordHashAlgorithm::Argon2i13,
513            Some("argon2id") => PasswordHashAlgorithm::Argon2id13,
514            Some(field) if field.starts_with("v=") => {
515                return Err(Error::missing_data(
516                    crate::ErrorContext::PasswordHashAlgorithm,
517                ));
518            }
519            Some(_) => {
520                return Err(Error::invalid_encoding(
521                    crate::ErrorContext::PasswordHashAlgorithm,
522                ));
523            }
524            None => {
525                return Err(Error::missing_data(
526                    crate::ErrorContext::PasswordHashAlgorithm,
527                ));
528            }
529        };
530
531        let version = fields
532            .next()
533            .ok_or(Error::missing_data(
534                crate::ErrorContext::PasswordHashVersion,
535            ))?
536            .strip_prefix("v=")
537            .ok_or(Error::invalid_encoding(
538                crate::ErrorContext::PasswordHashVersion,
539            ))?;
540        let version =
541            parse_minimal_pwhash_decimal(version, crate::ErrorContext::PasswordHashVersion)?;
542        if version != ARGON2_VERSION_NUMBER {
543            return Err(Error::invalid_encoding(
544                crate::ErrorContext::PasswordHashVersion,
545            ));
546        }
547
548        let parameters = fields.next().ok_or(Error::missing_data(
549            crate::ErrorContext::PasswordHashMemoryCost,
550        ))?;
551        let mut parameters = parameters.split(',');
552        let m_cost = parse_pwhash_parameter(
553            parameters.next(),
554            "m=",
555            crate::ErrorContext::PasswordHashMemoryCost,
556        )?;
557        let t_cost = parse_pwhash_parameter(
558            parameters.next(),
559            "t=",
560            crate::ErrorContext::PasswordHashTimeCost,
561        )?;
562        let parallelism = parse_pwhash_parameter(
563            parameters.next(),
564            "p=",
565            crate::ErrorContext::PasswordHashParallelism,
566        )?;
567        if parameters.next().is_some() {
568            return Err(Error::invalid_encoding(crate::ErrorContext::PasswordHash));
569        }
570
571        let salt = fields
572            .next()
573            .filter(|field| !field.is_empty())
574            .ok_or(Error::missing_data(crate::ErrorContext::PasswordHashSalt))?;
575        let salt = base64_no_pad_decode(salt).ok_or(Error::invalid_encoding(
576            crate::ErrorContext::PasswordHashSalt,
577        ))?;
578
579        let pwhash = fields
580            .next()
581            .filter(|field| !field.is_empty())
582            .ok_or(Error::missing_data(crate::ErrorContext::PasswordHash))?;
583        let pwhash = base64_no_pad_decode(pwhash)
584            .ok_or(Error::invalid_encoding(crate::ErrorContext::PasswordHash))?;
585
586        if fields.next().is_some() {
587            return Err(Error::invalid_encoding(crate::ErrorContext::PasswordHash));
588        }
589
590        crate::argon2::validate_argon2_pwhash_parameters(
591            pwhash.len(),
592            salt.len(),
593            t_cost,
594            m_cost,
595            parallelism,
596        )?;
597
598        Ok(Self {
599            pwhash: Some(pwhash),
600            salt: Some(salt),
601            type_: Some(algorithm),
602            t_cost: Some(t_cost),
603            m_cost: Some(m_cost),
604            parallelism: Some(parallelism),
605        })
606    }
607}
608
609#[cfg(feature = "base64")]
610fn parse_pwhash_parameter(
611    parameter: Option<&str>,
612    prefix: &str,
613    context: crate::ErrorContext,
614) -> Result<u32, Error> {
615    let value = parameter
616        .ok_or(Error::missing_data(context))?
617        .strip_prefix(prefix)
618        .ok_or(Error::invalid_encoding(context))?;
619    parse_minimal_pwhash_decimal(value, context)
620}
621
622#[cfg(feature = "base64")]
623fn parse_minimal_pwhash_decimal(value: &str, context: crate::ErrorContext) -> Result<u32, Error> {
624    if value.is_empty()
625        || !value.bytes().all(|byte| byte.is_ascii_digit())
626        || (value.len() > 1 && value.starts_with('0'))
627    {
628        return Err(Error::invalid_encoding(context));
629    }
630    value
631        .parse::<u32>()
632        .map_err(|_| Error::invalid_encoding(context))
633}
634
635/// Verifies that `hashed_password` is valid for `password`, assuming the hashed
636/// password was encoded using `crypto_pwhash_str`.
637///
638/// Compatible with libsodium's `crypto_pwhash_str_verify`.
639///
640/// # Errors
641///
642/// Returns an error if `hashed_password` is malformed, uses unsupported
643/// parameters, or does not match `password`.
644#[cfg(any(feature = "base64", all(doc, not(doctest))))]
645#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
646pub fn crypto_pwhash_str_verify(hashed_password: &str, password: &[u8]) -> Result<(), Error> {
647    let pwhash = Pwhash::parse_encoded_pwhash(hashed_password)?;
648    let t_cost = pwhash.t_cost.ok_or(Error::missing_data(
649        crate::ErrorContext::PasswordHashTimeCost,
650    ))?;
651    let m_cost = pwhash.m_cost.ok_or(Error::missing_data(
652        crate::ErrorContext::PasswordHashMemoryCost,
653    ))?;
654    let parallelism = pwhash.parallelism.ok_or(Error::missing_data(
655        crate::ErrorContext::PasswordHashParallelism,
656    ))?;
657    let salt = pwhash
658        .salt
659        .ok_or(Error::missing_data(crate::ErrorContext::PasswordHashSalt))?;
660    let algorithm = pwhash.type_.ok_or(Error::missing_data(
661        crate::ErrorContext::PasswordHashAlgorithm,
662    ))?;
663    let expected_hash = pwhash
664        .pwhash
665        .ok_or(Error::missing_data(crate::ErrorContext::PasswordHash))?;
666
667    verify_pwhash_parts(
668        &expected_hash,
669        password,
670        &salt,
671        t_cost,
672        m_cost,
673        parallelism,
674        algorithm,
675    )
676}
677
678pub(crate) fn verify_pwhash_parts(
679    expected_hash: &[u8],
680    password: &[u8],
681    salt: &[u8],
682    t_cost: u32,
683    m_cost: u32,
684    parallelism: u32,
685    algorithm: PasswordHashAlgorithm,
686) -> Result<(), Error> {
687    let mut hash = Zeroizing::new(vec![0u8; expected_hash.len()]);
688    argon2_hash(
689        t_cost,
690        m_cost,
691        parallelism,
692        password,
693        salt,
694        None,
695        None,
696        &mut hash,
697        algorithm.into(),
698    )?;
699
700    if hash.as_slice().ct_eq(expected_hash).unwrap_u8() == 1 {
701        Ok(())
702    } else {
703        Err(Error::AuthenticationFailed)
704    }
705}
706
707/// Checks if the parameters for `hashed_password` match those passed to the
708/// function. Returns `false` if the parameters match, and `true` if the
709/// parameters are mismatched (requiring a rehash).
710///
711/// Compatible with libsodium's `crypto_pwhash_str_needs_rehash`.
712///
713/// # Errors
714///
715/// Returns an error if `hashed_password` is malformed or uses unsupported
716/// parameters.
717#[cfg(any(feature = "base64", all(doc, not(doctest))))]
718#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
719pub fn crypto_pwhash_str_needs_rehash(
720    hashed_password: &str,
721    opslimit: u64,
722    memlimit: usize,
723) -> Result<bool, Error> {
724    let (t_cost, m_cost) = convert_costs_checked(opslimit, memlimit)?;
725    let pwhash = Pwhash::parse_encoded_pwhash(hashed_password)?;
726    let parsed_t_cost = pwhash.t_cost.ok_or(Error::missing_data(
727        crate::ErrorContext::PasswordHashTimeCost,
728    ))?;
729    let parsed_m_cost = pwhash.m_cost.ok_or(Error::missing_data(
730        crate::ErrorContext::PasswordHashMemoryCost,
731    ))?;
732
733    if t_cost != parsed_t_cost || m_cost != parsed_m_cost {
734        Ok(true)
735    } else {
736        Ok(false)
737    }
738}
739
740#[cfg(all(test, dryoc_native_tests))]
741mod tests {
742    use super::*;
743
744    #[test]
745    fn test_crypto_pwhash() {
746        use sodiumoxide::crypto::pwhash;
747
748        use crate::rng::copy_randombytes;
749
750        let mut hash = [0u8; 32];
751        let mut so_hash = [0u8; 32];
752        let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
753
754        copy_randombytes(&mut salt);
755
756        let password = b"donkey kong";
757
758        crypto_pwhash(
759            &mut hash,
760            password,
761            &salt,
762            CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
763            CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
764            PasswordHashAlgorithm::Argon2id13,
765        )
766        .expect("pwhash failed");
767
768        let _ = pwhash::argon2id13::derive_key(
769            &mut so_hash,
770            password,
771            &pwhash::argon2id13::Salt::from_slice(&salt).expect("salt failed"),
772            pwhash::argon2id13::OPSLIMIT_INTERACTIVE,
773            pwhash::argon2id13::MEMLIMIT_INTERACTIVE,
774        )
775        .expect("so pwhash failed");
776
777        assert_eq!(hash, so_hash);
778    }
779
780    #[cfg(feature = "base64")]
781    #[test]
782    fn test_base64_no_pad_matches_base64_crate() {
783        use base64::Engine as _;
784        use base64::engine::general_purpose;
785
786        for len in 0..128 {
787            let input: Vec<u8> = (0..len).map(|i| (i * 31 + len) as u8).collect();
788            let encoded = base64_no_pad_encode(&input);
789            assert_eq!(encoded, general_purpose::STANDARD_NO_PAD.encode(&input));
790            assert_eq!(
791                base64_no_pad_decode(&encoded).as_deref(),
792                Some(input.as_slice())
793            );
794        }
795
796        assert_eq!(base64_no_pad_decode("A"), None);
797        assert_eq!(base64_no_pad_decode("AA="), None);
798        assert_eq!(base64_no_pad_decode("A/"), None);
799        assert_eq!(base64_no_pad_decode("AA/"), None);
800
801        let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
802        let hash = [0u8; STR_HASHBYTES];
803        let encoded = pwhash_to_string(
804            PasswordHashAlgorithm::Argon2id13,
805            2,
806            65_536,
807            1,
808            &salt,
809            &hash,
810        );
811        assert_eq!(
812            pwhash_string_len(
813                PasswordHashAlgorithm::Argon2id13,
814                2,
815                65_536,
816                1,
817                salt.len(),
818                hash.len(),
819            ),
820            Some(encoded.len())
821        );
822
823        assert_eq!(
824            pwhash_string_len(
825                PasswordHashAlgorithm::Argon2id13,
826                2,
827                65_536,
828                1,
829                usize::MAX,
830                0
831            ),
832            None,
833        );
834        #[cfg(target_pointer_width = "32")]
835        assert_eq!(
836            pwhash_string_len(
837                PasswordHashAlgorithm::Argon2id13,
838                2,
839                65_536,
840                1,
841                3_221_225_471,
842                0,
843            ),
844            None,
845        );
846    }
847
848    #[cfg(feature = "base64")]
849    #[test]
850    fn malformed_password_hash_fields_have_structured_errors() {
851        const SALT: &str = "AAAAAAAAAAA";
852        const HASH: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
853        let cases = [
854            (
855                format!("$argon2wat$v=19$m=65536,t=2,p=1${SALT}${HASH}"),
856                crate::ErrorContext::PasswordHashAlgorithm,
857            ),
858            (
859                format!("$argon2id$v=nope$m=65536,t=2,p=1${SALT}${HASH}"),
860                crate::ErrorContext::PasswordHashVersion,
861            ),
862            (
863                format!("$argon2id$v=19$m=nope,t=2,p=1${SALT}${HASH}"),
864                crate::ErrorContext::PasswordHashMemoryCost,
865            ),
866            (
867                format!("$argon2id$v=19$m=65536,t=nope,p=1${SALT}${HASH}"),
868                crate::ErrorContext::PasswordHashTimeCost,
869            ),
870            (
871                format!("$argon2id$v=19$m=65536,t=2,p=nope${SALT}${HASH}"),
872                crate::ErrorContext::PasswordHashParallelism,
873            ),
874            (
875                format!("$argon2id$v=19$m=65536,t=2,p=1$A${HASH}"),
876                crate::ErrorContext::PasswordHashSalt,
877            ),
878            (
879                format!("$argon2id$v=19$m=65536,t=2,p=1${SALT}$A"),
880                crate::ErrorContext::PasswordHash,
881            ),
882            (
883                format!("$argon2id$v=18$m=65536,t=2,p=1${SALT}${HASH}"),
884                crate::ErrorContext::PasswordHashVersion,
885            ),
886            (
887                format!("$argon2id$v=019$m=65536,t=2,p=1${SALT}${HASH}"),
888                crate::ErrorContext::PasswordHashVersion,
889            ),
890            (
891                format!("$argon2id$v=19$m=065536,t=2,p=1${SALT}${HASH}"),
892                crate::ErrorContext::PasswordHashMemoryCost,
893            ),
894            (
895                format!("$argon2id$v=19$m=65536,t=+2,p=1${SALT}${HASH}"),
896                crate::ErrorContext::PasswordHashTimeCost,
897            ),
898            (
899                format!("$argon2id$v=19$m=65536,t=2,p=01${SALT}${HASH}"),
900                crate::ErrorContext::PasswordHashParallelism,
901            ),
902        ];
903
904        for (encoded, expected_context) in cases {
905            let error = match Pwhash::parse_encoded_pwhash(&encoded) {
906                Ok(_) => panic!("the malformed field should be rejected"),
907                Err(error) => error,
908            };
909            assert!(matches!(
910                error,
911                Error::InvalidEncoding { context } if context == expected_context
912            ));
913        }
914
915        let missing_hash = format!("$argon2id$v=19$m=65536,t=2,p=1${SALT}");
916        assert!(matches!(
917            Pwhash::parse_encoded_pwhash(&missing_hash),
918            Err(Error::MissingData {
919                context: crate::ErrorContext::PasswordHash,
920            })
921        ));
922
923        let missing_algorithm = format!("$v=19$m=65536,t=2,p=1${SALT}${HASH}");
924        assert!(matches!(
925            Pwhash::parse_encoded_pwhash(&missing_algorithm),
926            Err(Error::MissingData {
927                context: crate::ErrorContext::PasswordHashAlgorithm,
928            })
929        ));
930    }
931
932    #[test]
933    fn password_hashing_reports_invalid_resource_limits() {
934        let mut output = [0u8; CRYPTO_PWHASH_BYTES_MIN];
935        let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
936        let password = b"password";
937
938        for (opslimit, memlimit, expected_context) in [
939            (
940                CRYPTO_PWHASH_OPSLIMIT_MIN - 1,
941                CRYPTO_PWHASH_MEMLIMIT_MIN,
942                crate::ErrorContext::OperationsLimit,
943            ),
944            (
945                CRYPTO_PWHASH_OPSLIMIT_MIN,
946                CRYPTO_PWHASH_MEMLIMIT_MIN - 1,
947                crate::ErrorContext::MemoryLimit,
948            ),
949        ] {
950            let error = crypto_pwhash(
951                &mut output,
952                password,
953                &salt,
954                opslimit,
955                memlimit,
956                PasswordHashAlgorithm::Argon2id13,
957            )
958            .expect_err("invalid resource limits should fail");
959            assert!(matches!(
960                error,
961                Error::InvalidValue { context, .. } if context == expected_context
962            ));
963
964            #[cfg(feature = "base64")]
965            {
966                let error = crypto_pwhash_str(password, opslimit, memlimit)
967                    .expect_err("invalid resource limits should fail");
968                assert!(matches!(
969                    error,
970                    Error::InvalidValue { context, .. } if context == expected_context
971                ));
972            }
973        }
974    }
975
976    #[test]
977    fn password_hashing_enforces_classic_parameter_contract() {
978        let mut output = [0u8; CRYPTO_PWHASH_BYTES_MIN];
979        let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
980
981        for opslimit in 1..CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN {
982            assert!(matches!(
983                crypto_pwhash(
984                    &mut output,
985                    b"password",
986                    &salt,
987                    opslimit,
988                    CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MIN,
989                    PasswordHashAlgorithm::Argon2i13,
990                ),
991                Err(Error::InvalidValue {
992                    context: crate::ErrorContext::OperationsLimit,
993                    ..
994                })
995            ));
996        }
997
998        assert!(matches!(
999            crypto_pwhash(
1000                &mut output,
1001                b"password",
1002                &salt[..CRYPTO_PWHASH_SALTBYTES - 1],
1003                CRYPTO_PWHASH_OPSLIMIT_MIN,
1004                CRYPTO_PWHASH_MEMLIMIT_MIN,
1005                PasswordHashAlgorithm::Argon2id13,
1006            ),
1007            Err(Error::InvalidLength {
1008                context: crate::ErrorContext::PasswordHashSalt,
1009                constraint: crate::LengthConstraint::Exact(CRYPTO_PWHASH_SALTBYTES),
1010                ..
1011            })
1012        ));
1013
1014        assert!(PasswordHashAlgorithm::try_from(0).is_err());
1015        assert_eq!(
1016            PasswordHashAlgorithm::try_from(CRYPTO_PWHASH_ALG_ARGON2ID13 as u32)
1017                .expect("valid algorithm"),
1018            PasswordHashAlgorithm::Argon2id13
1019        );
1020    }
1021
1022    #[cfg(feature = "base64")]
1023    #[test]
1024    fn test_crypto_pwhash_str() {
1025        use sodiumoxide::crypto::pwhash;
1026
1027        let password = b"donkey kong";
1028
1029        let pwhash = crypto_pwhash_str(
1030            password,
1031            CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1032            CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
1033        )
1034        .expect("pwhash failed");
1035        let pwhash2 = crypto_pwhash_str(
1036            password,
1037            CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1038            CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
1039        )
1040        .expect("pwhash failed");
1041
1042        let parsed = Pwhash::parse_encoded_pwhash(&pwhash).expect("couldn't parse pwhash");
1043        let parsed2 = Pwhash::parse_encoded_pwhash(&pwhash2).expect("couldn't parse pwhash");
1044
1045        assert_ne!(
1046            parsed.salt.as_ref().expect("missing salt"),
1047            &vec![0u8; CRYPTO_PWHASH_SALTBYTES]
1048        );
1049        assert_ne!(parsed.salt, parsed2.salt);
1050
1051        let mut pwhash_bytes = [0u8; CRYPTO_PWHASH_STRBYTES];
1052        pwhash_bytes[..pwhash.len()].copy_from_slice(pwhash.as_bytes());
1053
1054        assert!(pwhash::argon2id13::pwhash_verify(
1055            &pwhash::argon2id13::HashedPassword::from_slice(&pwhash_bytes)
1056                .expect("hashed password failed"),
1057            password,
1058        ));
1059
1060        let argon2i = crypto_pwhash_str_alg(
1061            password,
1062            CRYPTO_PWHASH_ARGON2I_OPSLIMIT_INTERACTIVE,
1063            CRYPTO_PWHASH_ARGON2I_MEMLIMIT_INTERACTIVE,
1064            PasswordHashAlgorithm::Argon2i13,
1065        )
1066        .expect("argon2i pwhash failed");
1067        assert!(argon2i.starts_with(CRYPTO_PWHASH_ARGON2I_STRPREFIX));
1068        crypto_pwhash_str_verify(&argon2i, password).expect("argon2i verify failed");
1069    }
1070
1071    #[cfg(feature = "base64")]
1072    #[test]
1073    fn test_crypto_pwhash_str_verify() {
1074        use sodiumoxide::crypto::pwhash;
1075
1076        let password = b"donkey kong";
1077
1078        let pwhash = pwhash::argon2id13::pwhash(
1079            password,
1080            pwhash::argon2id13::OPSLIMIT_INTERACTIVE,
1081            pwhash::argon2id13::MEMLIMIT_INTERACTIVE,
1082        )
1083        .expect("so pwhash failed");
1084
1085        let pw_str = std::str::from_utf8(&pwhash.0)
1086            .expect("from ut8 failed")
1087            .trim_end_matches('\x00');
1088
1089        crypto_pwhash_str_verify(pw_str, password).expect("verify failed");
1090        crypto_pwhash_str_verify(pw_str, b"invalid password")
1091            .expect_err("verify should have failed");
1092
1093        for encoded in [
1094            concat!(
1095                "$argon2id$v=19$m=256,t=3,p=1$MDEyMzQ1Njc$",
1096                "G5ajKFCoUzaXRLdz7UJb5wGkb2Xt+X5/GQjUYtS2+TE",
1097            ),
1098            concat!(
1099                "$argon2i$v=19$m=4096,t=3,p=2$b2RpZHVlamRpc29kaXNrdw$",
1100                "TNnWIwlu1061JHrnCqIAmjs3huSxYIU+0jWipu7Kc9M",
1101            ),
1102        ] {
1103            crypto_pwhash_str_verify(encoded, b"password")
1104                .expect("valid libsodium Argon2 vector should verify");
1105        }
1106
1107        assert!(crypto_pwhash_str_verify(&format!("{pw_str}$garbage"), password).is_err());
1108        assert!(crypto_pwhash_str_verify(pw_str.trim_start_matches('$'), password).is_err());
1109        for invalid_parallelism in ["0", "4294967295"] {
1110            let malformed = pw_str.replace(",p=1", &format!(",p={invalid_parallelism}"));
1111            assert!(crypto_pwhash_str_verify(&malformed, password).is_err());
1112            assert!(
1113                crypto_pwhash_str_needs_rehash(
1114                    &malformed,
1115                    CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1116                    CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
1117                )
1118                .is_err()
1119            );
1120        }
1121
1122        // should be false
1123        assert!(
1124            !crypto_pwhash_str_needs_rehash(
1125                pw_str,
1126                CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1127                CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
1128            )
1129            .expect("verify rehash failed")
1130        );
1131
1132        // should be true
1133        assert!(
1134            crypto_pwhash_str_needs_rehash(
1135                pw_str,
1136                CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE + 1,
1137                CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
1138            )
1139            .expect("verify rehash failed")
1140        );
1141
1142        assert!(
1143            crypto_pwhash_str_needs_rehash(pw_str, 0, 0,)
1144                .expect("zero costs are a valid rehash comparison")
1145        );
1146
1147        assert!(matches!(
1148            crypto_pwhash_str_needs_rehash(pw_str, u32::MAX as u64 + 1, 0),
1149            Err(Error::InvalidValue {
1150                context: crate::ErrorContext::OperationsLimit,
1151                ..
1152            })
1153        ));
1154    }
1155}