Skip to main content

bitwarden_crypto/keys/
kdf.rs

1use std::{num::NonZeroU32, pin::Pin};
2
3use hybrid_array::Array;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use sha2::Digest;
7use typenum::U32;
8use zeroize::Zeroize;
9
10use crate::{
11    CryptoError,
12    slow_crypto_span::{SlowCryptoOp, measure},
13};
14
15const PBKDF2_MIN_ITERATIONS: u32 = 5000;
16
17const ARGON2ID_MIN_MEMORY: u32 = 16 * 1024;
18const ARGON2ID_MIN_ITERATIONS: u32 = 2;
19const ARGON2ID_MIN_PARALLELISM: u32 = 1;
20
21/// Holding struct for key material derived from a KDF.
22///
23/// The internal key material should not be used directly for cryptographic operations. Instead it
24/// MUST be converted to the appropriate type such as `SymmetricCryptoKey`, `MasterKey` or any other
25/// key type. This can be done by either directly consuming the key material or by stretching it
26/// further using HKDF (HMAC-based Key Derivation Function).
27///
28/// Uses a pinned heap data structure, as noted in [Pinned heap data][crate#pinned-heap-data]
29#[cfg_attr(feature = "dangerous-crypto-debug", derive(Debug))]
30pub struct KdfDerivedKeyMaterial(pub(super) Pin<Box<Array<u8, U32>>>);
31
32impl KdfDerivedKeyMaterial {
33    /// Derive a key from a secret and salt using the provided KDF.
34    pub(super) fn derive_kdf_key(
35        secret: &[u8],
36        salt: &[u8],
37        kdf: &Kdf,
38    ) -> Result<Self, CryptoError> {
39        match kdf {
40            Kdf::PBKDF2 { iterations } => {
41                let iterations = iterations.get();
42                if iterations < PBKDF2_MIN_ITERATIONS {
43                    return Err(CryptoError::InsufficientKdfParameters);
44                }
45
46                let mut hash = measure(SlowCryptoOp::Pbkdf2, || {
47                    crate::util::pbkdf2(secret, salt, iterations)
48                });
49
50                let key_material = Box::pin(hash.into());
51                hash.zeroize();
52                Ok(KdfDerivedKeyMaterial(key_material))
53            }
54            Kdf::Argon2id {
55                iterations,
56                memory,
57                parallelism,
58            } => {
59                let memory = memory.get() * 1024; // Convert MiB to KiB;
60                let iterations = iterations.get();
61                let parallelism = parallelism.get();
62
63                if memory < ARGON2ID_MIN_MEMORY
64                    || iterations < ARGON2ID_MIN_ITERATIONS
65                    || parallelism < ARGON2ID_MIN_PARALLELISM
66                {
67                    return Err(CryptoError::InsufficientKdfParameters);
68                }
69
70                let salt_sha = sha2::Sha256::new().chain_update(salt).finalize();
71
72                let mut hash = Box::pin(Array::<u8, U32>::default());
73
74                use argon2::*;
75                let params = Params::new(memory, iterations, parallelism, Some(32))?;
76                let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
77                measure(SlowCryptoOp::Argon2id, || {
78                    argon.hash_password_into(secret, &salt_sha, hash.as_mut_slice())
79                })?;
80
81                // Argon2 is using some stack memory that is not zeroed. Eventually some function
82                // will overwrite the stack, but we use this trick to force the used
83                // stack to be zeroed.
84                crate::util::clear_stack();
85
86                Ok(KdfDerivedKeyMaterial(hash))
87            }
88        }
89    }
90
91    /// Derives a users master key from their password, email and KDF.
92    ///
93    /// Note: the email is trimmed and converted to lowercase before being used.
94    pub(super) fn derive(password: &str, email: &str, kdf: &Kdf) -> Result<Self, CryptoError> {
95        Self::derive_kdf_key(
96            password.as_bytes(),
97            email.trim().to_lowercase().as_bytes(),
98            kdf,
99        )
100    }
101}
102
103#[deprecated(
104    note = "This function is only meant as a temporary stop-gap to expose KDF derivation in PureCrypto until the higher-level consumers are moved to the SDK directly. DO NOT USE THIS OUTSIDE OF PureCrypto!"
105)]
106/// Derives KDF material given a password, salt and kdf configuration. This function is
107/// a stop-gap solution and should not be used outside of PureCrypto.
108///
109/// The clean-up ticket is tracked here:
110/// `https://bitwarden.atlassian.net/browse/PM-23168`
111pub fn dangerous_derive_kdf_material(
112    password: &[u8],
113    salt: &[u8],
114    kdf: &Kdf,
115) -> Result<Vec<u8>, CryptoError> {
116    KdfDerivedKeyMaterial::derive_kdf_key(password, salt, kdf).map(|kdf_key| kdf_key.0.to_vec())
117}
118
119/// Key Derivation Function for Bitwarden Account
120///
121/// In Bitwarden accounts can use multiple KDFs to derive their master key from their password. This
122/// Enum represents all the possible KDFs.
123#[allow(missing_docs)]
124#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone, PartialEq)]
125#[serde(rename_all = "camelCase", deny_unknown_fields)]
126#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
127#[bitwarden_ffi::wasm_record]
128pub enum Kdf {
129    PBKDF2 {
130        iterations: NonZeroU32,
131    },
132    Argon2id {
133        iterations: NonZeroU32,
134        memory: NonZeroU32,
135        parallelism: NonZeroU32,
136    },
137}
138
139#[cfg(feature = "wasm")]
140impl TryFrom<wasm_bindgen::JsValue> for Kdf {
141    type Error = serde_wasm_bindgen::Error;
142
143    fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
144        serde_wasm_bindgen::from_value(value)
145    }
146}
147
148impl Kdf {
149    /// Default KDF for new encryption V1 accounts.
150    pub fn default_pbkdf2() -> Kdf {
151        Kdf::PBKDF2 {
152            iterations: default_pbkdf2_iterations(),
153        }
154    }
155
156    /// Default KDF for new encryption V2 accounts.
157    pub fn default_argon2() -> Kdf {
158        Kdf::Argon2id {
159            iterations: default_argon2_iterations(),
160            memory: default_argon2_memory(),
161            parallelism: default_argon2_parallelism(),
162        }
163    }
164}
165
166/// Default PBKDF2 iterations
167fn default_pbkdf2_iterations() -> NonZeroU32 {
168    NonZeroU32::new(600_000).expect("Non-zero number")
169}
170/// Default Argon2 iterations
171fn default_argon2_iterations() -> NonZeroU32 {
172    NonZeroU32::new(6).expect("Non-zero number")
173}
174/// Default Argon2 memory
175fn default_argon2_memory() -> NonZeroU32 {
176    NonZeroU32::new(32).expect("Non-zero number")
177}
178/// Default Argon2 parallelism
179fn default_argon2_parallelism() -> NonZeroU32 {
180    NonZeroU32::new(4).expect("Non-zero number")
181}
182
183#[cfg(test)]
184mod tests {
185    use std::num::{NonZero, NonZeroU32};
186
187    use crate::keys::kdf::{Kdf, KdfDerivedKeyMaterial};
188
189    #[test]
190    fn test_derive_kdf_minimums() {
191        fn nz(n: u32) -> NonZero<u32> {
192            NonZero::new(n).unwrap()
193        }
194
195        let secret = [0u8; 32];
196        let salt = [0u8; 32];
197
198        for kdf in [
199            Kdf::PBKDF2 {
200                iterations: nz(4999),
201            },
202            Kdf::Argon2id {
203                iterations: nz(1),
204                memory: nz(16),
205                parallelism: nz(1),
206            },
207            Kdf::Argon2id {
208                iterations: nz(2),
209                memory: nz(15),
210                parallelism: nz(1),
211            },
212            Kdf::Argon2id {
213                iterations: nz(1),
214                memory: nz(15),
215                parallelism: nz(1),
216            },
217        ] {
218            assert_eq!(
219                KdfDerivedKeyMaterial::derive_kdf_key(&secret, &salt, &kdf)
220                    .err()
221                    .unwrap()
222                    .to_string(),
223                "Insufficient KDF parameters"
224            );
225        }
226    }
227
228    #[test]
229    fn test_master_key_derive_pbkdf2() {
230        let kdf_key = KdfDerivedKeyMaterial::derive(
231            "67t9b5g67$%Dh89n",
232            "test_key",
233            &Kdf::PBKDF2 {
234                iterations: NonZeroU32::new(10000).unwrap(),
235            },
236        )
237        .unwrap();
238
239        assert_eq!(
240            [
241                31, 79, 104, 226, 150, 71, 177, 90, 194, 80, 172, 209, 17, 129, 132, 81, 138, 167,
242                69, 167, 254, 149, 2, 27, 39, 197, 64, 42, 22, 195, 86, 75
243            ],
244            kdf_key.0.as_slice()
245        );
246    }
247
248    #[test]
249    fn test_master_key_derive_argon2() {
250        let kdf_key = KdfDerivedKeyMaterial::derive(
251            "67t9b5g67$%Dh89n",
252            "test_key",
253            &Kdf::Argon2id {
254                iterations: NonZeroU32::new(4).unwrap(),
255                memory: NonZeroU32::new(32).unwrap(),
256                parallelism: NonZeroU32::new(2).unwrap(),
257            },
258        )
259        .unwrap();
260
261        assert_eq!(
262            [
263                207, 240, 225, 177, 162, 19, 163, 76, 98, 106, 179, 175, 224, 9, 17, 240, 20, 147,
264                237, 47, 246, 150, 141, 184, 62, 225, 131, 242, 51, 53, 225, 242
265            ],
266            kdf_key.0.as_slice()
267        );
268    }
269}