Skip to main content

oxicrypto_kdf/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Pure Rust KDF implementations for the OxiCrypto stack.
4//!
5//! | Function | Module | Backend |
6//! |----------|--------|---------|
7//! | HKDF-SHA-256 / SHA-512 | (inline) | `hkdf` |
8//! | HKDF-Expand-Label (TLS 1.3 / QUIC) | [`hkdf_label`] | `hkdf` |
9//! | PBKDF2-SHA-256 / SHA-512 | [`pbkdf2_kdf`] | `pbkdf2` |
10//! | Argon2id | [`argon2_kdf`] | `argon2` |
11//! | scrypt | [`scrypt_kdf`] | `scrypt` |
12//! | Balloon (SHA-256 / SHA-512) | [`balloon`] | `sha2` |
13
14pub mod argon2_kdf;
15pub mod balloon;
16pub mod hkdf_label;
17pub mod kbkdf;
18pub mod pbkdf2_kdf;
19pub mod scrypt_kdf;
20pub mod stretcher;
21
22// ── OWASP 2023 minimum iteration counts ───────────────────────────────────────
23
24/// OWASP 2023 Password Storage Cheat Sheet minimum iteration count for
25/// PBKDF2-HMAC-SHA-256.
26///
27/// Reference: <https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html>
28pub const PBKDF2_SHA256_MIN_ITERATIONS: u32 = 600_000;
29
30/// OWASP 2023 Password Storage Cheat Sheet minimum iteration count for
31/// PBKDF2-HMAC-SHA-512.
32///
33/// SHA-512 is ~2× faster than SHA-256 per round on 64-bit CPUs, so the
34/// equivalent minimum is approximately 210,000.
35pub const PBKDF2_SHA512_MIN_ITERATIONS: u32 = 210_000;
36
37pub use argon2_kdf::{
38    argon2d_derive, argon2i_derive, argon2id_derive, argon2id_to_phc_string, argon2id_verify_phc,
39    Argon2Params, Argon2idHasher,
40};
41pub use balloon::{
42    balloon_sha256, balloon_sha256_secret, balloon_sha512, balloon_sha512_secret, BalloonHasher,
43    BalloonParams, BalloonVariant, BALLOON_DELTA,
44};
45pub use hkdf_label::{hkdf_expand_label_sha256, hkdf_expand_label_sha384};
46pub use kbkdf::{
47    kbkdf_counter_hmac_sha256, kbkdf_counter_hmac_sha256_secret, kbkdf_counter_hmac_sha384,
48    kbkdf_counter_hmac_sha512,
49};
50pub use pbkdf2_kdf::{
51    pbkdf2_sha256, pbkdf2_sha512, Pbkdf2Params, Pbkdf2Sha256Hasher, Pbkdf2Sha512Hasher,
52};
53pub use scrypt_kdf::{scrypt_derive, ScryptHasher, ScryptParams};
54pub use stretcher::{
55    Argon2idStretchParams, BalloonStretchParams, KeyStretcher, Pbkdf2StretchParams,
56    ScryptStretchParams, StretchParams, Stretcher,
57};
58
59use hkdf::Hkdf;
60use oxicrypto_core::{CryptoError, Kdf, PasswordHash};
61use subtle::ConstantTimeEq;
62
63// ── HKDF-SHA-256 ──────────────────────────────────────────────────────────────
64
65/// HKDF-SHA-256 key derivation function.
66#[derive(Debug, Default, Clone, Copy)]
67pub struct HkdfSha256;
68
69impl Kdf for HkdfSha256 {
70    fn name(&self) -> &'static str {
71        "HKDF-SHA-256"
72    }
73    fn derive(
74        &self,
75        ikm: &[u8],
76        salt: &[u8],
77        info: &[u8],
78        okm_out: &mut [u8],
79    ) -> Result<(), CryptoError> {
80        if okm_out.is_empty() {
81            return Err(CryptoError::BadInput);
82        }
83        let salt_opt = if salt.is_empty() { None } else { Some(salt) };
84        let hk = Hkdf::<sha2::Sha256>::new(salt_opt, ikm);
85        hk.expand(info, okm_out)
86            .map_err(|_| CryptoError::Internal("HKDF expand failed (output too long)"))?;
87        Ok(())
88    }
89}
90
91// ── HKDF-SHA-512 ──────────────────────────────────────────────────────────────
92
93/// HKDF-SHA-512 key derivation function.
94#[derive(Debug, Default, Clone, Copy)]
95pub struct HkdfSha512;
96
97impl Kdf for HkdfSha512 {
98    fn name(&self) -> &'static str {
99        "HKDF-SHA-512"
100    }
101    fn derive(
102        &self,
103        ikm: &[u8],
104        salt: &[u8],
105        info: &[u8],
106        okm_out: &mut [u8],
107    ) -> Result<(), CryptoError> {
108        if okm_out.is_empty() {
109            return Err(CryptoError::BadInput);
110        }
111        let salt_opt = if salt.is_empty() { None } else { Some(salt) };
112        let hk = Hkdf::<sha2::Sha512>::new(salt_opt, ikm);
113        hk.expand(info, okm_out)
114            .map_err(|_| CryptoError::Internal("HKDF expand failed (output too long)"))?;
115        Ok(())
116    }
117}
118
119// ── HKDF-SHA-384 ──────────────────────────────────────────────────────────────
120
121/// HKDF-SHA-384 key derivation function.
122#[derive(Debug, Default, Clone, Copy)]
123pub struct HkdfSha384;
124
125impl Kdf for HkdfSha384 {
126    fn name(&self) -> &'static str {
127        "HKDF-SHA-384"
128    }
129    fn derive(
130        &self,
131        ikm: &[u8],
132        salt: &[u8],
133        info: &[u8],
134        okm_out: &mut [u8],
135    ) -> Result<(), CryptoError> {
136        if okm_out.is_empty() {
137            return Err(CryptoError::BadInput);
138        }
139        let salt_opt = if salt.is_empty() { None } else { Some(salt) };
140        let hk = Hkdf::<sha2::Sha384>::new(salt_opt, ikm);
141        hk.expand(info, okm_out)
142            .map_err(|_| CryptoError::Internal("HKDF-SHA-384 expand failed (output too long)"))?;
143        Ok(())
144    }
145}
146
147// ── HKDF Extract-only / Expand-only (RFC 5869 separated phases) ─────────────
148
149/// Perform HKDF-Extract with SHA-256, returning the pseudorandom key (PRK).
150///
151/// This is the extraction phase only (RFC 5869 Section 2.2).
152/// The PRK is always 32 bytes (the output size of SHA-256).
153///
154/// Used by protocols like TLS 1.3 that need separated extract/expand.
155#[must_use]
156pub fn hkdf_sha256_extract(salt: &[u8], ikm: &[u8]) -> [u8; 32] {
157    let salt_opt = if salt.is_empty() { None } else { Some(salt) };
158    let (prk, _) = Hkdf::<sha2::Sha256>::extract(salt_opt, ikm);
159    let mut out = [0u8; 32];
160    out.copy_from_slice(&prk);
161    out
162}
163
164/// Perform HKDF-Expand with SHA-256 from a pre-extracted PRK.
165///
166/// This is the expansion phase only (RFC 5869 Section 2.3).
167/// `prk` should be the output of [`hkdf_sha256_extract`] (32 bytes).
168#[must_use = "HKDF expand result must be checked"]
169pub fn hkdf_sha256_expand(prk: &[u8], info: &[u8], okm_out: &mut [u8]) -> Result<(), CryptoError> {
170    if okm_out.is_empty() {
171        return Err(CryptoError::BadInput);
172    }
173    let hk = Hkdf::<sha2::Sha256>::from_prk(prk).map_err(|_| CryptoError::InvalidKey)?;
174    hk.expand(info, okm_out)
175        .map_err(|_| CryptoError::Internal("HKDF-SHA-256 expand failed (output too long)"))?;
176    Ok(())
177}
178
179/// Perform HKDF-Extract with SHA-384, returning the pseudorandom key (PRK).
180///
181/// The PRK is always 48 bytes (the output size of SHA-384).
182#[must_use]
183pub fn hkdf_sha384_extract(salt: &[u8], ikm: &[u8]) -> [u8; 48] {
184    let salt_opt = if salt.is_empty() { None } else { Some(salt) };
185    let (prk, _) = Hkdf::<sha2::Sha384>::extract(salt_opt, ikm);
186    let mut out = [0u8; 48];
187    out.copy_from_slice(&prk);
188    out
189}
190
191/// Perform HKDF-Expand with SHA-384 from a pre-extracted PRK.
192#[must_use = "HKDF expand result must be checked"]
193pub fn hkdf_sha384_expand(prk: &[u8], info: &[u8], okm_out: &mut [u8]) -> Result<(), CryptoError> {
194    if okm_out.is_empty() {
195        return Err(CryptoError::BadInput);
196    }
197    let hk = Hkdf::<sha2::Sha384>::from_prk(prk).map_err(|_| CryptoError::InvalidKey)?;
198    hk.expand(info, okm_out)
199        .map_err(|_| CryptoError::Internal("HKDF-SHA-384 expand failed (output too long)"))?;
200    Ok(())
201}
202
203/// Perform HKDF-Extract with SHA-512, returning the pseudorandom key (PRK).
204///
205/// The PRK is always 64 bytes (the output size of SHA-512).
206#[must_use]
207pub fn hkdf_sha512_extract(salt: &[u8], ikm: &[u8]) -> [u8; 64] {
208    let salt_opt = if salt.is_empty() { None } else { Some(salt) };
209    let (prk, _) = Hkdf::<sha2::Sha512>::extract(salt_opt, ikm);
210    let mut out = [0u8; 64];
211    out.copy_from_slice(&prk);
212    out
213}
214
215/// Perform HKDF-Expand with SHA-512 from a pre-extracted PRK.
216#[must_use = "HKDF expand result must be checked"]
217pub fn hkdf_sha512_expand(prk: &[u8], info: &[u8], okm_out: &mut [u8]) -> Result<(), CryptoError> {
218    if okm_out.is_empty() {
219        return Err(CryptoError::BadInput);
220    }
221    let hk = Hkdf::<sha2::Sha512>::from_prk(prk).map_err(|_| CryptoError::InvalidKey)?;
222    hk.expand(info, okm_out)
223        .map_err(|_| CryptoError::Internal("HKDF-SHA-512 expand failed (output too long)"))?;
224    Ok(())
225}
226
227// ── HKDF derive-to-Vec convenience wrappers ───────────────────────────────────
228
229/// Derive `len` bytes from `ikm`, `salt`, and `info` using HKDF-SHA-256, returning
230/// the output as an owned `Vec<u8>`.
231///
232/// This is a convenience wrapper around [`HkdfSha256::derive`] (which performs
233/// the full extract+expand sequence per RFC 5869).
234///
235/// # Errors
236/// Returns [`CryptoError::BadInput`] if `len == 0` or if the requested output
237/// exceeds 255 × 32 bytes (HKDF-SHA-256 maximum).
238#[must_use = "HKDF derive result must be checked"]
239pub fn hkdf_sha256_derive_to_vec(
240    ikm: &[u8],
241    salt: &[u8],
242    info: &[u8],
243    len: usize,
244) -> Result<Vec<u8>, CryptoError> {
245    if len == 0 {
246        return Err(CryptoError::BadInput);
247    }
248    let mut out = vec![0u8; len];
249    HkdfSha256.derive(ikm, salt, info, &mut out)?;
250    Ok(out)
251}
252
253/// Derive `len` bytes from `ikm`, `salt`, and `info` using HKDF-SHA-384, returning
254/// the output as an owned `Vec<u8>`.
255///
256/// # Errors
257/// Returns [`CryptoError::BadInput`] if `len == 0` or if the requested output
258/// exceeds 255 × 48 bytes (HKDF-SHA-384 maximum).
259#[must_use = "HKDF derive result must be checked"]
260pub fn hkdf_sha384_derive_to_vec(
261    ikm: &[u8],
262    salt: &[u8],
263    info: &[u8],
264    len: usize,
265) -> Result<Vec<u8>, CryptoError> {
266    if len == 0 {
267        return Err(CryptoError::BadInput);
268    }
269    let mut out = vec![0u8; len];
270    HkdfSha384.derive(ikm, salt, info, &mut out)?;
271    Ok(out)
272}
273
274/// Derive `len` bytes from `ikm`, `salt`, and `info` using HKDF-SHA-512, returning
275/// the output as an owned `Vec<u8>`.
276///
277/// # Errors
278/// Returns [`CryptoError::BadInput`] if `len == 0` or if the requested output
279/// exceeds 255 × 64 bytes (HKDF-SHA-512 maximum).
280#[must_use = "HKDF derive result must be checked"]
281pub fn hkdf_sha512_derive_to_vec(
282    ikm: &[u8],
283    salt: &[u8],
284    info: &[u8],
285    len: usize,
286) -> Result<Vec<u8>, CryptoError> {
287    if len == 0 {
288        return Err(CryptoError::BadInput);
289    }
290    let mut out = vec![0u8; len];
291    HkdfSha512.derive(ikm, salt, info, &mut out)?;
292    Ok(out)
293}
294
295// ── Salt generation helpers ────────────────────────────────────────────────────
296
297/// Generate a random 16-byte salt using the system CSPRNG.
298///
299/// Suitable for PBKDF2 (recommended ≥ 16 bytes per NIST SP 800-132) and
300/// Argon2id (requires ≥ 8 bytes per RFC 9106).
301///
302/// # Errors
303/// Returns [`CryptoError::Rng`] if the OS entropy source is unavailable.
304#[must_use = "generated salt result must be checked"]
305pub fn generate_salt_16() -> Result<[u8; 16], CryptoError> {
306    let bytes = oxicrypto_rand::random_bytes(16)?;
307    let mut out = [0u8; 16];
308    out.copy_from_slice(&bytes);
309    Ok(out)
310}
311
312/// Generate a random 32-byte salt using the system CSPRNG.
313///
314/// Suitable for Argon2id and scrypt where a longer salt provides additional
315/// domain separation.
316///
317/// # Errors
318/// Returns [`CryptoError::Rng`] if the OS entropy source is unavailable.
319#[must_use = "generated salt result must be checked"]
320pub fn generate_salt_32() -> Result<[u8; 32], CryptoError> {
321    let bytes = oxicrypto_rand::random_bytes(32)?;
322    let mut out = [0u8; 32];
323    out.copy_from_slice(&bytes);
324    Ok(out)
325}
326
327// ---------------------------------------------------------------------------
328// verify_password — constant-time password verification
329// ---------------------------------------------------------------------------
330
331/// Verify a password by re-hashing and comparing in constant time.
332///
333/// Hashes `password` with `salt` using `hasher` into a temporary buffer of
334/// `expected.len()` bytes, then compares the result to `expected` using
335/// [`subtle::ConstantTimeEq`].  The comparison time does not depend on the
336/// position of the first differing byte.
337///
338/// # Errors
339/// - Returns `Err(CryptoError::BadInput)` if `expected` is empty.
340/// - Returns the underlying [`CryptoError`] if hashing fails (e.g. bad salt length).
341/// - Returns `Err(CryptoError::InvalidTag)` if the password does not match.
342///
343/// # Example
344/// ```ignore
345/// use oxicrypto_kdf::{Argon2idHasher, Argon2Params, verify_password};
346///
347/// let hasher = Argon2idHasher::new(Argon2Params::TEST_PARAMS);
348/// let salt   = b"0123456789abcdef";
349/// let mut expected = [0u8; 32];
350/// hasher.hash_password(b"password", salt, &hasher.params, &mut expected).unwrap();
351///
352/// verify_password(&hasher, b"password", salt, &expected).unwrap();        // ok
353/// assert!(verify_password(&hasher, b"wrong", salt, &expected).is_err()); // rejected
354/// ```
355#[must_use = "password verification result must be checked"]
356pub fn verify_password<H>(
357    hasher: &H,
358    password: &[u8],
359    salt: &[u8],
360    expected: &[u8],
361) -> Result<(), CryptoError>
362where
363    H: PasswordHash,
364{
365    if expected.is_empty() {
366        return Err(CryptoError::BadInput);
367    }
368
369    // Allocate a stack-sized temporary buffer.  For passwords the expected
370    // output is typically 16–64 bytes, so heap allocation is not required;
371    // but we use a Vec here to support arbitrary output lengths.
372    let mut computed = vec![0u8; expected.len()];
373
374    // Use empty params — each concrete hasher uses its own stored params.
375    struct NullParams;
376    impl oxicrypto_core::PasswordHashParams for NullParams {
377        fn memory_cost(&self) -> Option<u32> {
378            None
379        }
380        fn time_cost(&self) -> Option<u32> {
381            None
382        }
383        fn parallelism(&self) -> Option<u32> {
384            None
385        }
386    }
387
388    hasher.hash_password(password, salt, &NullParams, &mut computed)?;
389
390    // Constant-time comparison: returns 0x01 iff equal.
391    let ok: bool = computed.ct_eq(expected).into();
392    if ok {
393        Ok(())
394    } else {
395        Err(CryptoError::InvalidTag)
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    fn hex_decode(s: &str) -> Vec<u8> {
404        (0..s.len())
405            .step_by(2)
406            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
407            .collect()
408    }
409
410    // RFC 5869 Test Case 1 for HKDF-SHA-256
411    // Hash = SHA-256
412    // IKM  = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b (22 bytes)
413    // salt = 0x000102030405060708090a0b0c (13 bytes)
414    // info = 0xf0f1f2f3f4f5f6f7f8f9 (10 bytes)
415    // L    = 42 bytes
416    // OKM  = 0x3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865
417    #[test]
418    fn hkdf_sha256_rfc5869_tc1() {
419        let ikm = hex_decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
420        let salt = hex_decode("000102030405060708090a0b0c");
421        let info = hex_decode("f0f1f2f3f4f5f6f7f8f9");
422        let expected = hex_decode(
423            "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
424        );
425
426        let kdf = HkdfSha256;
427        let mut okm = vec![0u8; 42];
428        kdf.derive(&ikm, &salt, &info, &mut okm)
429            .expect("HKDF-SHA-256 RFC5869 TC1 failed");
430        assert_eq!(okm, expected, "HKDF-SHA-256 RFC5869 TC1 mismatch");
431    }
432
433    #[test]
434    fn hkdf_sha256_empty_salt() {
435        // Empty salt causes HKDF to use a zero-filled salt of hash length.
436        let kdf = HkdfSha256;
437        let mut okm = [0u8; 32];
438        kdf.derive(b"input key material", b"", b"info", &mut okm)
439            .expect("HKDF with empty salt failed");
440        assert_ne!(okm, [0u8; 32]);
441    }
442
443    #[test]
444    fn hkdf_sha512_round_trip() {
445        let kdf = HkdfSha512;
446        let mut okm1 = [0u8; 64];
447        let mut okm2 = [0u8; 64];
448        kdf.derive(b"secret", b"salt", b"info", &mut okm1).unwrap();
449        kdf.derive(b"secret", b"salt", b"info", &mut okm2).unwrap();
450        assert_eq!(okm1, okm2, "HKDF-SHA-512 must be deterministic");
451        assert_ne!(okm1, [0u8; 64]);
452    }
453
454    #[test]
455    fn hkdf_empty_output_errors() {
456        let kdf = HkdfSha256;
457        let result = kdf.derive(b"ikm", b"salt", b"info", &mut []);
458        assert_eq!(result, Err(CryptoError::BadInput));
459    }
460
461    // ── HKDF-SHA-384 ─────────────────────────────────────────────────────────
462
463    #[test]
464    fn hkdf_sha384_round_trip() {
465        let kdf = HkdfSha384;
466        let mut okm1 = [0u8; 48];
467        let mut okm2 = [0u8; 48];
468        kdf.derive(b"secret", b"salt", b"info", &mut okm1)
469            .expect("derive 1 failed");
470        kdf.derive(b"secret", b"salt", b"info", &mut okm2)
471            .expect("derive 2 failed");
472        assert_eq!(okm1, okm2, "HKDF-SHA-384 must be deterministic");
473        assert_ne!(okm1, [0u8; 48]);
474    }
475
476    #[test]
477    fn hkdf_sha384_empty_output_errors() {
478        let kdf = HkdfSha384;
479        let result = kdf.derive(b"ikm", b"salt", b"info", &mut []);
480        assert_eq!(result, Err(CryptoError::BadInput));
481    }
482
483    // ── Extract-only / Expand-only ───────────────────────────────────────────
484
485    #[test]
486    fn hkdf_sha256_extract_expand_equivalent() {
487        // Extract+Expand should produce the same result as the full Kdf::derive.
488        let ikm = b"input key material";
489        let salt = b"salt value";
490        let info = b"info";
491
492        // Full derive.
493        let kdf = HkdfSha256;
494        let mut okm_full = [0u8; 42];
495        kdf.derive(ikm, salt, info, &mut okm_full)
496            .expect("full derive failed");
497
498        // Separated extract + expand.
499        let prk = hkdf_sha256_extract(salt, ikm);
500        let mut okm_sep = [0u8; 42];
501        hkdf_sha256_expand(&prk, info, &mut okm_sep).expect("expand failed");
502
503        assert_eq!(okm_full, okm_sep, "Extract+Expand must equal full derive");
504    }
505
506    #[test]
507    fn hkdf_sha384_extract_expand_round_trip() {
508        let prk = hkdf_sha384_extract(b"salt", b"ikm");
509        assert_eq!(prk.len(), 48);
510        let mut okm = [0u8; 32];
511        hkdf_sha384_expand(&prk, b"info", &mut okm).expect("expand failed");
512        assert_ne!(okm, [0u8; 32]);
513    }
514
515    #[test]
516    fn hkdf_sha512_extract_expand_round_trip() {
517        let prk = hkdf_sha512_extract(b"salt", b"ikm");
518        assert_eq!(prk.len(), 64);
519        let mut okm = [0u8; 64];
520        hkdf_sha512_expand(&prk, b"info", &mut okm).expect("expand failed");
521        assert_ne!(okm, [0u8; 64]);
522    }
523
524    #[test]
525    fn hkdf_expand_empty_output_errors() {
526        let prk = hkdf_sha256_extract(b"salt", b"ikm");
527        let result = hkdf_sha256_expand(&prk, b"info", &mut []);
528        assert_eq!(result, Err(CryptoError::BadInput));
529    }
530
531    // ── verify_password ──────────────────────────────────────────────────────
532
533    const VERIFY_SALT: &[u8] = b"0123456789abcdef"; // 16 bytes
534
535    #[test]
536    fn verify_password_argon2id_correct() {
537        let hasher = Argon2idHasher::new(Argon2Params::TEST_PARAMS);
538        let mut expected = [0u8; 32];
539        hasher
540            .hash_password(b"password", VERIFY_SALT, &hasher.params, &mut expected)
541            .expect("hash");
542        verify_password(&hasher, b"password", VERIFY_SALT, &expected)
543            .expect("correct password must pass");
544    }
545
546    #[test]
547    fn verify_password_argon2id_wrong_password() {
548        let hasher = Argon2idHasher::new(Argon2Params::TEST_PARAMS);
549        let mut expected = [0u8; 32];
550        hasher
551            .hash_password(b"password", VERIFY_SALT, &hasher.params, &mut expected)
552            .expect("hash");
553        let result = verify_password(&hasher, b"wrongpassword", VERIFY_SALT, &expected);
554        assert_eq!(result, Err(CryptoError::InvalidTag));
555    }
556
557    #[test]
558    fn verify_password_pbkdf2_correct() {
559        let hasher = Pbkdf2Sha256Hasher::new(1_000);
560        let mut expected = [0u8; 32];
561        hasher
562            .hash_password(b"mypassword", VERIFY_SALT, &hasher.params(), &mut expected)
563            .expect("hash");
564        verify_password(&hasher, b"mypassword", VERIFY_SALT, &expected)
565            .expect("correct password must pass");
566    }
567
568    #[test]
569    fn verify_password_pbkdf2_wrong_password() {
570        let hasher = Pbkdf2Sha256Hasher::new(1_000);
571        let mut expected = [0u8; 32];
572        hasher
573            .hash_password(b"mypassword", VERIFY_SALT, &hasher.params(), &mut expected)
574            .expect("hash");
575        let result = verify_password(&hasher, b"notmypassword", VERIFY_SALT, &expected);
576        assert_eq!(result, Err(CryptoError::InvalidTag));
577    }
578
579    #[test]
580    fn verify_password_empty_expected_errors() {
581        let hasher = Pbkdf2Sha256Hasher::new(1_000);
582        let result = verify_password(&hasher, b"password", VERIFY_SALT, &[]);
583        assert_eq!(result, Err(CryptoError::BadInput));
584    }
585}