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#[cfg_attr(feature = "dangerous-crypto-debug", derive(Debug))]
30pub struct KdfDerivedKeyMaterial(pub(super) Pin<Box<Array<u8, U32>>>);
31
32impl KdfDerivedKeyMaterial {
33 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; 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 crate::util::clear_stack();
85
86 Ok(KdfDerivedKeyMaterial(hash))
87 }
88 }
89 }
90
91 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)]
106pub 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#[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 pub fn default_pbkdf2() -> Kdf {
151 Kdf::PBKDF2 {
152 iterations: default_pbkdf2_iterations(),
153 }
154 }
155
156 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
166fn default_pbkdf2_iterations() -> NonZeroU32 {
168 NonZeroU32::new(600_000).expect("Non-zero number")
169}
170fn default_argon2_iterations() -> NonZeroU32 {
172 NonZeroU32::new(6).expect("Non-zero number")
173}
174fn default_argon2_memory() -> NonZeroU32 {
176 NonZeroU32::new(32).expect("Non-zero number")
177}
178fn 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}