1#![forbid(unsafe_code)]
2
3use 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#[derive(Debug, Clone, Copy)]
40pub struct Argon2idStretchParams {
41 pub params: Argon2Params,
43 pub out_len: usize,
45}
46
47#[derive(Debug, Clone, Copy)]
49pub struct ScryptStretchParams {
50 pub log_n: u8,
52 pub r: u32,
54 pub p: u32,
56 pub out_len: usize,
58}
59
60#[derive(Debug, Clone, Copy)]
62pub struct Pbkdf2StretchParams {
63 pub iterations: u32,
65 pub out_len: usize,
67}
68
69#[derive(Debug, Clone, Copy)]
74pub struct BalloonStretchParams {
75 pub space_cost: u64,
77 pub time_cost: u64,
79}
80
81#[derive(Debug, Clone, Copy)]
83pub enum StretchParams {
84 Argon2id(Argon2idStretchParams),
86 Scrypt(ScryptStretchParams),
88 Pbkdf2Sha256(Pbkdf2StretchParams),
90 BalloonSha256(BalloonStretchParams),
92}
93
94impl StretchParams {
95 #[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 #[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
118pub trait KeyStretcher {
123 fn stretch(&self, password: &[u8], salt: &[u8]) -> Result<SecretVec, CryptoError>;
129
130 fn output_len(&self) -> usize;
132
133 fn name(&self) -> &'static str;
135}
136
137#[derive(Debug, Clone, Copy)]
140pub struct Stretcher {
141 params: StretchParams,
142}
143
144impl Stretcher {
145 #[must_use]
147 pub fn new(params: StretchParams) -> Self {
148 Self { params }
149 }
150
151 #[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 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 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 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 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 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}