1use std::{num::NonZeroU32, pin::Pin};
2
3use hybrid_array::Array;
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#[cfg_attr(feature = "dangerous-crypto-debug", derive(Debug))]
29pub struct KdfDerivedKeyMaterial(pub(super) Pin<Box<Array<u8, U32>>>);
30
31impl KdfDerivedKeyMaterial {
32 pub(super) fn derive_kdf_key(
34 secret: &[u8],
35 salt: &[u8],
36 kdf: &Kdf,
37 ) -> Result<Self, CryptoError> {
38 match kdf {
39 Kdf::PBKDF2 { iterations } => {
40 let iterations = iterations.get();
41 if iterations < PBKDF2_MIN_ITERATIONS {
42 return Err(CryptoError::InsufficientKdfParameters);
43 }
44
45 let mut hash = crate::util::pbkdf2(secret, salt, iterations);
46
47 let key_material = Box::pin(hash.into());
48 hash.zeroize();
49 Ok(KdfDerivedKeyMaterial(key_material))
50 }
51 Kdf::Argon2id {
52 iterations,
53 memory,
54 parallelism,
55 } => {
56 let memory = memory.get() * 1024; let iterations = iterations.get();
58 let parallelism = parallelism.get();
59
60 if memory < ARGON2ID_MIN_MEMORY
61 || iterations < ARGON2ID_MIN_ITERATIONS
62 || parallelism < ARGON2ID_MIN_PARALLELISM
63 {
64 return Err(CryptoError::InsufficientKdfParameters);
65 }
66
67 let salt_sha = sha2::Sha256::new().chain_update(salt).finalize();
68
69 let mut hash = Box::pin(Array::<u8, U32>::default());
70
71 use argon2::*;
72 let params = Params::new(memory, iterations, parallelism, Some(32))?;
73 let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
74 argon.hash_password_into(secret, &salt_sha, hash.as_mut_slice())?;
75
76 #[inline(never)]
80 fn clear_stack() {
81 std::hint::black_box([0u8; 4096]);
82 }
83 clear_stack();
84
85 Ok(KdfDerivedKeyMaterial(hash))
86 }
87 }
88 }
89
90 pub(super) fn derive(password: &str, email: &str, kdf: &Kdf) -> Result<Self, CryptoError> {
94 Self::derive_kdf_key(
95 password.as_bytes(),
96 email.trim().to_lowercase().as_bytes(),
97 kdf,
98 )
99 }
100}
101
102#[deprecated(
103 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!"
104)]
105pub fn dangerous_derive_kdf_material(
111 password: &[u8],
112 salt: &[u8],
113 kdf: &Kdf,
114) -> Result<Vec<u8>, CryptoError> {
115 KdfDerivedKeyMaterial::derive_kdf_key(password, salt, kdf).map(|kdf_key| kdf_key.0.to_vec())
116}
117
118#[allow(missing_docs)]
123#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone, PartialEq)]
124#[serde(rename_all = "camelCase", deny_unknown_fields)]
125#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
126#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
127pub enum Kdf {
128 PBKDF2 {
129 iterations: NonZeroU32,
130 },
131 Argon2id {
132 iterations: NonZeroU32,
133 memory: NonZeroU32,
134 parallelism: NonZeroU32,
135 },
136}
137
138#[cfg(feature = "wasm")]
139impl TryFrom<wasm_bindgen::JsValue> for Kdf {
140 type Error = serde_wasm_bindgen::Error;
141
142 fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
143 serde_wasm_bindgen::from_value(value)
144 }
145}
146
147impl Kdf {
148 pub fn default_pbkdf2() -> Kdf {
150 Kdf::PBKDF2 {
151 iterations: default_pbkdf2_iterations(),
152 }
153 }
154
155 pub fn default_argon2() -> Kdf {
157 Kdf::Argon2id {
158 iterations: default_argon2_iterations(),
159 memory: default_argon2_memory(),
160 parallelism: default_argon2_parallelism(),
161 }
162 }
163}
164
165fn default_pbkdf2_iterations() -> NonZeroU32 {
167 NonZeroU32::new(600_000).expect("Non-zero number")
168}
169fn default_argon2_iterations() -> NonZeroU32 {
171 NonZeroU32::new(6).expect("Non-zero number")
172}
173fn default_argon2_memory() -> NonZeroU32 {
175 NonZeroU32::new(32).expect("Non-zero number")
176}
177fn default_argon2_parallelism() -> NonZeroU32 {
179 NonZeroU32::new(4).expect("Non-zero number")
180}
181
182#[cfg(test)]
183mod tests {
184 use std::num::{NonZero, NonZeroU32};
185
186 use crate::keys::kdf::{Kdf, KdfDerivedKeyMaterial};
187
188 #[test]
189 fn test_derive_kdf_minimums() {
190 fn nz(n: u32) -> NonZero<u32> {
191 NonZero::new(n).unwrap()
192 }
193
194 let secret = [0u8; 32];
195 let salt = [0u8; 32];
196
197 for kdf in [
198 Kdf::PBKDF2 {
199 iterations: nz(4999),
200 },
201 Kdf::Argon2id {
202 iterations: nz(1),
203 memory: nz(16),
204 parallelism: nz(1),
205 },
206 Kdf::Argon2id {
207 iterations: nz(2),
208 memory: nz(15),
209 parallelism: nz(1),
210 },
211 Kdf::Argon2id {
212 iterations: nz(1),
213 memory: nz(15),
214 parallelism: nz(1),
215 },
216 ] {
217 assert_eq!(
218 KdfDerivedKeyMaterial::derive_kdf_key(&secret, &salt, &kdf)
219 .err()
220 .unwrap()
221 .to_string(),
222 "Insufficient KDF parameters"
223 );
224 }
225 }
226
227 #[test]
228 fn test_master_key_derive_pbkdf2() {
229 let kdf_key = KdfDerivedKeyMaterial::derive(
230 "67t9b5g67$%Dh89n",
231 "test_key",
232 &Kdf::PBKDF2 {
233 iterations: NonZeroU32::new(10000).unwrap(),
234 },
235 )
236 .unwrap();
237
238 assert_eq!(
239 [
240 31, 79, 104, 226, 150, 71, 177, 90, 194, 80, 172, 209, 17, 129, 132, 81, 138, 167,
241 69, 167, 254, 149, 2, 27, 39, 197, 64, 42, 22, 195, 86, 75
242 ],
243 kdf_key.0.as_slice()
244 );
245 }
246
247 #[test]
248 fn test_master_key_derive_argon2() {
249 let kdf_key = KdfDerivedKeyMaterial::derive(
250 "67t9b5g67$%Dh89n",
251 "test_key",
252 &Kdf::Argon2id {
253 iterations: NonZeroU32::new(4).unwrap(),
254 memory: NonZeroU32::new(32).unwrap(),
255 parallelism: NonZeroU32::new(2).unwrap(),
256 },
257 )
258 .unwrap();
259
260 assert_eq!(
261 [
262 207, 240, 225, 177, 162, 19, 163, 76, 98, 106, 179, 175, 224, 9, 17, 240, 20, 147,
263 237, 47, 246, 150, 141, 184, 62, 225, 131, 242, 51, 53, 225, 242
264 ],
265 kdf_key.0.as_slice()
266 );
267 }
268}