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_next::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/// Key Derivation Function for Bitwarden Account
102///
103/// In Bitwarden accounts can use multiple KDFs to derive their master key from their password. This
104/// Enum represents all the possible KDFs.
105#[allow(missing_docs)]
106#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)]
107#[serde(rename_all = "camelCase", deny_unknown_fields)]
108#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
109#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
110pub enum Kdf {
111    PBKDF2 {
112        iterations: NonZeroU32,
113    },
114    Argon2id {
115        iterations: NonZeroU32,
116        memory: NonZeroU32,
117        parallelism: NonZeroU32,
118    },
119}
120
121impl Default for Kdf {
122    /// Default KDF for new accounts.
123    fn default() -> Self {
124        Kdf::PBKDF2 {
125            iterations: default_pbkdf2_iterations(),
126        }
127    }
128}
129
130/// Default PBKDF2 iterations
131pub fn default_pbkdf2_iterations() -> NonZeroU32 {
132    NonZeroU32::new(600_000).expect("Non-zero number")
133}
134/// Default Argon2 iterations
135pub fn default_argon2_iterations() -> NonZeroU32 {
136    NonZeroU32::new(3).expect("Non-zero number")
137}
138/// Default Argon2 memory
139pub fn default_argon2_memory() -> NonZeroU32 {
140    NonZeroU32::new(64).expect("Non-zero number")
141}
142/// Default Argon2 parallelism
143pub fn default_argon2_parallelism() -> NonZeroU32 {
144    NonZeroU32::new(4).expect("Non-zero number")
145}
146
147#[cfg(test)]
148mod tests {
149    use std::num::{NonZero, NonZeroU32};
150
151    use crate::keys::kdf::{Kdf, KdfDerivedKeyMaterial};
152
153    #[test]
154    fn test_derive_kdf_minimums() {
155        fn nz(n: u32) -> NonZero<u32> {
156            NonZero::new(n).unwrap()
157        }
158
159        let secret = [0u8; 32];
160        let salt = [0u8; 32];
161
162        for kdf in [
163            Kdf::PBKDF2 {
164                iterations: nz(4999),
165            },
166            Kdf::Argon2id {
167                iterations: nz(1),
168                memory: nz(16),
169                parallelism: nz(1),
170            },
171            Kdf::Argon2id {
172                iterations: nz(2),
173                memory: nz(15),
174                parallelism: nz(1),
175            },
176            Kdf::Argon2id {
177                iterations: nz(1),
178                memory: nz(15),
179                parallelism: nz(1),
180            },
181        ] {
182            assert_eq!(
183                KdfDerivedKeyMaterial::derive_kdf_key(&secret, &salt, &kdf)
184                    .err()
185                    .unwrap()
186                    .to_string(),
187                "Insufficient KDF parameters"
188            );
189        }
190    }
191
192    #[test]
193    fn test_master_key_derive_pbkdf2() {
194        let kdf_key = KdfDerivedKeyMaterial::derive(
195            "67t9b5g67$%Dh89n",
196            "test_key",
197            &Kdf::PBKDF2 {
198                iterations: NonZeroU32::new(10000).unwrap(),
199            },
200        )
201        .unwrap();
202
203        assert_eq!(
204            [
205                31, 79, 104, 226, 150, 71, 177, 90, 194, 80, 172, 209, 17, 129, 132, 81, 138, 167,
206                69, 167, 254, 149, 2, 27, 39, 197, 64, 42, 22, 195, 86, 75
207            ],
208            kdf_key.0.as_slice()
209        );
210    }
211
212    #[test]
213    fn test_master_key_derive_argon2() {
214        let kdf_key = KdfDerivedKeyMaterial::derive(
215            "67t9b5g67$%Dh89n",
216            "test_key",
217            &Kdf::Argon2id {
218                iterations: NonZeroU32::new(4).unwrap(),
219                memory: NonZeroU32::new(32).unwrap(),
220                parallelism: NonZeroU32::new(2).unwrap(),
221            },
222        )
223        .unwrap();
224
225        assert_eq!(
226            [
227                207, 240, 225, 177, 162, 19, 163, 76, 98, 106, 179, 175, 224, 9, 17, 240, 20, 147,
228                237, 47, 246, 150, 141, 184, 62, 225, 131, 242, 51, 53, 225, 242
229            ],
230            kdf_key.0.as_slice()
231        );
232    }
233}