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