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
20pub struct KdfDerivedKeyMaterial(pub(super) Pin<Box<GenericArray<u8, U32>>>);
29
30impl KdfDerivedKeyMaterial {
31 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; 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 #[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 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)]
104pub 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#[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 fn default() -> Self {
140 Kdf::PBKDF2 {
141 iterations: default_pbkdf2_iterations(),
142 }
143 }
144}
145
146pub fn default_pbkdf2_iterations() -> NonZeroU32 {
148 NonZeroU32::new(600_000).expect("Non-zero number")
149}
150pub fn default_argon2_iterations() -> NonZeroU32 {
152 NonZeroU32::new(3).expect("Non-zero number")
153}
154pub fn default_argon2_memory() -> NonZeroU32 {
156 NonZeroU32::new(64).expect("Non-zero number")
157}
158pub 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}