Skip to main content

oxicrypto_kdf/
stretcher.rs

1#![forbid(unsafe_code)]
2
3//! A unifying [`KeyStretcher`] abstraction over the crate's memory-hard and
4//! iteration-hard password-based key-derivation functions.
5//!
6//! [`KeyStretcher`] is an **object-safe** trait — `Box<dyn KeyStretcher>` works
7//! — exposing a single `stretch(password, salt) -> SecretVec` entry point so
8//! callers can select an algorithm at runtime without committing to a concrete
9//! type. [`Stretcher`] is the built-in implementation; it wraps a
10//! [`StretchParams`] enum that delegates to:
11//!
12//! - **Argon2id** ([`crate::argon2id_derive`])
13//! - **scrypt** ([`crate::scrypt_derive`])
14//! - **PBKDF2-HMAC-SHA-256** ([`crate::pbkdf2_sha256`])
15//! - **Balloon-SHA-256** ([`crate::balloon_sha256`])
16//!
17//! Each variant carries its own cost parameters and the desired derived-key
18//! length; the result is wrapped in a [`SecretVec`] that zeroizes on drop.
19//!
20//! ```
21//! use oxicrypto_kdf::{KeyStretcher, Stretcher, StretchParams};
22//! use oxicrypto_kdf::Pbkdf2StretchParams;
23//!
24//! let stretcher: Box<dyn KeyStretcher> = Box::new(Stretcher::new(
25//!     StretchParams::Pbkdf2Sha256(Pbkdf2StretchParams { iterations: 1000, out_len: 32 }),
26//! ));
27//! let key = stretcher.stretch(b"password", b"salt").expect("derive");
28//! assert_eq!(key.len(), 32);
29//! ```
30
31use oxicrypto_core::{CryptoError, SecretVec};
32
33use crate::argon2_kdf::{argon2id_derive, Argon2Params};
34use crate::balloon::balloon_sha256;
35use crate::pbkdf2_kdf::pbkdf2_sha256;
36use crate::scrypt_kdf::scrypt_derive;
37
38/// Parameters for the Argon2id stretching backend.
39#[derive(Debug, Clone, Copy)]
40pub struct Argon2idStretchParams {
41    /// Argon2 cost parameters (`m_cost`, `t_cost`, `p_cost`).
42    pub params: Argon2Params,
43    /// Derived-key length in bytes (1..=64, per the Argon2 spec).
44    pub out_len: usize,
45}
46
47/// Parameters for the scrypt stretching backend.
48#[derive(Debug, Clone, Copy)]
49pub struct ScryptStretchParams {
50    /// CPU/memory cost factor as logâ‚‚(N).
51    pub log_n: u8,
52    /// Block size (RFC 7914 recommends `r = 8`).
53    pub r: u32,
54    /// Parallelization factor (RFC 7914 recommends `p = 1`).
55    pub p: u32,
56    /// Derived-key length in bytes (> 0).
57    pub out_len: usize,
58}
59
60/// Parameters for the PBKDF2-HMAC-SHA-256 stretching backend.
61#[derive(Debug, Clone, Copy)]
62pub struct Pbkdf2StretchParams {
63    /// Iteration count (> 0).
64    pub iterations: u32,
65    /// Derived-key length in bytes (> 0).
66    pub out_len: usize,
67}
68
69/// Parameters for the Balloon-SHA-256 stretching backend.
70///
71/// Balloon-SHA-256's output length is fixed at 32 bytes (the SHA-256 digest
72/// size), so no `out_len` field is exposed.
73#[derive(Debug, Clone, Copy)]
74pub struct BalloonStretchParams {
75    /// Number of 32-byte blocks held in memory (`>= 1`).
76    pub space_cost: u64,
77    /// Number of mixing rounds (`>= 1`).
78    pub time_cost: u64,
79}
80
81/// Algorithm + parameter selection for a [`Stretcher`].
82#[derive(Debug, Clone, Copy)]
83pub enum StretchParams {
84    /// Argon2id (memory-hard, side-channel-resistant; RFC 9106).
85    Argon2id(Argon2idStretchParams),
86    /// scrypt (memory-hard; RFC 7914).
87    Scrypt(ScryptStretchParams),
88    /// PBKDF2-HMAC-SHA-256 (iteration-hard; RFC 8018 / NIST SP 800-132).
89    Pbkdf2Sha256(Pbkdf2StretchParams),
90    /// Balloon-SHA-256 (memory-hard, cache-hard; ASIACRYPT 2016). 32-byte output.
91    BalloonSha256(BalloonStretchParams),
92}
93
94impl StretchParams {
95    /// Length in bytes of the key this configuration derives.
96    #[must_use]
97    pub fn output_len(&self) -> usize {
98        match self {
99            StretchParams::Argon2id(p) => p.out_len,
100            StretchParams::Scrypt(p) => p.out_len,
101            StretchParams::Pbkdf2Sha256(p) => p.out_len,
102            StretchParams::BalloonSha256(_) => 32,
103        }
104    }
105
106    /// Stable, human-readable algorithm name.
107    #[must_use]
108    pub fn name(&self) -> &'static str {
109        match self {
110            StretchParams::Argon2id(_) => "argon2id",
111            StretchParams::Scrypt(_) => "scrypt",
112            StretchParams::Pbkdf2Sha256(_) => "pbkdf2-sha256",
113            StretchParams::BalloonSha256(_) => "balloon-sha256",
114        }
115    }
116}
117
118/// An object-safe key-stretching interface.
119///
120/// Implemented by [`Stretcher`]. Stretch a low-entropy `password` together with
121/// a `salt` into a high-cost-derived key returned as a [`SecretVec`].
122pub trait KeyStretcher {
123    /// Derive a key from `password` and `salt`.
124    ///
125    /// # Errors
126    /// Returns the underlying [`CryptoError`] from the selected backend (e.g.
127    /// [`CryptoError::BadInput`] for invalid parameters or salt length).
128    fn stretch(&self, password: &[u8], salt: &[u8]) -> Result<SecretVec, CryptoError>;
129
130    /// Length in bytes of the derived key.
131    fn output_len(&self) -> usize;
132
133    /// Stable, human-readable algorithm name.
134    fn name(&self) -> &'static str;
135}
136
137/// The built-in [`KeyStretcher`] implementation, parameterized by
138/// [`StretchParams`].
139#[derive(Debug, Clone, Copy)]
140pub struct Stretcher {
141    params: StretchParams,
142}
143
144impl Stretcher {
145    /// Construct a stretcher from an algorithm + parameter selection.
146    #[must_use]
147    pub fn new(params: StretchParams) -> Self {
148        Self { params }
149    }
150
151    /// Borrow the underlying parameter selection.
152    #[must_use]
153    pub fn params(&self) -> &StretchParams {
154        &self.params
155    }
156}
157
158impl KeyStretcher for Stretcher {
159    fn stretch(&self, password: &[u8], salt: &[u8]) -> Result<SecretVec, CryptoError> {
160        match self.params {
161            StretchParams::Argon2id(p) => {
162                if p.out_len == 0 {
163                    return Err(CryptoError::BadInput);
164                }
165                let mut out = vec![0u8; p.out_len];
166                argon2id_derive(password, salt, p.params, &mut out)?;
167                Ok(SecretVec::new(out))
168            }
169            StretchParams::Scrypt(p) => {
170                if p.out_len == 0 {
171                    return Err(CryptoError::BadInput);
172                }
173                let mut out = vec![0u8; p.out_len];
174                scrypt_derive(password, salt, p.log_n, p.r, p.p, &mut out)?;
175                Ok(SecretVec::new(out))
176            }
177            StretchParams::Pbkdf2Sha256(p) => {
178                if p.out_len == 0 {
179                    return Err(CryptoError::BadInput);
180                }
181                let mut out = vec![0u8; p.out_len];
182                pbkdf2_sha256(password, salt, p.iterations, &mut out)?;
183                Ok(SecretVec::new(out))
184            }
185            StretchParams::BalloonSha256(p) => {
186                let mut out = vec![0u8; 32];
187                balloon_sha256(password, salt, p.space_cost, p.time_cost, &mut out)?;
188                Ok(SecretVec::new(out))
189            }
190        }
191    }
192
193    fn output_len(&self) -> usize {
194        self.params.output_len()
195    }
196
197    fn name(&self) -> &'static str {
198        self.params.name()
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    const SALT16: &[u8] = b"0123456789abcdef";
207
208    fn check_backend(params: StretchParams, expect_len: usize) {
209        let stretcher = Stretcher::new(params);
210        assert_eq!(stretcher.output_len(), expect_len);
211
212        // Determinism through the trait.
213        let k1 = stretcher.stretch(b"password", SALT16).expect("stretch 1");
214        let k2 = stretcher.stretch(b"password", SALT16).expect("stretch 2");
215        assert_eq!(k1.as_bytes(), k2.as_bytes(), "{}", stretcher.name());
216        assert_eq!(k1.len(), expect_len);
217        assert_ne!(k1.as_bytes(), vec![0u8; expect_len].as_slice());
218
219        // Different salt ⇒ different key.
220        let k3 = stretcher
221            .stretch(b"password", b"fedcba9876543210")
222            .expect("stretch 3");
223        assert_ne!(
224            k1.as_bytes(),
225            k3.as_bytes(),
226            "{} salt sensitivity",
227            stretcher.name()
228        );
229    }
230
231    #[test]
232    fn argon2id_backend() {
233        check_backend(
234            StretchParams::Argon2id(Argon2idStretchParams {
235                params: Argon2Params::TEST_PARAMS,
236                out_len: 32,
237            }),
238            32,
239        );
240    }
241
242    #[test]
243    fn scrypt_backend() {
244        check_backend(
245            StretchParams::Scrypt(ScryptStretchParams {
246                log_n: 4,
247                r: 8,
248                p: 1,
249                out_len: 32,
250            }),
251            32,
252        );
253    }
254
255    #[test]
256    fn pbkdf2_backend() {
257        check_backend(
258            StretchParams::Pbkdf2Sha256(Pbkdf2StretchParams {
259                iterations: 1000,
260                out_len: 48,
261            }),
262            48,
263        );
264    }
265
266    #[test]
267    fn balloon_backend() {
268        check_backend(
269            StretchParams::BalloonSha256(BalloonStretchParams {
270                space_cost: 8,
271                time_cost: 3,
272            }),
273            32,
274        );
275    }
276
277    #[test]
278    fn trait_object_dispatch() {
279        // Heterogeneous list of boxed stretchers exercises object-safety.
280        let backends: Vec<Box<dyn KeyStretcher>> = vec![
281            Box::new(Stretcher::new(StretchParams::Argon2id(
282                Argon2idStretchParams {
283                    params: Argon2Params::TEST_PARAMS,
284                    out_len: 32,
285                },
286            ))),
287            Box::new(Stretcher::new(StretchParams::Scrypt(ScryptStretchParams {
288                log_n: 4,
289                r: 8,
290                p: 1,
291                out_len: 32,
292            }))),
293            Box::new(Stretcher::new(StretchParams::Pbkdf2Sha256(
294                Pbkdf2StretchParams {
295                    iterations: 1000,
296                    out_len: 32,
297                },
298            ))),
299            Box::new(Stretcher::new(StretchParams::BalloonSha256(
300                BalloonStretchParams {
301                    space_cost: 8,
302                    time_cost: 3,
303                },
304            ))),
305        ];
306        for b in &backends {
307            let key = b.stretch(b"password", SALT16).expect("dispatch stretch");
308            assert_eq!(key.len(), b.output_len(), "{}", b.name());
309        }
310        // Each algorithm should produce a distinct derived key for identical input.
311        let outs: Vec<Vec<u8>> = backends
312            .iter()
313            .map(|b| {
314                b.stretch(b"password", SALT16)
315                    .expect("k")
316                    .as_bytes()
317                    .to_vec()
318            })
319            .collect();
320        for i in 0..outs.len() {
321            for j in (i + 1)..outs.len() {
322                assert_ne!(outs[i], outs[j], "backends {i} and {j} collided");
323            }
324        }
325    }
326
327    #[test]
328    fn matches_standalone_pbkdf2() {
329        // Trait output must equal the standalone function for the same params.
330        let stretcher = Stretcher::new(StretchParams::Pbkdf2Sha256(Pbkdf2StretchParams {
331            iterations: 1000,
332            out_len: 32,
333        }));
334        let via_trait = stretcher.stretch(b"password", b"salt").expect("trait");
335        let mut direct = [0u8; 32];
336        pbkdf2_sha256(b"password", b"salt", 1000, &mut direct).expect("direct");
337        assert_eq!(via_trait.as_bytes(), &direct[..]);
338    }
339
340    #[test]
341    fn zero_output_len_rejected() {
342        let stretcher = Stretcher::new(StretchParams::Pbkdf2Sha256(Pbkdf2StretchParams {
343            iterations: 1000,
344            out_len: 0,
345        }));
346        assert_eq!(
347            stretcher.stretch(b"pw", SALT16).err(),
348            Some(CryptoError::BadInput)
349        );
350    }
351}