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#[cfg_attr(feature = "dangerous-crypto-debug", derive(Debug))]
27pub struct KdfDerivedKeyMaterial(pub(super) Pin<Box<Array<u8, U32>>>);
28
29impl KdfDerivedKeyMaterial {
30 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; 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 crate::util::clear_stack();
78
79 Ok(KdfDerivedKeyMaterial(hash))
80 }
81 }
82 }
83
84 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)]
99pub 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#[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 pub fn default_pbkdf2() -> Kdf {
144 Kdf::PBKDF2 {
145 iterations: default_pbkdf2_iterations(),
146 }
147 }
148
149 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
159fn default_pbkdf2_iterations() -> NonZeroU32 {
161 NonZeroU32::new(600_000).expect("Non-zero number")
162}
163fn default_argon2_iterations() -> NonZeroU32 {
165 NonZeroU32::new(6).expect("Non-zero number")
166}
167fn default_argon2_memory() -> NonZeroU32 {
169 NonZeroU32::new(32).expect("Non-zero number")
170}
171fn 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}