rust_crypto_utils/
keyderivation.rs

1//! Key derivation functions (PBKDF2, HKDF)
2
3use hmac::Hmac;
4use sha2::{Sha256, Sha512};
5use zeroize::{Zeroize, ZeroizeOnDrop};
6
7type HmacSha256 = Hmac<Sha256>;
8type HmacSha512 = Hmac<Sha512>;
9
10/// Derived key with automatic zeroization
11#[derive(Zeroize, ZeroizeOnDrop)]
12pub struct DerivedKey {
13    key: Vec<u8>,
14}
15
16impl DerivedKey {
17    /// Create from bytes
18    pub fn from_bytes(bytes: Vec<u8>) -> Self {
19        Self { key: bytes }
20    }
21
22    /// Get key bytes
23    pub fn as_bytes(&self) -> &[u8] {
24        &self.key
25    }
26
27    /// Get key length
28    pub fn len(&self) -> usize {
29        self.key.len()
30    }
31
32    /// Check if empty
33    pub fn is_empty(&self) -> bool {
34        self.key.is_empty()
35    }
36}
37
38/// PBKDF2 key derivation (NIST SP 800-132)
39pub struct Pbkdf2;
40
41impl Pbkdf2 {
42    /// Derive key using PBKDF2-HMAC-SHA256
43    ///
44    /// # Arguments
45    /// * `password` - Password bytes
46    /// * `salt` - Salt (should be at least 16 bytes)
47    /// * `iterations` - Number of iterations (NIST recommends at least 10,000)
48    /// * `key_length` - Desired key length in bytes
49    pub fn derive_key_sha256(
50        password: &[u8],
51        salt: &[u8],
52        iterations: u32,
53        key_length: usize,
54    ) -> DerivedKey {
55        let mut derived = vec![0u8; key_length];
56        pbkdf2::pbkdf2::<HmacSha256>(password, salt, iterations, &mut derived)
57            .expect("PBKDF2 derivation failed");
58        DerivedKey::from_bytes(derived)
59    }
60
61    /// Derive key using PBKDF2-HMAC-SHA512
62    pub fn derive_key_sha512(
63        password: &[u8],
64        salt: &[u8],
65        iterations: u32,
66        key_length: usize,
67    ) -> DerivedKey {
68        let mut derived = vec![0u8; key_length];
69        pbkdf2::pbkdf2::<HmacSha512>(password, salt, iterations, &mut derived)
70            .expect("PBKDF2 derivation failed");
71        DerivedKey::from_bytes(derived)
72    }
73}
74
75/// HKDF key derivation (RFC 5869)
76pub struct Hkdf;
77
78impl Hkdf {
79    /// Extract-and-Expand key derivation using HMAC-SHA256
80    ///
81    /// # Arguments
82    /// * `input_key_material` - Input keying material
83    /// * `salt` - Optional salt (use empty slice if none)
84    /// * `info` - Optional context/application info
85    /// * `output_length` - Desired output length in bytes
86    pub fn derive_key(
87        input_key_material: &[u8],
88        salt: &[u8],
89        info: &[u8],
90        output_length: usize,
91    ) -> DerivedKey {
92        use hkdf::Hkdf as HkdfImpl;
93
94        let hk = HkdfImpl::<Sha256>::new(Some(salt), input_key_material);
95        let mut okm = vec![0u8; output_length];
96        hk.expand(info, &mut okm).expect("HKDF expand failed");
97
98        DerivedKey::from_bytes(okm)
99    }
100
101    /// Derive multiple keys from a single input
102    pub fn derive_multiple_keys(
103        input_key_material: &[u8],
104        salt: &[u8],
105        contexts: &[&[u8]],
106        key_length: usize,
107    ) -> Vec<DerivedKey> {
108        contexts
109            .iter()
110            .map(|context| Self::derive_key(input_key_material, salt, context, key_length))
111            .collect()
112    }
113}
114
115/// Secure password strength validator
116pub struct PasswordStrength;
117
118impl PasswordStrength {
119    /// Check password strength
120    /// Returns (score, feedback) where score is 0-4
121    pub fn check(password: &str) -> (u8, Vec<String>) {
122        let mut score = 0u8;
123        let mut feedback = Vec::new();
124
125        // Length check
126        if password.len() >= 12 {
127            score += 1;
128        } else {
129            feedback.push("Password should be at least 12 characters".to_string());
130        }
131
132        // Uppercase check
133        if password.chars().any(|c| c.is_uppercase()) {
134            score += 1;
135        } else {
136            feedback.push("Add uppercase letters".to_string());
137        }
138
139        // Lowercase check
140        if password.chars().any(|c| c.is_lowercase()) {
141            score += 1;
142        } else {
143            feedback.push("Add lowercase letters".to_string());
144        }
145
146        // Digit check
147        if password.chars().any(|c| c.is_numeric()) {
148            score += 1;
149        } else {
150            feedback.push("Add numbers".to_string());
151        }
152
153        // Special character check
154        if password.chars().any(|c| !c.is_alphanumeric()) {
155            score += 1;
156        } else {
157            feedback.push("Add special characters".to_string());
158        }
159
160        // Common password check (basic)
161        let common_passwords = [
162            "password", "123456", "qwerty", "admin", "letmein", "welcome", "monkey", "dragon",
163            "master", "sunshine", "princess", "football",
164        ];
165        if common_passwords
166            .iter()
167            .any(|&common| password.to_lowercase().contains(common))
168        {
169            score = score.saturating_sub(2);
170            feedback.push("Avoid common passwords".to_string());
171        }
172
173        // Sequential characters check
174        if Self::has_sequential_chars(password) {
175            score = score.saturating_sub(1);
176            feedback.push("Avoid sequential characters (abc, 123, etc.)".to_string());
177        }
178
179        (score.min(4), feedback)
180    }
181
182    fn has_sequential_chars(password: &str) -> bool {
183        let chars: Vec<char> = password.chars().collect();
184        for window in chars.windows(3) {
185            if window.len() == 3 {
186                let a = window[0] as i32;
187                let b = window[1] as i32;
188                let c = window[2] as i32;
189                if (b == a + 1 && c == b + 1) || (b == a - 1 && c == b - 1) {
190                    return true;
191                }
192            }
193        }
194        false
195    }
196
197    /// Get strength description
198    pub fn strength_description(score: u8) -> &'static str {
199        match score {
200            0 => "Very Weak",
201            1 => "Weak",
202            2 => "Fair",
203            3 => "Strong",
204            4 | 5 => "Very Strong",
205            _ => "Unknown",
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_pbkdf2_derivation() {
216        let password = b"my_secure_password";
217        let salt = b"random_salt_12345";
218        let iterations = 10000;
219        let key_length = 32;
220
221        let key = Pbkdf2::derive_key_sha256(password, salt, iterations, key_length);
222        assert_eq!(key.len(), key_length);
223    }
224
225    #[test]
226    fn test_pbkdf2_deterministic() {
227        let password = b"test_password";
228        let salt = b"test_salt";
229        let iterations = 1000;
230
231        let key1 = Pbkdf2::derive_key_sha256(password, salt, iterations, 32);
232        let key2 = Pbkdf2::derive_key_sha256(password, salt, iterations, 32);
233
234        assert_eq!(key1.as_bytes(), key2.as_bytes());
235    }
236
237    #[test]
238    fn test_hkdf_derivation() {
239        let ikm = b"input_key_material";
240        let salt = b"salt";
241        let info = b"application_context";
242        let key_length = 32;
243
244        let key = Hkdf::derive_key(ikm, salt, info, key_length);
245        assert_eq!(key.len(), key_length);
246    }
247
248    #[test]
249    fn test_hkdf_multiple_keys() {
250        let ikm = b"shared_secret";
251        let salt = b"salt";
252        let contexts = vec![b"encryption".as_slice(), b"authentication".as_slice()];
253
254        let keys = Hkdf::derive_multiple_keys(ikm, salt, &contexts, 32);
255        assert_eq!(keys.len(), 2);
256        assert_ne!(keys[0].as_bytes(), keys[1].as_bytes());
257    }
258
259    #[test]
260    fn test_password_strength_weak() {
261        let (score, _feedback) = PasswordStrength::check("pass");
262        assert!(score <= 2);
263    }
264
265    #[test]
266    fn test_password_strength_strong() {
267        let (score, feedback) = PasswordStrength::check("MyStr0ng!P@ssw0rd2024");
268        assert_eq!(score, 4); // Maximum score is 4 per implementation
269        assert!(feedback.is_empty());
270    }
271
272    #[test]
273    fn test_password_strength_common() {
274        let (score, feedback) = PasswordStrength::check("password123");
275        assert!(score < 3);
276        assert!(feedback.iter().any(|f| f.contains("common")));
277    }
278
279    #[test]
280    fn test_password_strength_sequential() {
281        let (_score, feedback) = PasswordStrength::check("abc123xyz");
282        assert!(feedback.iter().any(|f| f.contains("sequential")));
283    }
284
285    #[test]
286    fn test_strength_descriptions() {
287        assert_eq!(PasswordStrength::strength_description(0), "Very Weak");
288        assert_eq!(PasswordStrength::strength_description(2), "Fair");
289        assert_eq!(PasswordStrength::strength_description(4), "Very Strong");
290    }
291
292    #[test]
293    fn test_derived_key_zeroization() {
294        let password = b"test";
295        let salt = b"salt";
296        {
297            let _key = Pbkdf2::derive_key_sha256(password, salt, 1000, 32);
298            // Key should be zeroized when dropped
299        }
300        // If we could inspect memory, it would be zeroed
301    }
302}