bitwarden_crypto/keys/
kdf.rs

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