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