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//! | bcrypt (`$2b$`) | [`bcrypt_kdf`] | (pure Rust, from scratch) |
14
15pub mod argon2_kdf;
16pub mod balloon;
17pub mod bcrypt_kdf;
18pub mod hkdf_label;
19pub mod kbkdf;
20pub mod pbkdf2_kdf;
21pub mod scrypt_kdf;
22pub mod stretcher;
23
24// ── OWASP 2023 minimum iteration counts ───────────────────────────────────────
25
26/// OWASP 2023 Password Storage Cheat Sheet minimum iteration count for
27/// PBKDF2-HMAC-SHA-256.
28///
29/// Reference: <https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html>
30pub const PBKDF2_SHA256_MIN_ITERATIONS: u32 = 600_000;
31
32/// OWASP 2023 Password Storage Cheat Sheet minimum iteration count for
33/// PBKDF2-HMAC-SHA-512.
34///
35/// SHA-512 is ~2× faster than SHA-256 per round on 64-bit CPUs, so the
36/// equivalent minimum is approximately 210,000.
37pub const PBKDF2_SHA512_MIN_ITERATIONS: u32 = 210_000;
38
39pub use argon2_kdf::{
40    argon2d_derive, argon2i_derive, argon2id_derive, argon2id_to_phc_string, argon2id_verify_phc,
41    Argon2Params, Argon2idHasher,
42};
43pub use balloon::{
44    balloon_sha256, balloon_sha256_secret, balloon_sha512, balloon_sha512_secret, BalloonHasher,
45    BalloonParams, BalloonVariant, BALLOON_DELTA,
46};
47pub use bcrypt_kdf::{bcrypt_hash, bcrypt_verify, BcryptHasher, BcryptParams};
48pub use hkdf_label::{hkdf_expand_label_sha256, hkdf_expand_label_sha384};
49pub use kbkdf::{
50    kbkdf_counter_hmac_sha256, kbkdf_counter_hmac_sha256_secret, kbkdf_counter_hmac_sha384,
51    kbkdf_counter_hmac_sha512,
52};
53pub use pbkdf2_kdf::{
54    pbkdf2_sha256, pbkdf2_sha512, Pbkdf2Params, Pbkdf2Sha256Hasher, Pbkdf2Sha512Hasher,
55};
56pub use scrypt_kdf::{scrypt_derive, ScryptHasher, ScryptParams};
57pub use stretcher::{
58    Argon2idStretchParams, BalloonStretchParams, KeyStretcher, Pbkdf2StretchParams,
59    ScryptStretchParams, StretchParams, Stretcher,
60};
61
62use hkdf::Hkdf;
63use oxicrypto_core::{CryptoError, Kdf, PasswordHash};
64use subtle::ConstantTimeEq;
65
66// ── HKDF-SHA-256 ──────────────────────────────────────────────────────────────
67
68/// HKDF-SHA-256 key derivation function.
69#[derive(Debug, Default, Clone, Copy)]
70pub struct HkdfSha256;
71
72impl Kdf for HkdfSha256 {
73    fn name(&self) -> &'static str {
74        "HKDF-SHA-256"
75    }
76    fn derive(
77        &self,
78        ikm: &[u8],
79        salt: &[u8],
80        info: &[u8],
81        okm_out: &mut [u8],
82    ) -> Result<(), CryptoError> {
83        if okm_out.is_empty() {
84            return Err(CryptoError::BadInput);
85        }
86        let salt_opt = if salt.is_empty() { None } else { Some(salt) };
87        let hk = Hkdf::<sha2::Sha256>::new(salt_opt, ikm);
88        hk.expand(info, okm_out)
89            .map_err(|_| CryptoError::Internal("HKDF expand failed (output too long)"))?;
90        Ok(())
91    }
92}
93
94// ── HKDF-SHA-512 ──────────────────────────────────────────────────────────────
95
96/// HKDF-SHA-512 key derivation function.
97#[derive(Debug, Default, Clone, Copy)]
98pub struct HkdfSha512;
99
100impl Kdf for HkdfSha512 {
101    fn name(&self) -> &'static str {
102        "HKDF-SHA-512"
103    }
104    fn derive(
105        &self,
106        ikm: &[u8],
107        salt: &[u8],
108        info: &[u8],
109        okm_out: &mut [u8],
110    ) -> Result<(), CryptoError> {
111        if okm_out.is_empty() {
112            return Err(CryptoError::BadInput);
113        }
114        let salt_opt = if salt.is_empty() { None } else { Some(salt) };
115        let hk = Hkdf::<sha2::Sha512>::new(salt_opt, ikm);
116        hk.expand(info, okm_out)
117            .map_err(|_| CryptoError::Internal("HKDF expand failed (output too long)"))?;
118        Ok(())
119    }
120}
121
122// ── HKDF-SHA-384 ──────────────────────────────────────────────────────────────
123
124/// HKDF-SHA-384 key derivation function.
125#[derive(Debug, Default, Clone, Copy)]
126pub struct HkdfSha384;
127
128impl Kdf for HkdfSha384 {
129    fn name(&self) -> &'static str {
130        "HKDF-SHA-384"
131    }
132    fn derive(
133        &self,
134        ikm: &[u8],
135        salt: &[u8],
136        info: &[u8],
137        okm_out: &mut [u8],
138    ) -> Result<(), CryptoError> {
139        if okm_out.is_empty() {
140            return Err(CryptoError::BadInput);
141        }
142        let salt_opt = if salt.is_empty() { None } else { Some(salt) };
143        let hk = Hkdf::<sha2::Sha384>::new(salt_opt, ikm);
144        hk.expand(info, okm_out)
145            .map_err(|_| CryptoError::Internal("HKDF-SHA-384 expand failed (output too long)"))?;
146        Ok(())
147    }
148}
149
150// ── HKDF Extract-only / Expand-only (RFC 5869 separated phases) ─────────────
151
152/// Perform HKDF-Extract with SHA-256, returning the pseudorandom key (PRK).
153///
154/// This is the extraction phase only (RFC 5869 Section 2.2).
155/// The PRK is always 32 bytes (the output size of SHA-256).
156///
157/// Used by protocols like TLS 1.3 that need separated extract/expand.
158#[must_use]
159pub fn hkdf_sha256_extract(salt: &[u8], ikm: &[u8]) -> [u8; 32] {
160    let salt_opt = if salt.is_empty() { None } else { Some(salt) };
161    let (prk, _) = Hkdf::<sha2::Sha256>::extract(salt_opt, ikm);
162    let mut out = [0u8; 32];
163    out.copy_from_slice(&prk);
164    out
165}
166
167/// Perform HKDF-Expand with SHA-256 from a pre-extracted PRK.
168///
169/// This is the expansion phase only (RFC 5869 Section 2.3).
170/// `prk` should be the output of [`hkdf_sha256_extract`] (32 bytes).
171#[must_use = "HKDF expand result must be checked"]
172pub fn hkdf_sha256_expand(prk: &[u8], info: &[u8], okm_out: &mut [u8]) -> Result<(), CryptoError> {
173    if okm_out.is_empty() {
174        return Err(CryptoError::BadInput);
175    }
176    let hk = Hkdf::<sha2::Sha256>::from_prk(prk).map_err(|_| CryptoError::InvalidKey)?;
177    hk.expand(info, okm_out)
178        .map_err(|_| CryptoError::Internal("HKDF-SHA-256 expand failed (output too long)"))?;
179    Ok(())
180}
181
182/// Perform HKDF-Extract with SHA-384, returning the pseudorandom key (PRK).
183///
184/// The PRK is always 48 bytes (the output size of SHA-384).
185#[must_use]
186pub fn hkdf_sha384_extract(salt: &[u8], ikm: &[u8]) -> [u8; 48] {
187    let salt_opt = if salt.is_empty() { None } else { Some(salt) };
188    let (prk, _) = Hkdf::<sha2::Sha384>::extract(salt_opt, ikm);
189    let mut out = [0u8; 48];
190    out.copy_from_slice(&prk);
191    out
192}
193
194/// Perform HKDF-Expand with SHA-384 from a pre-extracted PRK.
195#[must_use = "HKDF expand result must be checked"]
196pub fn hkdf_sha384_expand(prk: &[u8], info: &[u8], okm_out: &mut [u8]) -> Result<(), CryptoError> {
197    if okm_out.is_empty() {
198        return Err(CryptoError::BadInput);
199    }
200    let hk = Hkdf::<sha2::Sha384>::from_prk(prk).map_err(|_| CryptoError::InvalidKey)?;
201    hk.expand(info, okm_out)
202        .map_err(|_| CryptoError::Internal("HKDF-SHA-384 expand failed (output too long)"))?;
203    Ok(())
204}
205
206/// Perform HKDF-Extract with SHA-512, returning the pseudorandom key (PRK).
207///
208/// The PRK is always 64 bytes (the output size of SHA-512).
209#[must_use]
210pub fn hkdf_sha512_extract(salt: &[u8], ikm: &[u8]) -> [u8; 64] {
211    let salt_opt = if salt.is_empty() { None } else { Some(salt) };
212    let (prk, _) = Hkdf::<sha2::Sha512>::extract(salt_opt, ikm);
213    let mut out = [0u8; 64];
214    out.copy_from_slice(&prk);
215    out
216}
217
218/// Perform HKDF-Expand with SHA-512 from a pre-extracted PRK.
219#[must_use = "HKDF expand result must be checked"]
220pub fn hkdf_sha512_expand(prk: &[u8], info: &[u8], okm_out: &mut [u8]) -> Result<(), CryptoError> {
221    if okm_out.is_empty() {
222        return Err(CryptoError::BadInput);
223    }
224    let hk = Hkdf::<sha2::Sha512>::from_prk(prk).map_err(|_| CryptoError::InvalidKey)?;
225    hk.expand(info, okm_out)
226        .map_err(|_| CryptoError::Internal("HKDF-SHA-512 expand failed (output too long)"))?;
227    Ok(())
228}
229
230// ── HKDF derive-to-Vec convenience wrappers ───────────────────────────────────
231
232/// Derive `len` bytes from `ikm`, `salt`, and `info` using HKDF-SHA-256, returning
233/// the output as an owned `Vec<u8>`.
234///
235/// This is a convenience wrapper around [`HkdfSha256::derive`] (which performs
236/// the full extract+expand sequence per RFC 5869).
237///
238/// # Errors
239/// Returns [`CryptoError::BadInput`] if `len == 0` or if the requested output
240/// exceeds 255 × 32 bytes (HKDF-SHA-256 maximum).
241#[must_use = "HKDF derive result must be checked"]
242pub fn hkdf_sha256_derive_to_vec(
243    ikm: &[u8],
244    salt: &[u8],
245    info: &[u8],
246    len: usize,
247) -> Result<Vec<u8>, CryptoError> {
248    if len == 0 {
249        return Err(CryptoError::BadInput);
250    }
251    // Guard against OOM on obviously-too-large requests before allocating.
252    // HKDF-SHA-256 maximum: 255 * 32 = 8 160 bytes.
253    const MAX_HKDF_SHA256: usize = 255 * 32;
254    if len > MAX_HKDF_SHA256 {
255        return Err(CryptoError::Internal(
256            "requested length exceeds HKDF-SHA-256 maximum (255 * 32)",
257        ));
258    }
259    let mut out = vec![0u8; len];
260    HkdfSha256.derive(ikm, salt, info, &mut out)?;
261    Ok(out)
262}
263
264/// Derive `len` bytes from `ikm`, `salt`, and `info` using HKDF-SHA-384, returning
265/// the output as an owned `Vec<u8>`.
266///
267/// # Errors
268/// Returns [`CryptoError::BadInput`] if `len == 0` or if the requested output
269/// exceeds 255 × 48 bytes (HKDF-SHA-384 maximum).
270#[must_use = "HKDF derive result must be checked"]
271pub fn hkdf_sha384_derive_to_vec(
272    ikm: &[u8],
273    salt: &[u8],
274    info: &[u8],
275    len: usize,
276) -> Result<Vec<u8>, CryptoError> {
277    if len == 0 {
278        return Err(CryptoError::BadInput);
279    }
280    // Guard against OOM on obviously-too-large requests before allocating.
281    // HKDF-SHA-384 maximum: 255 * 48 = 12 240 bytes.
282    const MAX_HKDF_SHA384: usize = 255 * 48;
283    if len > MAX_HKDF_SHA384 {
284        return Err(CryptoError::Internal(
285            "requested length exceeds HKDF-SHA-384 maximum (255 * 48)",
286        ));
287    }
288    let mut out = vec![0u8; len];
289    HkdfSha384.derive(ikm, salt, info, &mut out)?;
290    Ok(out)
291}
292
293/// Derive `len` bytes from `ikm`, `salt`, and `info` using HKDF-SHA-512, returning
294/// the output as an owned `Vec<u8>`.
295///
296/// # Errors
297/// Returns [`CryptoError::BadInput`] if `len == 0` or if the requested output
298/// exceeds 255 × 64 bytes (HKDF-SHA-512 maximum).
299#[must_use = "HKDF derive result must be checked"]
300pub fn hkdf_sha512_derive_to_vec(
301    ikm: &[u8],
302    salt: &[u8],
303    info: &[u8],
304    len: usize,
305) -> Result<Vec<u8>, CryptoError> {
306    if len == 0 {
307        return Err(CryptoError::BadInput);
308    }
309    // Guard against OOM on obviously-too-large requests before allocating.
310    // HKDF-SHA-512 maximum: 255 * 64 = 16 320 bytes.
311    const MAX_HKDF_SHA512: usize = 255 * 64;
312    if len > MAX_HKDF_SHA512 {
313        return Err(CryptoError::Internal(
314            "requested length exceeds HKDF-SHA-512 maximum (255 * 64)",
315        ));
316    }
317    let mut out = vec![0u8; len];
318    HkdfSha512.derive(ikm, salt, info, &mut out)?;
319    Ok(out)
320}
321
322// ── Salt generation helpers ────────────────────────────────────────────────────
323
324/// Generate a random salt of variable length using the provided CSPRNG.
325///
326/// # Arguments
327/// - `rng`  — mutable reference to an [`oxicrypto_rand::OxiRng`]
328/// - `len`  — number of random bytes to generate; must be ≥ 1
329///
330/// # Errors
331/// - Returns [`CryptoError::BadInput`] if `len == 0`.
332/// - Returns [`CryptoError::Rng`] if the RNG fails to produce bytes.
333///
334/// # Example
335/// ```ignore
336/// use oxicrypto_kdf::generate_salt;
337/// use oxicrypto_rand::OxiRng;
338///
339/// let mut rng = OxiRng::new().unwrap();
340/// let salt = generate_salt(&mut rng, 32).unwrap();
341/// assert_eq!(salt.len(), 32);
342/// ```
343#[must_use = "generated salt result must be checked"]
344pub fn generate_salt(rng: &mut oxicrypto_rand::OxiRng, len: usize) -> Result<Vec<u8>, CryptoError> {
345    if len == 0 {
346        return Err(CryptoError::BadInput);
347    }
348    let mut buf = vec![0u8; len];
349    oxicrypto_core::Rng::fill(rng, &mut buf)?;
350    Ok(buf)
351}
352
353/// Generate a random 16-byte salt using the system CSPRNG.
354///
355/// Suitable for PBKDF2 (recommended ≥ 16 bytes per NIST SP 800-132) and
356/// Argon2id (requires ≥ 8 bytes per RFC 9106).
357///
358/// # Errors
359/// Returns [`CryptoError::Rng`] if the OS entropy source is unavailable.
360#[must_use = "generated salt result must be checked"]
361pub fn generate_salt_16() -> Result<[u8; 16], CryptoError> {
362    let bytes = oxicrypto_rand::random_bytes(16)?;
363    let mut out = [0u8; 16];
364    out.copy_from_slice(&bytes);
365    Ok(out)
366}
367
368/// Generate a random 32-byte salt using the system CSPRNG.
369///
370/// Suitable for Argon2id and scrypt where a longer salt provides additional
371/// domain separation.
372///
373/// # Errors
374/// Returns [`CryptoError::Rng`] if the OS entropy source is unavailable.
375#[must_use = "generated salt result must be checked"]
376pub fn generate_salt_32() -> Result<[u8; 32], CryptoError> {
377    let bytes = oxicrypto_rand::random_bytes(32)?;
378    let mut out = [0u8; 32];
379    out.copy_from_slice(&bytes);
380    Ok(out)
381}
382
383// ---------------------------------------------------------------------------
384// verify_password — constant-time password verification
385// ---------------------------------------------------------------------------
386
387/// Verify a password by re-hashing and comparing in constant time.
388///
389/// Hashes `password` with `salt` using `hasher` into a temporary buffer of
390/// `expected.len()` bytes, then compares the result to `expected` using
391/// [`subtle::ConstantTimeEq`].  The comparison time does not depend on the
392/// position of the first differing byte.
393///
394/// # Errors
395/// - Returns `Err(CryptoError::BadInput)` if `expected` is empty.
396/// - Returns the underlying [`CryptoError`] if hashing fails (e.g. bad salt length).
397/// - Returns `Err(CryptoError::InvalidTag)` if the password does not match.
398///
399/// # Example
400/// ```ignore
401/// use oxicrypto_kdf::{Argon2idHasher, Argon2Params, verify_password};
402///
403/// let hasher = Argon2idHasher::new(Argon2Params::TEST_PARAMS);
404/// let salt   = b"0123456789abcdef";
405/// let mut expected = [0u8; 32];
406/// hasher.hash_password(b"password", salt, &hasher.params, &mut expected).unwrap();
407///
408/// verify_password(&hasher, b"password", salt, &expected).unwrap();        // ok
409/// assert!(verify_password(&hasher, b"wrong", salt, &expected).is_err()); // rejected
410/// ```
411#[must_use = "password verification result must be checked"]
412pub fn verify_password<H>(
413    hasher: &H,
414    password: &[u8],
415    salt: &[u8],
416    expected: &[u8],
417) -> Result<(), CryptoError>
418where
419    H: PasswordHash,
420{
421    if expected.is_empty() {
422        return Err(CryptoError::BadInput);
423    }
424
425    // Allocate a stack-sized temporary buffer.  For passwords the expected
426    // output is typically 16–64 bytes, so heap allocation is not required;
427    // but we use a Vec here to support arbitrary output lengths.
428    let mut computed = vec![0u8; expected.len()];
429
430    // Use empty params — each concrete hasher uses its own stored params.
431    #[derive(Debug)]
432    struct NullParams;
433    impl oxicrypto_core::PasswordHashParams for NullParams {
434        fn memory_cost(&self) -> Option<u32> {
435            None
436        }
437        fn time_cost(&self) -> Option<u32> {
438            None
439        }
440        fn parallelism(&self) -> Option<u32> {
441            None
442        }
443    }
444
445    hasher.hash_password(password, salt, &NullParams, &mut computed)?;
446
447    // Constant-time comparison: returns 0x01 iff equal.
448    let ok: bool = computed.ct_eq(expected).into();
449    if ok {
450        Ok(())
451    } else {
452        Err(CryptoError::InvalidTag)
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    fn hex_decode(s: &str) -> Vec<u8> {
461        (0..s.len())
462            .step_by(2)
463            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
464            .collect()
465    }
466
467    // RFC 5869 Test Case 1 for HKDF-SHA-256
468    // Hash = SHA-256
469    // IKM  = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b (22 bytes)
470    // salt = 0x000102030405060708090a0b0c (13 bytes)
471    // info = 0xf0f1f2f3f4f5f6f7f8f9 (10 bytes)
472    // L    = 42 bytes
473    // OKM  = 0x3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865
474    #[test]
475    fn hkdf_sha256_rfc5869_tc1() {
476        let ikm = hex_decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
477        let salt = hex_decode("000102030405060708090a0b0c");
478        let info = hex_decode("f0f1f2f3f4f5f6f7f8f9");
479        let expected = hex_decode(
480            "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
481        );
482
483        let kdf = HkdfSha256;
484        let mut okm = vec![0u8; 42];
485        kdf.derive(&ikm, &salt, &info, &mut okm)
486            .expect("HKDF-SHA-256 RFC5869 TC1 failed");
487        assert_eq!(okm, expected, "HKDF-SHA-256 RFC5869 TC1 mismatch");
488    }
489
490    #[test]
491    fn hkdf_sha256_empty_salt() {
492        // Empty salt causes HKDF to use a zero-filled salt of hash length.
493        let kdf = HkdfSha256;
494        let mut okm = [0u8; 32];
495        kdf.derive(b"input key material", b"", b"info", &mut okm)
496            .expect("HKDF with empty salt failed");
497        assert_ne!(okm, [0u8; 32]);
498    }
499
500    #[test]
501    fn hkdf_sha512_round_trip() {
502        let kdf = HkdfSha512;
503        let mut okm1 = [0u8; 64];
504        let mut okm2 = [0u8; 64];
505        kdf.derive(b"secret", b"salt", b"info", &mut okm1).unwrap();
506        kdf.derive(b"secret", b"salt", b"info", &mut okm2).unwrap();
507        assert_eq!(okm1, okm2, "HKDF-SHA-512 must be deterministic");
508        assert_ne!(okm1, [0u8; 64]);
509    }
510
511    #[test]
512    fn hkdf_empty_output_errors() {
513        let kdf = HkdfSha256;
514        let result = kdf.derive(b"ikm", b"salt", b"info", &mut []);
515        assert_eq!(result, Err(CryptoError::BadInput));
516    }
517
518    // ── HKDF-SHA-384 ─────────────────────────────────────────────────────────
519
520    #[test]
521    fn hkdf_sha384_round_trip() {
522        let kdf = HkdfSha384;
523        let mut okm1 = [0u8; 48];
524        let mut okm2 = [0u8; 48];
525        kdf.derive(b"secret", b"salt", b"info", &mut okm1)
526            .expect("derive 1 failed");
527        kdf.derive(b"secret", b"salt", b"info", &mut okm2)
528            .expect("derive 2 failed");
529        assert_eq!(okm1, okm2, "HKDF-SHA-384 must be deterministic");
530        assert_ne!(okm1, [0u8; 48]);
531    }
532
533    #[test]
534    fn hkdf_sha384_empty_output_errors() {
535        let kdf = HkdfSha384;
536        let result = kdf.derive(b"ikm", b"salt", b"info", &mut []);
537        assert_eq!(result, Err(CryptoError::BadInput));
538    }
539
540    // ── Extract-only / Expand-only ───────────────────────────────────────────
541
542    #[test]
543    fn hkdf_sha256_extract_expand_equivalent() {
544        // Extract+Expand should produce the same result as the full Kdf::derive.
545        let ikm = b"input key material";
546        let salt = b"salt value";
547        let info = b"info";
548
549        // Full derive.
550        let kdf = HkdfSha256;
551        let mut okm_full = [0u8; 42];
552        kdf.derive(ikm, salt, info, &mut okm_full)
553            .expect("full derive failed");
554
555        // Separated extract + expand.
556        let prk = hkdf_sha256_extract(salt, ikm);
557        let mut okm_sep = [0u8; 42];
558        hkdf_sha256_expand(&prk, info, &mut okm_sep).expect("expand failed");
559
560        assert_eq!(okm_full, okm_sep, "Extract+Expand must equal full derive");
561    }
562
563    #[test]
564    fn hkdf_sha384_extract_expand_round_trip() {
565        let prk = hkdf_sha384_extract(b"salt", b"ikm");
566        assert_eq!(prk.len(), 48);
567        let mut okm = [0u8; 32];
568        hkdf_sha384_expand(&prk, b"info", &mut okm).expect("expand failed");
569        assert_ne!(okm, [0u8; 32]);
570    }
571
572    #[test]
573    fn hkdf_sha512_extract_expand_round_trip() {
574        let prk = hkdf_sha512_extract(b"salt", b"ikm");
575        assert_eq!(prk.len(), 64);
576        let mut okm = [0u8; 64];
577        hkdf_sha512_expand(&prk, b"info", &mut okm).expect("expand failed");
578        assert_ne!(okm, [0u8; 64]);
579    }
580
581    #[test]
582    fn hkdf_expand_empty_output_errors() {
583        let prk = hkdf_sha256_extract(b"salt", b"ikm");
584        let result = hkdf_sha256_expand(&prk, b"info", &mut []);
585        assert_eq!(result, Err(CryptoError::BadInput));
586    }
587
588    // ── verify_password ──────────────────────────────────────────────────────
589
590    const VERIFY_SALT: &[u8] = b"0123456789abcdef"; // 16 bytes
591
592    #[test]
593    fn verify_password_argon2id_correct() {
594        let hasher = Argon2idHasher::new(Argon2Params::TEST_PARAMS);
595        let mut expected = [0u8; 32];
596        hasher
597            .hash_password(b"password", VERIFY_SALT, &hasher.params, &mut expected)
598            .expect("hash");
599        verify_password(&hasher, b"password", VERIFY_SALT, &expected)
600            .expect("correct password must pass");
601    }
602
603    #[test]
604    fn verify_password_argon2id_wrong_password() {
605        let hasher = Argon2idHasher::new(Argon2Params::TEST_PARAMS);
606        let mut expected = [0u8; 32];
607        hasher
608            .hash_password(b"password", VERIFY_SALT, &hasher.params, &mut expected)
609            .expect("hash");
610        let result = verify_password(&hasher, b"wrongpassword", VERIFY_SALT, &expected);
611        assert_eq!(result, Err(CryptoError::InvalidTag));
612    }
613
614    #[test]
615    fn verify_password_pbkdf2_correct() {
616        let hasher = Pbkdf2Sha256Hasher::new(1_000);
617        let mut expected = [0u8; 32];
618        hasher
619            .hash_password(b"mypassword", VERIFY_SALT, &hasher.params(), &mut expected)
620            .expect("hash");
621        verify_password(&hasher, b"mypassword", VERIFY_SALT, &expected)
622            .expect("correct password must pass");
623    }
624
625    #[test]
626    fn verify_password_pbkdf2_wrong_password() {
627        let hasher = Pbkdf2Sha256Hasher::new(1_000);
628        let mut expected = [0u8; 32];
629        hasher
630            .hash_password(b"mypassword", VERIFY_SALT, &hasher.params(), &mut expected)
631            .expect("hash");
632        let result = verify_password(&hasher, b"notmypassword", VERIFY_SALT, &expected);
633        assert_eq!(result, Err(CryptoError::InvalidTag));
634    }
635
636    #[test]
637    fn verify_password_empty_expected_errors() {
638        let hasher = Pbkdf2Sha256Hasher::new(1_000);
639        let result = verify_password(&hasher, b"password", VERIFY_SALT, &[]);
640        assert_eq!(result, Err(CryptoError::BadInput));
641    }
642
643    // ── generate_salt ─────────────────────────────────────────────────────────
644
645    #[test]
646    fn generate_salt_variable_returns_correct_length() {
647        let mut rng = oxicrypto_rand::OxiRng::new().expect("OxiRng::new");
648        for len in [8usize, 16, 32, 64] {
649            let salt = generate_salt(&mut rng, len).expect("generate_salt");
650            assert_eq!(
651                salt.len(),
652                len,
653                "salt length must equal requested len {len}"
654            );
655        }
656    }
657
658    #[test]
659    fn generate_salt_zero_len_errors() {
660        let mut rng = oxicrypto_rand::OxiRng::new().expect("OxiRng::new");
661        let result = generate_salt(&mut rng, 0);
662        assert_eq!(result, Err(CryptoError::BadInput));
663    }
664
665    #[test]
666    fn generate_salt_produces_distinct_outputs() {
667        let mut rng = oxicrypto_rand::OxiRng::new().expect("OxiRng::new");
668        let s1 = generate_salt(&mut rng, 32).expect("salt 1");
669        let s2 = generate_salt(&mut rng, 32).expect("salt 2");
670        // Two independent 32-byte random salts must differ (with overwhelming probability).
671        assert_ne!(s1, s2, "generate_salt must return distinct salts");
672    }
673
674    // ── PBKDF2 — zero-iteration guard ────────────────────────────────────────
675
676    #[test]
677    fn pbkdf2_sha256_zero_iterations_returns_bad_input() {
678        let mut out = [0u8; 32];
679        let result = pbkdf2_sha256(b"password", b"saltsalt", 0, &mut out);
680        assert_eq!(
681            result,
682            Err(CryptoError::BadInput),
683            "0 iterations must be rejected"
684        );
685    }
686
687    #[test]
688    fn pbkdf2_sha512_zero_iterations_returns_bad_input() {
689        let mut out = [0u8; 64];
690        let result = pbkdf2_sha512(b"password", b"saltsalt", 0, &mut out);
691        assert_eq!(
692            result,
693            Err(CryptoError::BadInput),
694            "0 iterations must be rejected"
695        );
696    }
697
698    // ── Argon2id — short salt guard ────────────────────────────────────────────
699
700    #[test]
701    fn argon2id_salt_too_short_returns_bad_input() {
702        let params = Argon2Params::TEST_PARAMS;
703        let mut out = [0u8; 32];
704        // salt must be >= 8 bytes per RFC 9106; a 7-byte salt must error.
705        let result = argon2_kdf::argon2id_derive(b"password", b"tooshrt", params, &mut out);
706        assert_eq!(
707            result,
708            Err(CryptoError::BadInput),
709            "7-byte salt must be rejected (minimum is 8)"
710        );
711    }
712
713    #[test]
714    fn argon2id_empty_salt_returns_bad_input() {
715        let params = Argon2Params::TEST_PARAMS;
716        let mut out = [0u8; 32];
717        let result = argon2_kdf::argon2id_derive(b"password", b"", params, &mut out);
718        assert_eq!(
719            result,
720            Err(CryptoError::BadInput),
721            "empty salt must be rejected"
722        );
723    }
724
725    // ── HKDF output > 255 * HashLen → error ──────────────────────────────────
726
727    #[test]
728    fn hkdf_sha256_output_exceeding_max_errors() {
729        // HKDF-SHA-256 maximum output = 255 * 32 = 8160 bytes.
730        // Requesting 8161 bytes must return an error.
731        let kdf = HkdfSha256;
732        let max = 255 * 32 + 1; // one byte over the limit
733        let mut out = vec![0u8; max];
734        let result = kdf.derive(b"ikm", b"salt", b"info", &mut out);
735        assert!(
736            result.is_err(),
737            "HKDF-SHA-256 derive must fail when output > 255 * HashLen"
738        );
739    }
740
741    #[test]
742    fn hkdf_sha512_output_exceeding_max_errors() {
743        // HKDF-SHA-512 maximum output = 255 * 64 = 16320 bytes.
744        let kdf = HkdfSha512;
745        let max = 255 * 64 + 1;
746        let mut out = vec![0u8; max];
747        let result = kdf.derive(b"ikm", b"salt", b"info", &mut out);
748        assert!(
749            result.is_err(),
750            "HKDF-SHA-512 derive must fail when output > 255 * HashLen"
751        );
752    }
753
754    // ── scrypt — invalid parameters ────────────────────────────────────────────
755
756    #[test]
757    fn scrypt_log_n_64_rejected() {
758        // log_n = 64 would require 2^64 blocks — always rejected.
759        let result = scrypt_kdf::ScryptParams::new(64, 8, 1);
760        assert!(result.is_err(), "log_n=64 must be rejected");
761    }
762
763    #[test]
764    fn scrypt_zero_r_rejected() {
765        // r = 0 is invalid.
766        let result = scrypt_kdf::ScryptParams::new(14, 0, 1);
767        assert!(result.is_err(), "r=0 must be rejected by ScryptParams::new");
768    }
769
770    // ── Property: all KDFs are deterministic ─────────────────────────────────
771
772    #[test]
773    fn prop_kdf_hkdf_sha256_is_deterministic() {
774        let kdf = HkdfSha256;
775        let mut out1 = [0u8; 32];
776        let mut out2 = [0u8; 32];
777        kdf.derive(b"ikm", b"salt", b"info", &mut out1)
778            .expect("derive1");
779        kdf.derive(b"ikm", b"salt", b"info", &mut out2)
780            .expect("derive2");
781        assert_eq!(out1, out2, "HKDF-SHA-256 must be deterministic");
782    }
783
784    #[test]
785    fn prop_kdf_hkdf_sha384_is_deterministic() {
786        let kdf = HkdfSha384;
787        let mut out1 = [0u8; 48];
788        let mut out2 = [0u8; 48];
789        kdf.derive(b"ikm", b"salt", b"info", &mut out1)
790            .expect("derive1");
791        kdf.derive(b"ikm", b"salt", b"info", &mut out2)
792            .expect("derive2");
793        assert_eq!(out1, out2, "HKDF-SHA-384 must be deterministic");
794    }
795
796    // ── Property: different salts produce different outputs ────────────────────
797
798    #[test]
799    fn prop_kdf_different_salts_produce_different_outputs() {
800        let kdf = HkdfSha256;
801        let mut out1 = [0u8; 32];
802        let mut out2 = [0u8; 32];
803        kdf.derive(b"ikm", b"salt_a", b"info", &mut out1)
804            .expect("derive salt_a");
805        kdf.derive(b"ikm", b"salt_b", b"info", &mut out2)
806            .expect("derive salt_b");
807        assert_ne!(
808            out1, out2,
809            "different salts must produce different HKDF outputs"
810        );
811    }
812
813    #[test]
814    fn prop_pbkdf2_different_salts_produce_different_outputs() {
815        let mut out1 = [0u8; 32];
816        let mut out2 = [0u8; 32];
817        pbkdf2_sha256(b"password", b"salt_aaa", 1000, &mut out1).expect("pbkdf2 a");
818        pbkdf2_sha256(b"password", b"salt_bbb", 1000, &mut out2).expect("pbkdf2 b");
819        assert_ne!(
820            out1, out2,
821            "different salts must produce different PBKDF2 outputs"
822        );
823    }
824}