Skip to main content

oxicrypto_kdf/
scrypt_kdf.rs

1#![forbid(unsafe_code)]
2
3//! Scrypt password-based key derivation for the OxiCrypto stack.
4//!
5//! Backed by `scrypt` (RustCrypto, Pure Rust).
6//! Exposes a low-level interface with explicit N, r, p parameters.
7
8use oxicrypto_core::{CryptoError, PasswordHash as PasswordHashTrait, PasswordHashParams};
9use scrypt::{scrypt, Params as RcScryptParams};
10
11// ---------------------------------------------------------------------------
12// ScryptParams — our own strongly-typed parameter struct
13// ---------------------------------------------------------------------------
14
15/// Parameters for scrypt key derivation.
16///
17/// RFC 7914 recommends `log_n=14` (N=16384), `r=8`, `p=1` for interactive
18/// logins and higher values for sensitive use-cases.
19///
20/// Note: `log_n` encodes N as log₂(N), e.g. `log_n=14` means N=16384.
21#[derive(Debug, Clone, Copy)]
22pub struct ScryptParams {
23    /// CPU/memory cost factor as log₂(N).  N must be a power of 2.
24    pub log_n: u8,
25    /// Block size.  RFC 7914 recommends `r=8`.
26    pub r: u32,
27    /// Parallelization factor.  RFC 7914 recommends `p=1`.
28    pub p: u32,
29}
30
31impl ScryptParams {
32    /// Create new parameters, returning an error if they are invalid.
33    #[must_use = "ScryptParams creation result must be checked"]
34    pub fn new(log_n: u8, r: u32, p: u32) -> Result<Self, CryptoError> {
35        // Validate by constructing the underlying scrypt params.
36        RcScryptParams::new(log_n, r, p).map_err(|_| CryptoError::BadInput)?;
37        Ok(Self { log_n, r, p })
38    }
39
40    /// Interactive login preset.
41    ///
42    /// N=32768 (log_n=15), r=8, p=1
43    /// Provides ≈32 MiB memory and ~100–200 ms on a modern CPU.
44    #[must_use]
45    pub fn interactive() -> Self {
46        Self {
47            log_n: 15,
48            r: 8,
49            p: 1,
50        }
51    }
52
53    /// Moderate preset — balanced security and speed.
54    ///
55    /// N=131072 (log_n=17), r=8, p=1
56    /// Provides ≈128 MiB memory and ~1 s on a modern CPU.
57    #[must_use]
58    pub fn moderate() -> Self {
59        Self {
60            log_n: 17,
61            r: 8,
62            p: 1,
63        }
64    }
65
66    /// Sensitive (high-security) preset.
67    ///
68    /// N=1048576 (log_n=20), r=8, p=1
69    /// Provides ≈1 GiB memory and ~5–30 s on a modern CPU.
70    #[must_use]
71    pub fn sensitive() -> Self {
72        Self {
73            log_n: 20,
74            r: 8,
75            p: 1,
76        }
77    }
78}
79
80impl PasswordHashParams for ScryptParams {
81    /// Memory cost approximation: 128 × N × r bytes expressed in KiB.
82    fn memory_cost(&self) -> Option<u32> {
83        // 128 * r * N / 1024 = 128 * r * 2^log_n / 1024
84        let n: u64 = 1u64 << self.log_n;
85        let kib = 128u64.saturating_mul(n).saturating_mul(self.r as u64) / 1024;
86        u32::try_from(kib).ok()
87    }
88
89    fn time_cost(&self) -> Option<u32> {
90        // scrypt doesn't have a separate time cost; log_n encodes CPU+memory.
91        None
92    }
93
94    fn parallelism(&self) -> Option<u32> {
95        Some(self.p)
96    }
97}
98
99// ---------------------------------------------------------------------------
100// Low-level standalone function (existing API — preserved for compatibility)
101// ---------------------------------------------------------------------------
102
103/// Scrypt key derivation.
104///
105/// # Arguments
106/// - `password` — secret password bytes
107/// - `salt`     — random salt
108/// - `log_n`    — CPU/memory cost factor as log2(N); N must be a power of 2.
109///   RFC 7914 §2 uses N=1024 (log_n=10) for interactive logins.
110/// - `r`        — block size (RFC 7914 recommends r=8)
111/// - `p`        — parallelisation factor (RFC 7914 recommends p=1)
112/// - `out`      — output buffer (any length > 0)
113#[must_use = "scrypt derive result must be checked"]
114pub fn scrypt_derive(
115    password: &[u8],
116    salt: &[u8],
117    log_n: u8,
118    r: u32,
119    p: u32,
120    out: &mut [u8],
121) -> Result<(), CryptoError> {
122    if out.is_empty() {
123        return Err(CryptoError::BadInput);
124    }
125    let params = RcScryptParams::new(log_n, r, p).map_err(|_| CryptoError::BadInput)?;
126    scrypt(password, salt, &params, out).map_err(|_| CryptoError::Internal("scrypt failed"))
127}
128
129// ---------------------------------------------------------------------------
130// ScryptHasher — implements the `PasswordHash` trait from `oxicrypto-core`
131// ---------------------------------------------------------------------------
132
133/// A scrypt password hasher that bundles its own cost parameters.
134///
135/// Implements [`PasswordHash`](oxicrypto_core::PasswordHash) so it can be
136/// used polymorphically with [`crate::verify_password`].
137///
138/// # Design note — `params` argument is ignored
139/// The [`PasswordHash::hash_password`](oxicrypto_core::PasswordHash::hash_password)
140/// trait method accepts a `params: &dyn PasswordHashParams` argument, but this
141/// implementation ignores it and uses `self.params` instead. Callers that
142/// need different scrypt parameters should construct a new `ScryptHasher`
143/// with the desired `ScryptParams` rather than passing a different
144/// `PasswordHashParams` object.
145#[derive(Debug, Clone, Copy)]
146pub struct ScryptHasher {
147    /// Scrypt cost parameters.
148    pub params: ScryptParams,
149}
150
151impl ScryptHasher {
152    /// Create a new hasher with explicit parameters.
153    ///
154    /// Returns an error if the parameters are out of range.
155    pub fn new(params: ScryptParams) -> Result<Self, CryptoError> {
156        // Validate params eagerly.
157        RcScryptParams::new(params.log_n, params.r, params.p).map_err(|_| CryptoError::BadInput)?;
158        Ok(Self { params })
159    }
160
161    /// Create a new hasher, panicking if params are invalid.
162    ///
163    /// Prefer [`ScryptHasher::new`] in production code.
164    pub fn new_checked(params: ScryptParams) -> Self {
165        Self::new(params).expect("invalid ScryptParams")
166    }
167
168    /// Interactive login preset.
169    #[must_use]
170    pub fn interactive() -> Self {
171        Self {
172            params: ScryptParams::interactive(),
173        }
174    }
175
176    /// Moderate (balanced) preset.
177    #[must_use]
178    pub fn moderate() -> Self {
179        Self {
180            params: ScryptParams::moderate(),
181        }
182    }
183
184    /// Sensitive (high-security) preset.
185    #[must_use]
186    pub fn sensitive() -> Self {
187        Self {
188            params: ScryptParams::sensitive(),
189        }
190    }
191}
192
193impl PasswordHashTrait for ScryptHasher {
194    fn name(&self) -> &'static str {
195        "scrypt"
196    }
197
198    fn hash_password(
199        &self,
200        password: &[u8],
201        salt: &[u8],
202        _params: &dyn PasswordHashParams,
203        out: &mut [u8],
204    ) -> Result<(), CryptoError> {
205        scrypt_derive(
206            password,
207            salt,
208            self.params.log_n,
209            self.params.r,
210            self.params.p,
211            out,
212        )
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    // Use very small scrypt params so tests complete quickly.
221    fn test_params() -> ScryptParams {
222        ScryptParams {
223            log_n: 1,
224            r: 1,
225            p: 1,
226        }
227    }
228
229    fn test_hasher() -> ScryptHasher {
230        ScryptHasher::new(test_params()).expect("test params valid")
231    }
232
233    const SALT: &[u8] = b"test-salt-16byte";
234
235    #[test]
236    fn scrypt_derive_deterministic() {
237        let p = test_params();
238        let mut out1 = [0u8; 32];
239        let mut out2 = [0u8; 32];
240        scrypt_derive(b"password", SALT, p.log_n, p.r, p.p, &mut out1).expect("derive 1");
241        scrypt_derive(b"password", SALT, p.log_n, p.r, p.p, &mut out2).expect("derive 2");
242        assert_eq!(out1, out2, "scrypt must be deterministic");
243        assert_ne!(out1, [0u8; 32]);
244    }
245
246    #[test]
247    fn scrypt_derive_empty_output_errors() {
248        let p = test_params();
249        let result = scrypt_derive(b"password", SALT, p.log_n, p.r, p.p, &mut []);
250        assert_eq!(result, Err(CryptoError::BadInput));
251    }
252
253    #[test]
254    fn password_hash_trait_deterministic() {
255        let hasher = test_hasher();
256        let mut out1 = [0u8; 32];
257        let mut out2 = [0u8; 32];
258        hasher
259            .hash_password(b"password", SALT, &hasher.params, &mut out1)
260            .expect("hash 1");
261        hasher
262            .hash_password(b"password", SALT, &hasher.params, &mut out2)
263            .expect("hash 2");
264        assert_eq!(out1, out2);
265        assert_ne!(out1, [0u8; 32]);
266    }
267
268    #[test]
269    fn preset_cost_ordering() {
270        let interactive = ScryptParams::interactive();
271        let moderate = ScryptParams::moderate();
272        let sensitive = ScryptParams::sensitive();
273        // N increases with preset (log_n is monotone)
274        assert!(sensitive.log_n > moderate.log_n);
275        assert!(moderate.log_n > interactive.log_n);
276        // memory_cost is monotonically increasing
277        assert!(sensitive.memory_cost() > moderate.memory_cost());
278        assert!(moderate.memory_cost() > interactive.memory_cost());
279    }
280
281    #[test]
282    fn scrypt_params_password_hash_params_impl() {
283        let p = ScryptParams::interactive();
284        assert!(p.memory_cost().is_some());
285        assert!(p.time_cost().is_none());
286        assert_eq!(p.parallelism(), Some(1));
287    }
288
289    #[test]
290    fn hasher_name() {
291        assert_eq!(test_hasher().name(), "scrypt");
292    }
293}