Skip to main content

dryoc/classic/
crypto_pwhash.rs

1//! # Password hashing
2//!
3//! Implements libsodium's `crypto_pwhash_*` functions with Argon2i and
4//! Argon2id. Scrypt is not supported.
5//!
6//! String-based password hashes are enabled by default. Disable them by
7//! building without default features, or enable them explicitly with the
8//! `base64` feature.
9//!
10//! See the [libsodium documentation](https://doc.libsodium.org/password_hashing/default_phf)
11//! for details.
12//!
13//! ## Classic API example, key derivation
14//!
15//! ```
16//! use base64::Engine as _;
17//! use base64::engine::general_purpose;
18//! use dryoc::classic::crypto_pwhash::*;
19//! use dryoc::constants::{
20//!     CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
21//!     CRYPTO_PWHASH_SALTBYTES, CRYPTO_SECRETBOX_KEYBYTES,
22//! };
23//! use dryoc::rng::copy_randombytes;
24//!
25//! let mut key = [0u8; CRYPTO_SECRETBOX_KEYBYTES];
26//!
27//! // Generate a random salt.
28//! let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
29//! copy_randombytes(&mut salt);
30//!
31//! let password = b"a long, unique passphrase";
32//!
33//! crypto_pwhash(
34//!     &mut key,
35//!     password,
36//!     &salt,
37//!     CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
38//!     CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
39//!     PasswordHashAlgorithm::Argon2id13,
40//! )
41//! .expect("pwhash failed");
42//!
43//! // `key` can now be used as a secret key.
44//! println!("key = {}", general_purpose::STANDARD_NO_PAD.encode(&key));
45//! ```
46
47#[cfg(any(feature = "base64", all(doc, not(doctest))))]
48use alloc::string::String;
49#[cfg(any(feature = "base64", all(doc, not(doctest))))]
50use alloc::vec::Vec;
51
52#[cfg(feature = "serde")]
53use serde::{Deserialize, Serialize};
54use zeroize::{Zeroize, Zeroizing};
55
56#[cfg(feature = "base64")]
57use crate::argon2::ARGON2_VERSION_NUMBER;
58use crate::argon2::{self, argon2_hash};
59use crate::constants::*;
60use crate::error::Error;
61use crate::utils::verify_ct;
62
63pub(crate) const STR_HASHBYTES: usize = 32;
64
65#[derive(Zeroize, Clone, Copy, Debug, Eq, PartialEq)]
66#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
67/// Password hash algorithm implementations.
68pub enum PasswordHashAlgorithm {
69    /// Argon2i version 0x13 (v19)
70    Argon2i13  = 1,
71    /// Argon2id version 0x13 (v19)
72    Argon2id13 = 2,
73}
74
75impl TryFrom<u32> for PasswordHashAlgorithm {
76    type Error = Error;
77
78    fn try_from(num: u32) -> Result<Self, Self::Error> {
79        match num {
80            num if num == PasswordHashAlgorithm::Argon2i13 as u32 => {
81                Ok(PasswordHashAlgorithm::Argon2i13)
82            }
83            num if num == PasswordHashAlgorithm::Argon2id13 as u32 => {
84                Ok(PasswordHashAlgorithm::Argon2id13)
85            }
86            _ => Err(Error::InvalidValue {
87                context: crate::ErrorContext::PasswordHashAlgorithm,
88                actual: num as u64,
89                constraint: crate::ValueConstraint::Between {
90                    min: PasswordHashAlgorithm::Argon2i13 as u64,
91                    max: PasswordHashAlgorithm::Argon2id13 as u64,
92                },
93            }),
94        }
95    }
96}
97
98impl From<PasswordHashAlgorithm> for argon2::Argon2Type {
99    fn from(algo: PasswordHashAlgorithm) -> Self {
100        match algo {
101            PasswordHashAlgorithm::Argon2i13 => argon2::Argon2Type::Argon2i,
102            PasswordHashAlgorithm::Argon2id13 => argon2::Argon2Type::Argon2id,
103        }
104    }
105}
106
107/// Hashes `password` with `salt`, placing the resulting hash into `output`.
108///
109/// * `opslimit` specifies the number of iterations to use in the underlying
110///   algorithm
111/// * `memlimit` specifies the maximum amount of memory to use, in bytes
112///
113/// Generally speaking, you want to set `opslimit` and `memlimit` sufficiently
114/// large such that it's hard for someone to brute-force a password.
115///
116/// For your convenience, the following constants are defined which can be used
117/// with `opslimit` and `memlimit`:
118/// * [`CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE`] and
119///   [`CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE`] for interactive operations
120/// * [`CRYPTO_PWHASH_OPSLIMIT_MODERATE`] and
121///   [`CRYPTO_PWHASH_MEMLIMIT_MODERATE`] for typical operations, such as
122///   server-side password hashing
123/// * [`CRYPTO_PWHASH_OPSLIMIT_SENSITIVE`] and
124///   [`CRYPTO_PWHASH_MEMLIMIT_SENSITIVE`] for sensitive operations
125///
126/// Compatible with libsodium's `crypto_pwhash`.
127///
128/// # Errors
129///
130/// Returns an error if the cost parameters, salt length, or output length are
131/// invalid, or if Argon2 cannot hash the password with the requested settings.
132pub fn crypto_pwhash(
133    output: &mut [u8],
134    password: &[u8],
135    salt: &[u8],
136    opslimit: u64,
137    memlimit: usize,
138    algorithm: PasswordHashAlgorithm,
139) -> Result<(), Error> {
140    validate_pwhash_parameters(
141        output.len(),
142        password.len(),
143        salt.len(),
144        opslimit,
145        memlimit,
146        algorithm,
147    )?;
148
149    let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
150
151    argon2_hash(
152        t_cost,
153        m_cost,
154        1,
155        password,
156        salt,
157        None,
158        None,
159        output,
160        algorithm.into(),
161    )
162}
163
164pub(crate) fn validate_pwhash_parameters(
165    output_len: usize,
166    password_len: usize,
167    salt_len: usize,
168    opslimit: u64,
169    memlimit: usize,
170    algorithm: PasswordHashAlgorithm,
171) -> Result<(), Error> {
172    let (
173        bytes_min,
174        bytes_max,
175        password_max,
176        opslimit_min,
177        opslimit_max,
178        memlimit_min,
179        memlimit_max,
180    ) = match algorithm {
181        PasswordHashAlgorithm::Argon2i13 => (
182            CRYPTO_PWHASH_ARGON2I_BYTES_MIN,
183            CRYPTO_PWHASH_ARGON2I_BYTES_MAX,
184            CRYPTO_PWHASH_ARGON2I_PASSWD_MAX,
185            CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN,
186            CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MAX,
187            CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MIN,
188            CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MAX,
189        ),
190        PasswordHashAlgorithm::Argon2id13 => (
191            CRYPTO_PWHASH_ARGON2ID_BYTES_MIN,
192            CRYPTO_PWHASH_ARGON2ID_BYTES_MAX,
193            CRYPTO_PWHASH_ARGON2ID_PASSWD_MAX,
194            CRYPTO_PWHASH_ARGON2ID_OPSLIMIT_MIN,
195            CRYPTO_PWHASH_ARGON2ID_OPSLIMIT_MAX,
196            CRYPTO_PWHASH_ARGON2ID_MEMLIMIT_MIN,
197            CRYPTO_PWHASH_ARGON2ID_MEMLIMIT_MAX,
198        ),
199    };
200
201    validate_length!(
202        bytes_min,
203        bytes_max,
204        output_len,
205        crate::ErrorContext::Output
206    );
207    validate_length!(
208        CRYPTO_PWHASH_PASSWD_MIN,
209        password_max,
210        password_len,
211        crate::ErrorContext::Password
212    );
213    validate_length!(
214        exact CRYPTO_PWHASH_SALTBYTES,
215        salt_len,
216        crate::ErrorContext::PasswordHashSalt
217    );
218    validate_value!(
219        opslimit_min,
220        opslimit_max,
221        opslimit,
222        crate::ErrorContext::OperationsLimit
223    );
224    validate_value!(
225        memlimit_min,
226        memlimit_max,
227        memlimit,
228        crate::ErrorContext::MemoryLimit
229    );
230
231    Ok(())
232}
233
234#[cfg(any(feature = "base64", all(doc, not(doctest))))]
235#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
236pub(crate) fn pwhash_to_string(
237    algorithm: PasswordHashAlgorithm,
238    t_cost: u32,
239    m_cost: u32,
240    parallelism: u32,
241    salt: &[u8],
242    hash: &[u8],
243) -> String {
244    let algorithm_name = pwhash_algorithm_name(algorithm);
245    format!(
246        "${algorithm_name}$v={}$m={},t={},p={parallelism}${}${}",
247        argon2::ARGON2_VERSION_NUMBER,
248        m_cost,
249        t_cost,
250        base64_no_pad_encode(salt),
251        base64_no_pad_encode(hash),
252    )
253}
254
255#[cfg(any(feature = "base64", all(doc, not(doctest))))]
256pub(crate) fn pwhash_string_len(
257    algorithm: PasswordHashAlgorithm,
258    t_cost: u32,
259    m_cost: u32,
260    parallelism: u32,
261    salt_len: usize,
262    hash_len: usize,
263) -> Option<usize> {
264    let salt_len = base64_no_pad_encoded_len(salt_len)?;
265    let hash_len = base64_no_pad_encoded_len(hash_len)?;
266    1usize
267        .checked_add(pwhash_algorithm_name(algorithm).len())?
268        .checked_add(3 + decimal_len(ARGON2_VERSION_NUMBER))?
269        .checked_add(3 + decimal_len(m_cost))?
270        .checked_add(3 + decimal_len(t_cost))?
271        .checked_add(3 + decimal_len(parallelism))?
272        .checked_add(1)?
273        .checked_add(salt_len)?
274        .checked_add(1)?
275        .checked_add(hash_len)
276}
277
278#[cfg(any(feature = "base64", all(doc, not(doctest))))]
279const fn pwhash_algorithm_name(algorithm: PasswordHashAlgorithm) -> &'static str {
280    match algorithm {
281        PasswordHashAlgorithm::Argon2i13 => "argon2i",
282        PasswordHashAlgorithm::Argon2id13 => "argon2id",
283    }
284}
285
286#[cfg(any(feature = "base64", all(doc, not(doctest))))]
287const fn decimal_len(value: u32) -> usize {
288    if value == 0 {
289        1
290    } else {
291        value.ilog10() as usize + 1
292    }
293}
294
295#[cfg(any(feature = "base64", all(doc, not(doctest))))]
296const fn base64_no_pad_encoded_len(input_len: usize) -> Option<usize> {
297    let remainder_len = match input_len % 3 {
298        0 => 0,
299        1 => 2,
300        _ => 3,
301    };
302    match (input_len / 3).checked_mul(4) {
303        Some(full_len) => full_len.checked_add(remainder_len),
304        None => None,
305    }
306}
307
308#[cfg(any(feature = "base64", all(doc, not(doctest))))]
309fn base64_no_pad_encode(input: &[u8]) -> String {
310    const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
311
312    let mut output = String::with_capacity(input.len().div_ceil(3) * 4);
313
314    let (chunks, rem) = input.as_chunks::<3>();
315
316    for chunk in chunks {
317        let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
318        output.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
319        output.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
320        output.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
321        output.push(ALPHABET[(n & 0x3f) as usize] as char);
322    }
323
324    if rem.len() == 1 {
325        let n = (rem[0] as u32) << 16;
326        output.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
327        output.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
328    } else if rem.len() == 2 {
329        let n = ((rem[0] as u32) << 16) | ((rem[1] as u32) << 8);
330        output.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
331        output.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
332        output.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
333    }
334
335    output
336}
337
338#[cfg(feature = "base64")]
339fn base64_no_pad_decode(input: &str) -> Option<Vec<u8>> {
340    fn decode_byte(byte: u8) -> Option<u8> {
341        match byte {
342            b'A'..=b'Z' => Some(byte - b'A'),
343            b'a'..=b'z' => Some(byte - b'a' + 26),
344            b'0'..=b'9' => Some(byte - b'0' + 52),
345            b'+' => Some(62),
346            b'/' => Some(63),
347            _ => None,
348        }
349    }
350
351    let input = input.as_bytes();
352    if input.len() % 4 == 1 || input.contains(&b'=') {
353        return None;
354    }
355
356    let mut output = Vec::with_capacity(input.len() / 4 * 3 + 2);
357    let (chunks, rem) = input.as_chunks::<4>();
358
359    for chunk in chunks {
360        let n = ((decode_byte(chunk[0])? as u32) << 18)
361            | ((decode_byte(chunk[1])? as u32) << 12)
362            | ((decode_byte(chunk[2])? as u32) << 6)
363            | decode_byte(chunk[3])? as u32;
364        output.push((n >> 16) as u8);
365        output.push((n >> 8) as u8);
366        output.push(n as u8);
367    }
368
369    if rem.len() == 2 {
370        let second = decode_byte(rem[1])?;
371        if second & 0x0f != 0 {
372            return None;
373        }
374        let n = ((decode_byte(rem[0])? as u32) << 18) | ((second as u32) << 12);
375        output.push((n >> 16) as u8);
376    } else if rem.len() == 3 {
377        let third = decode_byte(rem[2])?;
378        if third & 0x03 != 0 {
379            return None;
380        }
381        let n = ((decode_byte(rem[0])? as u32) << 18)
382            | ((decode_byte(rem[1])? as u32) << 12)
383            | ((third as u32) << 6);
384        output.push((n >> 16) as u8);
385        output.push((n >> 8) as u8);
386    }
387
388    Some(output)
389}
390
391pub(crate) fn convert_costs(opslimit: u64, memlimit: usize) -> (u32, u32) {
392    (opslimit as u32, (memlimit / 1024) as u32)
393}
394
395pub(crate) fn convert_costs_checked(opslimit: u64, memlimit: usize) -> Result<(u32, u32), Error> {
396    let t_cost = u32::try_from(opslimit).map_err(|_| Error::InvalidValue {
397        context: crate::ErrorContext::OperationsLimit,
398        actual: opslimit,
399        constraint: crate::ValueConstraint::Between {
400            min: 0,
401            max: u32::MAX as u64,
402        },
403    })?;
404    let m_cost = u32::try_from(memlimit / 1024).map_err(|_| Error::InvalidValue {
405        context: crate::ErrorContext::MemoryLimit,
406        actual: memlimit as u64,
407        constraint: crate::ValueConstraint::Between {
408            min: 0,
409            max: (u32::MAX as u64) * 1024 + 1023,
410        },
411    })?;
412    Ok((t_cost, m_cost))
413}
414
415/// Hash a password string with a random salt.
416///
417/// This function provides a wrapper for [`crypto_pwhash`] that returns a string
418/// encoding of a hashed password with a random salt, suitable for use with
419/// password hash storage (i.e., in a database). Can be used to verify a
420/// password using [`crypto_pwhash_str_verify`].
421///
422/// Compatible with libsodium's `crypto_pwhash_str`.
423///
424/// # Errors
425///
426/// Returns an error if the password or resource limits are unsupported, or if
427/// Argon2 cannot hash the password.
428///
429/// # Panics
430///
431/// Panics if the operating system's random number generator fails.
432#[cfg(any(feature = "base64", all(doc, not(doctest))))]
433#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
434pub fn crypto_pwhash_str(password: &[u8], opslimit: u64, memlimit: usize) -> Result<String, Error> {
435    crypto_pwhash_str_alg(
436        password,
437        opslimit,
438        memlimit,
439        PasswordHashAlgorithm::Argon2id13,
440    )
441}
442
443/// Hashes a password with a random salt and the selected algorithm, returning
444/// a database-safe encoded string.
445///
446/// Compatible with libsodium's `crypto_pwhash_str_alg`.
447///
448/// # Errors
449///
450/// Returns an error if the password or resource limits are unsupported, or if
451/// Argon2 cannot hash the password.
452///
453/// # Panics
454///
455/// Panics if the operating system's random number generator fails.
456#[cfg(any(feature = "base64", all(doc, not(doctest))))]
457#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
458pub fn crypto_pwhash_str_alg(
459    password: &[u8],
460    opslimit: u64,
461    memlimit: usize,
462    algorithm: PasswordHashAlgorithm,
463) -> Result<String, Error> {
464    validate_pwhash_parameters(
465        STR_HASHBYTES,
466        password.len(),
467        CRYPTO_PWHASH_SALTBYTES,
468        opslimit,
469        memlimit,
470        algorithm,
471    )?;
472
473    let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
474    let mut hash = [0u8; STR_HASHBYTES];
475    crate::rng::copy_randombytes(&mut salt);
476
477    let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
478
479    crypto_pwhash(&mut hash, password, &salt, opslimit, memlimit, algorithm)?;
480
481    let pw = pwhash_to_string(algorithm, t_cost, m_cost, 1, &salt, &hash);
482
483    Ok(pw)
484}
485
486#[cfg(feature = "base64")]
487#[derive(Default)]
488pub(crate) struct Pwhash {
489    pub(crate) pwhash: Option<Vec<u8>>,
490    pub(crate) salt: Option<Vec<u8>>,
491    pub(crate) type_: Option<PasswordHashAlgorithm>,
492    pub(crate) t_cost: Option<u32>,
493    pub(crate) m_cost: Option<u32>,
494    pub(crate) parallelism: Option<u32>,
495}
496
497#[cfg(feature = "base64")]
498impl Pwhash {
499    pub(crate) fn parse_encoded_pwhash(hashed_password: &str) -> Result<Self, Error> {
500        let encoded = hashed_password
501            .strip_prefix('$')
502            .ok_or_else(|| Error::invalid_encoding(crate::ErrorContext::PasswordHash))?;
503        let mut fields = encoded.split('$');
504
505        let algorithm = match fields.next().filter(|field| !field.is_empty()) {
506            Some("argon2i") => PasswordHashAlgorithm::Argon2i13,
507            Some("argon2id") => PasswordHashAlgorithm::Argon2id13,
508            Some(field) if field.starts_with("v=") => {
509                return Err(Error::missing_data(
510                    crate::ErrorContext::PasswordHashAlgorithm,
511                ));
512            }
513            Some(_) => {
514                return Err(Error::invalid_encoding(
515                    crate::ErrorContext::PasswordHashAlgorithm,
516                ));
517            }
518            None => {
519                return Err(Error::missing_data(
520                    crate::ErrorContext::PasswordHashAlgorithm,
521                ));
522            }
523        };
524
525        let version = fields
526            .next()
527            .ok_or(Error::missing_data(
528                crate::ErrorContext::PasswordHashVersion,
529            ))?
530            .strip_prefix("v=")
531            .ok_or(Error::invalid_encoding(
532                crate::ErrorContext::PasswordHashVersion,
533            ))?;
534        let version =
535            parse_minimal_pwhash_decimal(version, crate::ErrorContext::PasswordHashVersion)?;
536        if version != ARGON2_VERSION_NUMBER {
537            return Err(Error::invalid_encoding(
538                crate::ErrorContext::PasswordHashVersion,
539            ));
540        }
541
542        let parameters = fields.next().ok_or(Error::missing_data(
543            crate::ErrorContext::PasswordHashMemoryCost,
544        ))?;
545        let mut parameters = parameters.split(',');
546        let m_cost = parse_pwhash_parameter(
547            parameters.next(),
548            "m=",
549            crate::ErrorContext::PasswordHashMemoryCost,
550        )?;
551        let t_cost = parse_pwhash_parameter(
552            parameters.next(),
553            "t=",
554            crate::ErrorContext::PasswordHashTimeCost,
555        )?;
556        let parallelism = parse_pwhash_parameter(
557            parameters.next(),
558            "p=",
559            crate::ErrorContext::PasswordHashParallelism,
560        )?;
561        if parameters.next().is_some() {
562            return Err(Error::invalid_encoding(crate::ErrorContext::PasswordHash));
563        }
564
565        let salt = fields
566            .next()
567            .filter(|field| !field.is_empty())
568            .ok_or(Error::missing_data(crate::ErrorContext::PasswordHashSalt))?;
569        let salt = base64_no_pad_decode(salt).ok_or(Error::invalid_encoding(
570            crate::ErrorContext::PasswordHashSalt,
571        ))?;
572
573        let pwhash = fields
574            .next()
575            .filter(|field| !field.is_empty())
576            .ok_or(Error::missing_data(crate::ErrorContext::PasswordHash))?;
577        let pwhash = base64_no_pad_decode(pwhash)
578            .ok_or(Error::invalid_encoding(crate::ErrorContext::PasswordHash))?;
579
580        if fields.next().is_some() {
581            return Err(Error::invalid_encoding(crate::ErrorContext::PasswordHash));
582        }
583
584        crate::argon2::validate_argon2_pwhash_parameters(
585            pwhash.len(),
586            salt.len(),
587            t_cost,
588            m_cost,
589            parallelism,
590        )?;
591
592        Ok(Self {
593            pwhash: Some(pwhash),
594            salt: Some(salt),
595            type_: Some(algorithm),
596            t_cost: Some(t_cost),
597            m_cost: Some(m_cost),
598            parallelism: Some(parallelism),
599        })
600    }
601}
602
603#[cfg(feature = "base64")]
604fn parse_pwhash_parameter(
605    parameter: Option<&str>,
606    prefix: &str,
607    context: crate::ErrorContext,
608) -> Result<u32, Error> {
609    let value = parameter
610        .ok_or(Error::missing_data(context))?
611        .strip_prefix(prefix)
612        .ok_or(Error::invalid_encoding(context))?;
613    parse_minimal_pwhash_decimal(value, context)
614}
615
616#[cfg(feature = "base64")]
617fn parse_minimal_pwhash_decimal(value: &str, context: crate::ErrorContext) -> Result<u32, Error> {
618    if value.is_empty()
619        || !value.bytes().all(|byte| byte.is_ascii_digit())
620        || (value.len() > 1 && value.starts_with('0'))
621    {
622        return Err(Error::invalid_encoding(context));
623    }
624    value
625        .parse::<u32>()
626        .map_err(|_| Error::invalid_encoding(context))
627}
628
629/// Verifies that `hashed_password` is valid for `password`, assuming the hashed
630/// password was encoded using `crypto_pwhash_str`.
631///
632/// Compatible with libsodium's `crypto_pwhash_str_verify`.
633///
634/// # Errors
635///
636/// Returns an error if `hashed_password` is malformed, uses unsupported
637/// parameters, or does not match `password`.
638#[cfg(any(feature = "base64", all(doc, not(doctest))))]
639#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
640pub fn crypto_pwhash_str_verify(hashed_password: &str, password: &[u8]) -> Result<(), Error> {
641    let pwhash = Pwhash::parse_encoded_pwhash(hashed_password)?;
642    let t_cost = pwhash.t_cost.ok_or(Error::missing_data(
643        crate::ErrorContext::PasswordHashTimeCost,
644    ))?;
645    let m_cost = pwhash.m_cost.ok_or(Error::missing_data(
646        crate::ErrorContext::PasswordHashMemoryCost,
647    ))?;
648    let parallelism = pwhash.parallelism.ok_or(Error::missing_data(
649        crate::ErrorContext::PasswordHashParallelism,
650    ))?;
651    let salt = pwhash
652        .salt
653        .ok_or(Error::missing_data(crate::ErrorContext::PasswordHashSalt))?;
654    let algorithm = pwhash.type_.ok_or(Error::missing_data(
655        crate::ErrorContext::PasswordHashAlgorithm,
656    ))?;
657    let expected_hash = pwhash
658        .pwhash
659        .ok_or(Error::missing_data(crate::ErrorContext::PasswordHash))?;
660
661    verify_pwhash_parts(
662        &expected_hash,
663        password,
664        &salt,
665        t_cost,
666        m_cost,
667        parallelism,
668        algorithm,
669    )
670}
671
672pub(crate) fn verify_pwhash_parts(
673    expected_hash: &[u8],
674    password: &[u8],
675    salt: &[u8],
676    t_cost: u32,
677    m_cost: u32,
678    parallelism: u32,
679    algorithm: PasswordHashAlgorithm,
680) -> Result<(), Error> {
681    let mut hash = Zeroizing::new(vec![0u8; expected_hash.len()]);
682    argon2_hash(
683        t_cost,
684        m_cost,
685        parallelism,
686        password,
687        salt,
688        None,
689        None,
690        &mut hash,
691        algorithm.into(),
692    )?;
693
694    verify_ct(hash.as_slice(), expected_hash)
695}
696
697/// Checks if the parameters for `hashed_password` match those passed to the
698/// function. Returns `false` if the parameters match, and `true` if the
699/// parameters are mismatched (requiring a rehash).
700///
701/// Compatible with libsodium's `crypto_pwhash_str_needs_rehash`.
702///
703/// # Errors
704///
705/// Returns an error if `hashed_password` is malformed or uses unsupported
706/// parameters.
707#[cfg(any(feature = "base64", all(doc, not(doctest))))]
708#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
709pub fn crypto_pwhash_str_needs_rehash(
710    hashed_password: &str,
711    opslimit: u64,
712    memlimit: usize,
713) -> Result<bool, Error> {
714    validate_length!(
715        max CRYPTO_PWHASH_STRBYTES - 1,
716        hashed_password.len(),
717        crate::ErrorContext::PasswordHash
718    );
719    let (t_cost, m_cost) = convert_costs_checked(opslimit, memlimit)?;
720    let pwhash = Pwhash::parse_encoded_pwhash(hashed_password)?;
721    let parsed_t_cost = pwhash.t_cost.ok_or(Error::missing_data(
722        crate::ErrorContext::PasswordHashTimeCost,
723    ))?;
724    let parsed_m_cost = pwhash.m_cost.ok_or(Error::missing_data(
725        crate::ErrorContext::PasswordHashMemoryCost,
726    ))?;
727
728    if t_cost != parsed_t_cost || m_cost != parsed_m_cost {
729        Ok(true)
730    } else {
731        Ok(false)
732    }
733}
734
735#[cfg(test)]
736mod tests {
737    #[cfg(feature = "base64")]
738    use alloc::borrow::ToOwned;
739
740    use super::*;
741
742    #[cfg(dryoc_native_tests)]
743    #[test]
744    fn test_crypto_pwhash() {
745        use crate::native_test_util::pwhash_argon2id;
746        use crate::rng::copy_randombytes;
747
748        crate::native_test_util::init();
749
750        let mut hash = [0u8; 32];
751        let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
752
753        copy_randombytes(&mut salt);
754
755        let password = b"donkey kong";
756
757        crypto_pwhash(
758            &mut hash,
759            password,
760            &salt,
761            CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
762            CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
763            PasswordHashAlgorithm::Argon2id13,
764        )
765        .expect("pwhash failed");
766
767        let so_hash: [u8; 32] = pwhash_argon2id(
768            password,
769            &salt,
770            libsodium_sys::crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE.into(),
771            libsodium_sys::crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE as usize,
772        );
773
774        assert_eq!(hash, so_hash);
775    }
776
777    #[cfg(feature = "base64")]
778    #[test]
779    fn test_base64_no_pad_matches_base64_crate() {
780        use base64::Engine as _;
781        use base64::engine::general_purpose;
782
783        for len in 0..128 {
784            let input: Vec<u8> = (0..len).map(|i| (i * 31 + len) as u8).collect();
785            let encoded = base64_no_pad_encode(&input);
786            assert_eq!(encoded, general_purpose::STANDARD_NO_PAD.encode(&input));
787            assert_eq!(
788                base64_no_pad_decode(&encoded).as_deref(),
789                Some(input.as_slice())
790            );
791        }
792
793        assert_eq!(base64_no_pad_decode("A"), None);
794        assert_eq!(base64_no_pad_decode("AA="), None);
795        assert_eq!(base64_no_pad_decode("A/"), None);
796        assert_eq!(base64_no_pad_decode("AA/"), None);
797
798        let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
799        let hash = [0u8; STR_HASHBYTES];
800        let encoded = pwhash_to_string(
801            PasswordHashAlgorithm::Argon2id13,
802            2,
803            65_536,
804            1,
805            &salt,
806            &hash,
807        );
808        assert_eq!(
809            pwhash_string_len(
810                PasswordHashAlgorithm::Argon2id13,
811                2,
812                65_536,
813                1,
814                salt.len(),
815                hash.len(),
816            ),
817            Some(encoded.len())
818        );
819
820        assert_eq!(
821            pwhash_string_len(
822                PasswordHashAlgorithm::Argon2id13,
823                2,
824                65_536,
825                1,
826                usize::MAX,
827                0
828            ),
829            None,
830        );
831        #[cfg(target_pointer_width = "32")]
832        assert_eq!(
833            pwhash_string_len(
834                PasswordHashAlgorithm::Argon2id13,
835                2,
836                65_536,
837                1,
838                3_221_225_471,
839                0,
840            ),
841            None,
842        );
843    }
844
845    #[cfg(feature = "base64")]
846    #[test]
847    fn malformed_password_hash_fields_have_structured_errors() {
848        const SALT: &str = "AAAAAAAAAAA";
849        const HASH: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
850        let cases = [
851            (
852                format!("$argon2wat$v=19$m=65536,t=2,p=1${SALT}${HASH}"),
853                crate::ErrorContext::PasswordHashAlgorithm,
854            ),
855            (
856                format!("$argon2id$v=nope$m=65536,t=2,p=1${SALT}${HASH}"),
857                crate::ErrorContext::PasswordHashVersion,
858            ),
859            (
860                format!("$argon2id$v=19$m=nope,t=2,p=1${SALT}${HASH}"),
861                crate::ErrorContext::PasswordHashMemoryCost,
862            ),
863            (
864                format!("$argon2id$v=19$m=65536,t=nope,p=1${SALT}${HASH}"),
865                crate::ErrorContext::PasswordHashTimeCost,
866            ),
867            (
868                format!("$argon2id$v=19$m=65536,t=2,p=nope${SALT}${HASH}"),
869                crate::ErrorContext::PasswordHashParallelism,
870            ),
871            (
872                format!("$argon2id$v=19$m=65536,t=2,p=1$A${HASH}"),
873                crate::ErrorContext::PasswordHashSalt,
874            ),
875            (
876                format!("$argon2id$v=19$m=65536,t=2,p=1${SALT}$A"),
877                crate::ErrorContext::PasswordHash,
878            ),
879            (
880                format!("$argon2id$v=18$m=65536,t=2,p=1${SALT}${HASH}"),
881                crate::ErrorContext::PasswordHashVersion,
882            ),
883            (
884                format!("$argon2id$v=019$m=65536,t=2,p=1${SALT}${HASH}"),
885                crate::ErrorContext::PasswordHashVersion,
886            ),
887            (
888                format!("$argon2id$v=19$m=065536,t=2,p=1${SALT}${HASH}"),
889                crate::ErrorContext::PasswordHashMemoryCost,
890            ),
891            (
892                format!("$argon2id$v=19$m=65536,t=+2,p=1${SALT}${HASH}"),
893                crate::ErrorContext::PasswordHashTimeCost,
894            ),
895            (
896                format!("$argon2id$v=19$m=65536,t=2,p=01${SALT}${HASH}"),
897                crate::ErrorContext::PasswordHashParallelism,
898            ),
899        ];
900
901        for (encoded, expected_context) in cases {
902            let error = match Pwhash::parse_encoded_pwhash(&encoded) {
903                Ok(_) => panic!("the malformed field should be rejected"),
904                Err(error) => error,
905            };
906            assert!(matches!(
907                error,
908                Error::InvalidEncoding { context } if context == expected_context
909            ));
910        }
911
912        let missing_hash = format!("$argon2id$v=19$m=65536,t=2,p=1${SALT}");
913        assert!(matches!(
914            Pwhash::parse_encoded_pwhash(&missing_hash),
915            Err(Error::MissingData {
916                context: crate::ErrorContext::PasswordHash,
917            })
918        ));
919
920        let missing_algorithm = format!("$v=19$m=65536,t=2,p=1${SALT}${HASH}");
921        assert!(matches!(
922            Pwhash::parse_encoded_pwhash(&missing_algorithm),
923            Err(Error::MissingData {
924                context: crate::ErrorContext::PasswordHashAlgorithm,
925            })
926        ));
927    }
928
929    #[test]
930    fn password_hashing_reports_invalid_resource_limits() {
931        let mut output = [0u8; CRYPTO_PWHASH_BYTES_MIN];
932        let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
933        let password = b"password";
934
935        for (opslimit, memlimit, expected_context) in [
936            (
937                CRYPTO_PWHASH_OPSLIMIT_MIN - 1,
938                CRYPTO_PWHASH_MEMLIMIT_MIN,
939                crate::ErrorContext::OperationsLimit,
940            ),
941            (
942                CRYPTO_PWHASH_OPSLIMIT_MIN,
943                CRYPTO_PWHASH_MEMLIMIT_MIN - 1,
944                crate::ErrorContext::MemoryLimit,
945            ),
946        ] {
947            let error = crypto_pwhash(
948                &mut output,
949                password,
950                &salt,
951                opslimit,
952                memlimit,
953                PasswordHashAlgorithm::Argon2id13,
954            )
955            .expect_err("invalid resource limits should fail");
956            assert!(matches!(
957                error,
958                Error::InvalidValue { context, .. } if context == expected_context
959            ));
960
961            #[cfg(feature = "base64")]
962            {
963                let error = crypto_pwhash_str(password, opslimit, memlimit)
964                    .expect_err("invalid resource limits should fail");
965                assert!(matches!(
966                    error,
967                    Error::InvalidValue { context, .. } if context == expected_context
968                ));
969            }
970        }
971    }
972
973    /// Every classic-API validation failure happens before Argon2 sees the
974    /// caller's output buffer.
975    #[test]
976    fn invalid_parameters_leave_output_unchanged() {
977        let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
978        let password = b"password";
979        let sentinel = [0xa5u8; CRYPTO_PWHASH_BYTES_MIN];
980
981        let mut too_short = [0xa5u8; CRYPTO_PWHASH_BYTES_MIN - 1];
982        assert!(
983            crypto_pwhash(
984                &mut too_short,
985                password,
986                &salt,
987                CRYPTO_PWHASH_OPSLIMIT_MIN,
988                CRYPTO_PWHASH_MEMLIMIT_MIN,
989                PasswordHashAlgorithm::Argon2id13,
990            )
991            .is_err()
992        );
993        assert_eq!(too_short, [0xa5u8; CRYPTO_PWHASH_BYTES_MIN - 1]);
994
995        for case in [0usize, 1, 2, 3] {
996            let mut output = sentinel;
997            let result = match case {
998                0 => crypto_pwhash(
999                    &mut output,
1000                    password,
1001                    &salt[..CRYPTO_PWHASH_SALTBYTES - 1],
1002                    CRYPTO_PWHASH_OPSLIMIT_MIN,
1003                    CRYPTO_PWHASH_MEMLIMIT_MIN,
1004                    PasswordHashAlgorithm::Argon2id13,
1005                ),
1006                1 => crypto_pwhash(
1007                    &mut output,
1008                    password,
1009                    &salt,
1010                    CRYPTO_PWHASH_OPSLIMIT_MIN - 1,
1011                    CRYPTO_PWHASH_MEMLIMIT_MIN,
1012                    PasswordHashAlgorithm::Argon2id13,
1013                ),
1014                2 => crypto_pwhash(
1015                    &mut output,
1016                    password,
1017                    &salt,
1018                    CRYPTO_PWHASH_OPSLIMIT_MIN,
1019                    CRYPTO_PWHASH_MEMLIMIT_MIN - 1,
1020                    PasswordHashAlgorithm::Argon2id13,
1021                ),
1022                _ => crypto_pwhash(
1023                    &mut output,
1024                    password,
1025                    &salt,
1026                    CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN - 1,
1027                    CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MIN,
1028                    PasswordHashAlgorithm::Argon2i13,
1029                ),
1030            };
1031            assert!(result.is_err(), "case {case}");
1032            assert_eq!(output, sentinel, "case {case}");
1033        }
1034    }
1035    #[test]
1036    fn password_hashing_enforces_classic_parameter_contract() {
1037        let mut output = [0u8; CRYPTO_PWHASH_BYTES_MIN];
1038        let salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
1039
1040        for opslimit in 1..CRYPTO_PWHASH_ARGON2I_OPSLIMIT_MIN {
1041            assert!(matches!(
1042                crypto_pwhash(
1043                    &mut output,
1044                    b"password",
1045                    &salt,
1046                    opslimit,
1047                    CRYPTO_PWHASH_ARGON2I_MEMLIMIT_MIN,
1048                    PasswordHashAlgorithm::Argon2i13,
1049                ),
1050                Err(Error::InvalidValue {
1051                    context: crate::ErrorContext::OperationsLimit,
1052                    ..
1053                })
1054            ));
1055        }
1056
1057        assert!(matches!(
1058            crypto_pwhash(
1059                &mut output,
1060                b"password",
1061                &salt[..CRYPTO_PWHASH_SALTBYTES - 1],
1062                CRYPTO_PWHASH_OPSLIMIT_MIN,
1063                CRYPTO_PWHASH_MEMLIMIT_MIN,
1064                PasswordHashAlgorithm::Argon2id13,
1065            ),
1066            Err(Error::InvalidLength {
1067                context: crate::ErrorContext::PasswordHashSalt,
1068                constraint: crate::LengthConstraint::Exact(CRYPTO_PWHASH_SALTBYTES),
1069                ..
1070            })
1071        ));
1072
1073        assert!(PasswordHashAlgorithm::try_from(0).is_err());
1074        assert_eq!(
1075            PasswordHashAlgorithm::try_from(CRYPTO_PWHASH_ALG_ARGON2ID13 as u32)
1076                .expect("valid algorithm"),
1077            PasswordHashAlgorithm::Argon2id13
1078        );
1079    }
1080
1081    #[cfg(dryoc_native_tests)]
1082    #[cfg(feature = "base64")]
1083    #[test]
1084    fn test_crypto_pwhash_str() {
1085        use crate::native_test_util::pwhash_argon2id_str_verify;
1086
1087        let password = b"donkey kong";
1088
1089        let pwhash = crypto_pwhash_str(
1090            password,
1091            CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1092            CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
1093        )
1094        .expect("pwhash failed");
1095        let pwhash2 = crypto_pwhash_str(
1096            password,
1097            CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1098            CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
1099        )
1100        .expect("pwhash failed");
1101
1102        let parsed = Pwhash::parse_encoded_pwhash(&pwhash).expect("couldn't parse pwhash");
1103        let parsed2 = Pwhash::parse_encoded_pwhash(&pwhash2).expect("couldn't parse pwhash");
1104
1105        assert_ne!(
1106            parsed.salt.as_ref().expect("missing salt"),
1107            &vec![0u8; CRYPTO_PWHASH_SALTBYTES]
1108        );
1109        assert_ne!(parsed.salt, parsed2.salt);
1110
1111        let mut pwhash_bytes = [0u8; CRYPTO_PWHASH_STRBYTES];
1112        pwhash_bytes[..pwhash.len()].copy_from_slice(pwhash.as_bytes());
1113
1114        assert!(pwhash_argon2id_str_verify(&pwhash_bytes, password));
1115
1116        let argon2i = crypto_pwhash_str_alg(
1117            password,
1118            CRYPTO_PWHASH_ARGON2I_OPSLIMIT_INTERACTIVE,
1119            CRYPTO_PWHASH_ARGON2I_MEMLIMIT_INTERACTIVE,
1120            PasswordHashAlgorithm::Argon2i13,
1121        )
1122        .expect("argon2i pwhash failed");
1123        assert!(argon2i.starts_with(CRYPTO_PWHASH_ARGON2I_STRPREFIX));
1124        crypto_pwhash_str_verify(&argon2i, password).expect("argon2i verify failed");
1125    }
1126
1127    #[cfg(dryoc_native_tests)]
1128    #[cfg(feature = "base64")]
1129    #[test]
1130    fn test_crypto_pwhash_str_verify() {
1131        use crate::native_test_util::pwhash_argon2id_str;
1132
1133        crate::native_test_util::init();
1134
1135        let password = b"donkey kong";
1136
1137        let pwhash = pwhash_argon2id_str(
1138            password,
1139            libsodium_sys::crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE.into(),
1140            libsodium_sys::crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE as usize,
1141        );
1142
1143        let pw_str = core::str::from_utf8(&pwhash)
1144            .expect("from ut8 failed")
1145            .trim_end_matches('\x00');
1146
1147        crypto_pwhash_str_verify(pw_str, password).expect("verify failed");
1148        crypto_pwhash_str_verify(pw_str, b"invalid password")
1149            .expect_err("verify should have failed");
1150
1151        for encoded in [
1152            concat!(
1153                "$argon2id$v=19$m=256,t=3,p=1$MDEyMzQ1Njc$",
1154                "G5ajKFCoUzaXRLdz7UJb5wGkb2Xt+X5/GQjUYtS2+TE",
1155            ),
1156            concat!(
1157                "$argon2i$v=19$m=4096,t=3,p=2$b2RpZHVlamRpc29kaXNrdw$",
1158                "TNnWIwlu1061JHrnCqIAmjs3huSxYIU+0jWipu7Kc9M",
1159            ),
1160        ] {
1161            crypto_pwhash_str_verify(encoded, b"password")
1162                .expect("valid libsodium Argon2 vector should verify");
1163        }
1164
1165        assert!(crypto_pwhash_str_verify(&format!("{pw_str}$garbage"), password).is_err());
1166        assert!(crypto_pwhash_str_verify(pw_str.trim_start_matches('$'), password).is_err());
1167        for invalid_parallelism in ["0", "4294967295"] {
1168            let malformed = pw_str.replace(",p=1", &format!(",p={invalid_parallelism}"));
1169            assert!(crypto_pwhash_str_verify(&malformed, password).is_err());
1170            assert!(
1171                crypto_pwhash_str_needs_rehash(
1172                    &malformed,
1173                    CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1174                    CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
1175                )
1176                .is_err()
1177            );
1178        }
1179
1180        // should be false
1181        assert!(
1182            !crypto_pwhash_str_needs_rehash(
1183                pw_str,
1184                CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
1185                CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
1186            )
1187            .expect("verify rehash failed")
1188        );
1189
1190        // should be true
1191        assert!(
1192            crypto_pwhash_str_needs_rehash(
1193                pw_str,
1194                CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE + 1,
1195                CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
1196            )
1197            .expect("verify rehash failed")
1198        );
1199
1200        assert!(
1201            crypto_pwhash_str_needs_rehash(pw_str, 0, 0,)
1202                .expect("zero costs are a valid rehash comparison")
1203        );
1204
1205        assert!(matches!(
1206            crypto_pwhash_str_needs_rehash(pw_str, u32::MAX as u64 + 1, 0),
1207            Err(Error::InvalidValue {
1208                context: crate::ErrorContext::OperationsLimit,
1209                ..
1210            })
1211        ));
1212    }
1213
1214    #[cfg(feature = "base64")]
1215    const FIXED_PASSWORD_HASH: &str = concat!(
1216        "$argon2id$v=19$m=8,t=1,p=1$9ekTPbzKHDiIpMlbQaGlhw$",
1217        "hNvEox1vwA2JfhZp1wZR15ZgoNTLZbWZo6XJcN6naXw",
1218    );
1219    #[cfg(feature = "base64")]
1220    const FIXED_PASSWORD_HASH_M16: &str = concat!(
1221        "$argon2id$v=19$m=16,t=1,p=1$s1CQM+Z7bXo6yOSiVIBY8A$",
1222        "Qx7ztSAZoE3T52niRU2LRwiV4hVtE7XHI+GxwYep4D0",
1223    );
1224
1225    #[cfg(feature = "base64")]
1226    fn exact_max_password_hash() -> String {
1227        let salt = [0x42u8; 8];
1228        let mut hash = [0u8; 66];
1229        argon2_hash(
1230            1,
1231            8,
1232            1,
1233            b"password",
1234            &salt,
1235            None,
1236            None,
1237            &mut hash,
1238            PasswordHashAlgorithm::Argon2id13.into(),
1239        )
1240        .expect("argon2");
1241        let encoded = pwhash_to_string(PasswordHashAlgorithm::Argon2id13, 1, 8, 1, &salt, &hash);
1242        assert_eq!(encoded.len(), CRYPTO_PWHASH_STRBYTES - 1);
1243        encoded
1244    }
1245
1246    #[cfg(feature = "base64")]
1247    fn mutated_password_hashes() -> Vec<(&'static str, String, bool)> {
1248        let valid = FIXED_PASSWORD_HASH;
1249        let mut cases = Vec::new();
1250        for (i, _) in valid.match_indices('$') {
1251            cases.push(("truncated before $", valid[..i].to_owned(), false));
1252            cases.push(("truncated after $", valid[..=i].to_owned(), false));
1253        }
1254        cases.extend([
1255            (
1256                "wrong prefix",
1257                valid.replacen("$argon2id$", "$argon2wat$", 1),
1258                false,
1259            ),
1260            ("extra field", format!("{valid}$extra"), false),
1261            (
1262                "non-canonical salt pad bits",
1263                valid.replace("9ekTPbzKHDiIpMlbQaGlhw", "9ekTPbzKHDiIpMlbQaGlhx"),
1264                false,
1265            ),
1266            (
1267                "non-canonical hash pad bits",
1268                format!("{}x", &valid[..valid.len() - 1]),
1269                false,
1270            ),
1271            ("m overflow", valid.replace("m=8", "m=4294967296"), false),
1272            ("t overflow", valid.replace("t=1", "t=4294967296"), false),
1273            ("p overflow", valid.replace("p=1", "p=4294967296"), false),
1274            ("m below one KiB block", valid.replace("m=8", "m=7"), false),
1275        ]);
1276        cases
1277    }
1278
1279    /// Pure parser, base64 and validation coverage: this test has no native
1280    /// dependency, so it also compiles for wasm.
1281    #[cfg(feature = "base64")]
1282    #[test]
1283    fn fixed_and_mutated_password_hashes_have_expected_results() {
1284        Pwhash::parse_encoded_pwhash(FIXED_PASSWORD_HASH).expect("fixed string parses");
1285        crypto_pwhash_str_verify(FIXED_PASSWORD_HASH, b"password").expect("fixed string verifies");
1286        crypto_pwhash_str_verify(FIXED_PASSWORD_HASH, b"wrong").expect_err("wrong password");
1287        assert!(!crypto_pwhash_str_needs_rehash(FIXED_PASSWORD_HASH, 1, 8192).expect("rehash"));
1288        assert!(crypto_pwhash_str_needs_rehash(FIXED_PASSWORD_HASH, 2, 8192).expect("rehash"));
1289
1290        for (name, encoded, valid) in mutated_password_hashes() {
1291            assert_eq!(
1292                Pwhash::parse_encoded_pwhash(&encoded).is_ok(),
1293                valid,
1294                "parse: {name}"
1295            );
1296            assert!(
1297                crypto_pwhash_str_verify(&encoded, b"password").is_err(),
1298                "verify: {name}"
1299            );
1300            assert!(
1301                crypto_pwhash_str_needs_rehash(&encoded, 1, 8192).is_err(),
1302                "needs_rehash: {name}"
1303            );
1304        }
1305
1306        // Parsing and verification accept long hashes, unlike generation,
1307        // which writes into libsodium's fixed-size output buffer.
1308        let salt = [0x5au8; 16];
1309        let mut hash = [0u8; 64];
1310        argon2_hash(
1311            1,
1312            8,
1313            1,
1314            b"password",
1315            &salt,
1316            None,
1317            None,
1318            &mut hash,
1319            PasswordHashAlgorithm::Argon2id13.into(),
1320        )
1321        .expect("derive long hash");
1322        let long = pwhash_to_string(PasswordHashAlgorithm::Argon2id13, 1, 8, 1, &salt, &hash);
1323        assert_eq!(long.len(), 136);
1324        Pwhash::parse_encoded_pwhash(&long).expect("long hash parses");
1325        crypto_pwhash_str_verify(&long, b"password").expect("long hash verifies");
1326        assert!(crypto_pwhash_str_needs_rehash(&long, 1, 8192).is_err());
1327        let encoded = exact_max_password_hash();
1328        Pwhash::parse_encoded_pwhash(&encoded).expect("max string parses");
1329        crypto_pwhash_str_verify(&encoded, b"password").expect("max string verifies");
1330        assert!(!crypto_pwhash_str_needs_rehash(&encoded, 1, 8192).expect("rehash"));
1331        let argon2i = FIXED_PASSWORD_HASH.replacen("$argon2id$", "$argon2i$", 1);
1332
1333        Pwhash::parse_encoded_pwhash(&argon2i).expect("algorithm mutation parses");
1334        assert!(crypto_pwhash_str_verify(&argon2i, b"password").is_err());
1335        assert!(!crypto_pwhash_str_needs_rehash(&argon2i, 1, 8192).expect("rehash"));
1336
1337        let parallel = FIXED_PASSWORD_HASH_M16.replace("p=1", "p=2");
1338        Pwhash::parse_encoded_pwhash(&parallel).expect("parallelism mutation parses");
1339        assert!(crypto_pwhash_str_verify(&parallel, b"password").is_err());
1340        assert!(!crypto_pwhash_str_needs_rehash(&parallel, 1, 16_384).expect("rehash"));
1341    }
1342
1343    /// Invalid parser/verify/rehash results in the mutation matrix agree with
1344    /// libsodium, including canonical pad bits and oversized rehash input.
1345    #[cfg(all(feature = "base64", dryoc_native_tests))]
1346    #[test]
1347    fn mutation_matrix_matches_libsodium() {
1348        use crate::native_test_util::{pwhash_str_needs_rehash, pwhash_str_verify};
1349
1350        for (name, encoded, _) in mutated_password_hashes() {
1351            let sodium_verify = pwhash_str_verify(&encoded, b"password");
1352            let sodium_rehash = pwhash_str_needs_rehash(&encoded, 1, 8192);
1353            assert_eq!(
1354                crypto_pwhash_str_verify(&encoded, b"password").is_ok(),
1355                sodium_verify,
1356                "verify: {name}"
1357            );
1358            let ours_rehash = crypto_pwhash_str_needs_rehash(&encoded, 1, 8192);
1359            assert_eq!(
1360                ours_rehash.is_err(),
1361                sodium_rehash.is_err(),
1362                "rehash validity: {name}"
1363            );
1364            if let Ok(ours_rehash) = ours_rehash {
1365                assert_eq!(
1366                    ours_rehash,
1367                    sodium_rehash == Ok(true),
1368                    "rehash result: {name}"
1369                );
1370            }
1371        }
1372    }
1373
1374    #[cfg(all(feature = "base64", dryoc_native_tests))]
1375    #[test]
1376    fn algorithm_and_parallelism_mutations_match_libsodium() {
1377        use crate::native_test_util::{pwhash_str_needs_rehash, pwhash_str_verify};
1378
1379        for (encoded, memlimit) in [
1380            (
1381                FIXED_PASSWORD_HASH.replacen("$argon2id$", "$argon2i$", 1),
1382                8192,
1383            ),
1384            (FIXED_PASSWORD_HASH_M16.replace("p=1", "p=2"), 16_384),
1385        ] {
1386            assert_eq!(
1387                crypto_pwhash_str_verify(&encoded, b"password").is_ok(),
1388                pwhash_str_verify(&encoded, b"password")
1389            );
1390            assert_eq!(
1391                crypto_pwhash_str_needs_rehash(&encoded, 1, memlimit).expect("rehash"),
1392                pwhash_str_needs_rehash(&encoded, 1, memlimit) == Ok(true)
1393            );
1394        }
1395    }
1396    /// The longest encodable string (`CRYPTO_PWHASH_STRBYTES - 1` bytes, a
1397    /// 66-byte hash) verifies and reports `needs_rehash` exactly as libsodium.
1398    #[cfg(all(feature = "base64", dryoc_native_tests))]
1399    #[test]
1400    fn exact_maximum_encoded_length_matches_libsodium() {
1401        use crate::native_test_util::{pwhash_str_needs_rehash, pwhash_str_verify};
1402
1403        let encoded = exact_max_password_hash();
1404        // Both must accept: an `Err == -1` agreement would hide a broken hash.
1405        // libsodium's verify recomputes Argon2id from the decoded 8-byte salt
1406        // and compares all 66 bytes, so this is also the raw-hash parity check
1407        // (the raw `crypto_pwhash` API expects a fixed 16-byte salt, so it
1408        // cannot reproduce this string's parameters directly).
1409        crypto_pwhash_str_verify(&encoded, b"password").expect("dryoc verifies");
1410        assert!(
1411            pwhash_str_verify(&encoded, b"password"),
1412            "libsodium verifies the 66-byte hash"
1413        );
1414        assert_eq!(
1415            crypto_pwhash_str_needs_rehash(&encoded, 1, 8192).expect("rehash"),
1416            pwhash_str_needs_rehash(&encoded, 1, 8192) == Ok(true)
1417        );
1418    }
1419
1420    /// libsodium verifies encoded hashes longer than its generation buffer.
1421    #[cfg(all(feature = "base64", dryoc_native_tests))]
1422    #[test]
1423    fn longer_encoded_hash_verifies_like_libsodium() {
1424        let password = b"long hash test password";
1425        let salt = [0x5au8; 16];
1426        let hash = crate::native_test_util::pwhash_argon2id::<64>(password, &salt, 1, 8192);
1427        let encoded = pwhash_to_string(PasswordHashAlgorithm::Argon2id13, 1, 8, 1, &salt, &hash);
1428        assert_eq!(encoded.len(), 136);
1429
1430        let parsed = Pwhash::parse_encoded_pwhash(&encoded).expect("long hash parses");
1431        assert_eq!(parsed.pwhash.as_deref(), Some(hash.as_slice()));
1432        crypto_pwhash_str_verify(&encoded, password).expect("dryoc verifies long hash");
1433
1434        assert!(
1435            crate::native_test_util::pwhash_str_verify(&encoded, password),
1436            "libsodium verifies the 64-byte hash"
1437        );
1438    }
1439}