Skip to main content

devolutions_crypto/key_derivation/
mod.rs

1//! Module for key derivation. Derives a key or password into a `SecretKey`
2//! and returns the `DerivationParameters` needed to reproduce the derivation.
3//!
4//! # Example (Argon2 — default)
5//! ```rust
6//! use devolutions_crypto::key_derivation::Argon2;
7//!
8//! let password = b"a very strong password";
9//! let argon2 = Argon2::new();
10//! let (secret_key, params) = argon2.derive(password).expect("derivation should not fail");
11//! // Serialize params to re-derive later:
12//! let params_bytes: Vec<u8> = params.into();
13//! ```
14//!
15//! # Example (PBKDF2)
16//! ```rust
17//! use devolutions_crypto::key_derivation::Pbkdf2;
18//!
19//! let password = b"a very strong password";
20//! let pbkdf2 = Pbkdf2::new();
21//! let (secret_key, params) = pbkdf2.derive(password).expect("derivation should not fail");
22//! ```
23
24mod key_derivation_v1;
25mod key_derivation_v2;
26
27pub use key_derivation_v1::Pbkdf2;
28pub use key_derivation_v2::Argon2;
29
30use key_derivation_v1::KeyDerivationV1;
31use key_derivation_v2::KeyDerivationV2;
32
33use std::borrow::Borrow;
34use std::convert::TryFrom;
35
36#[cfg(feature = "fuzz")]
37use arbitrary::Arbitrary;
38
39#[cfg(feature = "wbindgen")]
40use wasm_bindgen::prelude::*;
41
42use zeroize::Zeroizing;
43
44use crate::key::{secret_key_from_raw, SecretKey};
45
46#[cfg(feature = "fuzz")]
47use crate::Argon2Parameters;
48use crate::{DataType, Error, Header, HeaderType, KeyDerivationVersion, Result};
49
50use super::enums::KeyDerivationSubtype;
51
52// ── DerivationParameters ─────────────────────────────────────────────────────
53
54/// Serializable parameters that fully describe a completed key derivation.
55/// Can be stored alongside a user record to re-derive the same key later.
56#[derive(Clone, Debug)]
57#[cfg_attr(feature = "fuzz", derive(Arbitrary))]
58#[cfg_attr(feature = "wbindgen", wasm_bindgen(inspectable))]
59pub struct DerivationParameters {
60    pub(crate) header: Header<DerivationParameters>,
61    pub(super) payload: DerivationParametersPayload,
62}
63
64impl HeaderType for DerivationParameters {
65    type Version = KeyDerivationVersion;
66    type Subtype = KeyDerivationSubtype;
67
68    fn data_type() -> DataType {
69        DataType::KeyDerivation
70    }
71}
72
73#[derive(Clone, Debug)]
74#[cfg_attr(feature = "fuzz", derive(Arbitrary))]
75pub(super) enum DerivationParametersPayload {
76    V1(KeyDerivationV1),
77    V2(KeyDerivationV2),
78}
79
80#[cfg(feature = "fuzz")]
81impl<'a> Arbitrary<'a> for KeyDerivationV1 {
82    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
83        Ok(KeyDerivationV1 {
84            iterations: u32::arbitrary(u)?,
85            salt: Vec::<u8>::arbitrary(u)?,
86        })
87    }
88}
89
90#[cfg(feature = "fuzz")]
91impl<'a> Arbitrary<'a> for KeyDerivationV2 {
92    fn arbitrary(_u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
93        Ok(KeyDerivationV2 {
94            params: Argon2Parameters::default(),
95        })
96    }
97}
98
99impl From<DerivationParameters> for Vec<u8> {
100    fn from(data: DerivationParameters) -> Self {
101        let mut header: Self = data.header.borrow().into();
102        let mut payload: Self = data.payload.into();
103        header.append(&mut payload);
104        header
105    }
106}
107
108impl TryFrom<&[u8]> for DerivationParameters {
109    type Error = Error;
110
111    fn try_from(data: &[u8]) -> Result<Self> {
112        if data.len() < Header::len() {
113            return Err(Error::InvalidLength);
114        }
115
116        let header = Header::try_from(&data[0..Header::len()])?;
117
118        let payload = match header.version {
119            KeyDerivationVersion::V1 => {
120                DerivationParametersPayload::V1(KeyDerivationV1::try_from(&data[Header::len()..])?)
121            }
122            KeyDerivationVersion::V2 => {
123                DerivationParametersPayload::V2(KeyDerivationV2::try_from(&data[Header::len()..])?)
124            }
125            KeyDerivationVersion::Latest => return Err(Error::UnknownVersion),
126        };
127
128        Ok(Self { header, payload })
129    }
130}
131
132impl From<DerivationParametersPayload> for Vec<u8> {
133    fn from(payload: DerivationParametersPayload) -> Self {
134        match payload {
135            DerivationParametersPayload::V1(v1) => Vec::from(&v1),
136            DerivationParametersPayload::V2(v2) => Vec::from(&v2),
137        }
138    }
139}
140
141impl DerivationParameters {
142    /// Derives a [`SecretKey`] from `password` using these derivation parameters.
143    pub fn derive(&self, password: &[u8]) -> Result<SecretKey> {
144        let raw = match &self.payload {
145            DerivationParametersPayload::V1(v1) => v1.derive(password),
146            DerivationParametersPayload::V2(v2) => v2.derive(password)?,
147        };
148
149        secret_key_from_raw(raw)
150    }
151
152    /// Re-derives raw bytes from a password using the stored algorithm and parameters.
153    pub fn compute(&self, password: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
154        match &self.payload {
155            DerivationParametersPayload::V1(v1) => Ok(v1.derive(password)),
156            DerivationParametersPayload::V2(v2) => v2.derive(password),
157        }
158    }
159
160    /// Returns the byte-length of the hash that [`compute`](Self::compute) will produce.
161    pub fn output_length(&self) -> usize {
162        match &self.payload {
163            DerivationParametersPayload::V1(_) => key_derivation_v1::KEY_LENGTH,
164            DerivationParametersPayload::V2(v2) => v2.params.length as usize,
165        }
166    }
167}
168
169/// Derives a `SecretKey` from `password` using the algorithm selected by `version`.
170///
171/// * `KeyDerivationVersion::Latest` and `KeyDerivationVersion::V2` use **Argon2id** (recommended).
172/// * `KeyDerivationVersion::V1` uses **PBKDF2-HMAC-SHA256**.
173///
174/// A random salt is generated automatically; the returned [`DerivationParameters`] can be
175/// stored alongside the protected data so the same key can be reproduced later.
176///
177/// # Example
178/// ```rust
179/// use devolutions_crypto::key_derivation::{derive_key, DerivationParameters};
180/// use devolutions_crypto::KeyDerivationVersion;
181///
182/// let password = b"a very strong password";
183/// let (secret_key, params) = derive_key(password, KeyDerivationVersion::Latest).expect("derivation should not fail");
184/// // Serialize params to re-derive later:
185/// let params_bytes: Vec<u8> = params.into();
186/// ```
187pub fn derive_key(
188    password: &[u8],
189    version: KeyDerivationVersion,
190) -> Result<(SecretKey, DerivationParameters)> {
191    match version {
192        KeyDerivationVersion::V1 => Pbkdf2::new().derive(password),
193        KeyDerivationVersion::V2 | KeyDerivationVersion::Latest => Argon2::new().derive(password),
194    }
195}
196
197// ── Tests ─────────────────────────────────────────────────────────────────────
198
199#[cfg(test)]
200mod tests {
201    use std::convert::TryFrom;
202
203    use crate::key::secret_key_from_raw;
204    use crate::Argon2Parameters;
205
206    use super::*;
207
208    // ── Pbkdf2 ───────────────────────────────────────────────────────────────
209
210    #[test]
211    fn pbkdf2_derive_same_input_same_salt_produces_same_key() {
212        let pbkdf2 = Pbkdf2::with_params(10);
213        let salt = b"fixed_salt_value";
214        let (key1, _) = pbkdf2.derive_with_salt(b"password", salt).unwrap();
215        let (key2, _) = pbkdf2.derive_with_salt(b"password", salt).unwrap();
216        assert_eq!(key1.as_bytes(), key2.as_bytes());
217    }
218
219    #[test]
220    fn pbkdf2_derive_different_password_produces_different_key() {
221        let pbkdf2 = Pbkdf2::with_params(10);
222        let salt = b"fixed_salt_value";
223        let (key1, _) = pbkdf2.derive_with_salt(b"password1", salt).unwrap();
224        let (key2, _) = pbkdf2.derive_with_salt(b"password2", salt).unwrap();
225        assert_ne!(key1.as_bytes(), key2.as_bytes());
226    }
227
228    #[test]
229    fn pbkdf2_derive_different_salt_produces_different_key() {
230        let pbkdf2 = Pbkdf2::with_params(10);
231        let (key1, _) = pbkdf2.derive_with_salt(b"password", b"salt_one").unwrap();
232        let (key2, _) = pbkdf2.derive_with_salt(b"password", b"salt_two").unwrap();
233        assert_ne!(key1.as_bytes(), key2.as_bytes());
234    }
235
236    #[test]
237    fn pbkdf2_derive_generates_random_salt() {
238        let pbkdf2 = Pbkdf2::with_params(10);
239        let (_, params1) = pbkdf2.derive(b"password").unwrap();
240        let (_, params2) = pbkdf2.derive(b"password").unwrap();
241        let bytes1: Vec<u8> = params1.into();
242        let bytes2: Vec<u8> = params2.into();
243        assert_ne!(bytes1, bytes2);
244    }
245
246    #[test]
247    fn pbkdf2_derive_with_salt_roundtrip() {
248        let pbkdf2 = Pbkdf2::with_params(10);
249        let salt = b"roundtrip_salt!!";
250        let (key1, params) = pbkdf2.derive_with_salt(b"password", salt).unwrap();
251
252        // Serialize and deserialize params
253        let params_bytes: Vec<u8> = params.into();
254        let params2 = DerivationParameters::try_from(params_bytes.as_slice()).unwrap();
255
256        // Re-derive using deserialized params
257        let payload_bytes: Vec<u8> = params2.payload.into();
258        let v1 = KeyDerivationV1::try_from(payload_bytes.as_slice()).unwrap();
259        let raw = v1.derive(b"password");
260        let key2 = secret_key_from_raw(raw).unwrap();
261
262        assert_eq!(key1.as_bytes(), key2.as_bytes());
263    }
264
265    #[test]
266    fn pbkdf2_derivation_parameters_serialize_roundtrip() {
267        let pbkdf2 = Pbkdf2::with_params(12345);
268        let (_, params) = pbkdf2
269            .derive_with_salt(b"password", b"some_salt_here!!")
270            .unwrap();
271        let bytes: Vec<u8> = params.into();
272        let params2 = DerivationParameters::try_from(bytes.as_slice()).unwrap();
273        assert_eq!(params2.header.version, KeyDerivationVersion::V1);
274    }
275
276    // ── Argon2 ───────────────────────────────────────────────────────────────
277
278    #[test]
279    fn argon2_derive_same_params_same_input_produces_same_key() {
280        let mut argon2_params = Argon2Parameters::default();
281        argon2_params.iterations = 2;
282        argon2_params.memory = 64;
283        // Fix the salt so derivation is deterministic
284        argon2_params.set_salt(b"fixed_salt_16byt".to_vec());
285
286        let argon2 = Argon2::with_params(argon2_params.clone());
287        let (key1, _) = argon2.derive(b"password").unwrap();
288
289        let argon2 = Argon2::with_params(argon2_params);
290        let (key2, _) = argon2.derive(b"password").unwrap();
291
292        assert_eq!(key1.as_bytes(), key2.as_bytes());
293    }
294
295    #[test]
296    fn argon2_derive_different_salt_produces_different_key() {
297        // Two calls to new() generate different salts
298        let (key1, _) = Argon2::new().derive(b"password").unwrap();
299        let (key2, _) = Argon2::new().derive(b"password").unwrap();
300        assert_ne!(key1.as_bytes(), key2.as_bytes());
301    }
302
303    #[test]
304    fn argon2_derivation_parameters_serialize_roundtrip() {
305        let mut argon2_params = Argon2Parameters::default();
306        argon2_params.iterations = 2;
307        argon2_params.memory = 64;
308        let (_, params) = Argon2::with_params(argon2_params)
309            .derive(b"password")
310            .unwrap();
311        let bytes: Vec<u8> = params.into();
312        let params2 = DerivationParameters::try_from(bytes.as_slice()).unwrap();
313        assert_eq!(params2.header.version, KeyDerivationVersion::V2);
314    }
315
316    // ── validate_header ───────────────────────────────────────────────────────
317
318    #[test]
319    fn validate_header_accepts_key_derivation() {
320        use crate::utils::validate_header;
321        let (_, params) = Pbkdf2::with_params(10)
322            .derive_with_salt(b"pw", b"salt_16bytes!!!")
323            .unwrap();
324        let bytes: Vec<u8> = params.into();
325        assert!(validate_header(&bytes, DataType::KeyDerivation));
326    }
327
328    #[test]
329    fn validate_header_rejects_wrong_type() {
330        use crate::utils::validate_header;
331        use crate::DataType;
332        let (_, params) = Pbkdf2::with_params(10)
333            .derive_with_salt(b"pw", b"salt_16bytes!!!")
334            .unwrap();
335        let bytes: Vec<u8> = params.into();
336        assert!(!validate_header(&bytes, DataType::Ciphertext));
337        assert!(!validate_header(&bytes, DataType::Key));
338        assert!(!validate_header(&bytes, DataType::PasswordHash));
339    }
340}