llm_config_security/
crypto.rs

1//! Cryptographic validation and security
2
3use crate::errors::{SecurityError, SecurityResult};
4use constant_time_eq::constant_time_eq;
5use secrecy::{ExposeSecret, Secret};
6use zeroize::Zeroizing;
7
8/// Minimum key size in bytes
9const MIN_KEY_SIZE: usize = 32; // 256 bits
10
11/// Maximum key age in days
12const MAX_KEY_AGE_DAYS: i64 = 90;
13
14/// Crypto validator
15pub struct CryptoValidator {
16    strict_mode: bool,
17}
18
19impl CryptoValidator {
20    /// Create a new crypto validator
21    pub fn new(strict_mode: bool) -> Self {
22        Self { strict_mode }
23    }
24
25    /// Create in strict mode
26    pub fn strict() -> Self {
27        Self::new(true)
28    }
29
30    /// Validate encryption key
31    pub fn validate_key(&self, key: &[u8]) -> SecurityResult<()> {
32        // Check key size
33        if key.len() < MIN_KEY_SIZE {
34            return Err(SecurityError::CryptoError(format!(
35                "Key size too small: {} bytes (minimum: {})",
36                key.len(),
37                MIN_KEY_SIZE
38            )));
39        }
40
41        // Check for weak keys (all zeros, all ones, etc.)
42        if self.is_weak_key(key) {
43            return Err(SecurityError::CryptoError(
44                "Weak key detected".to_string(),
45            ));
46        }
47
48        // Check entropy in strict mode
49        if self.strict_mode && !self.has_sufficient_entropy(key) {
50            return Err(SecurityError::CryptoError(
51                "Key has insufficient entropy".to_string(),
52            ));
53        }
54
55        Ok(())
56    }
57
58    /// Check if a key is weak
59    fn is_weak_key(&self, key: &[u8]) -> bool {
60        if key.is_empty() {
61            return true;
62        }
63
64        // All zeros
65        if key.iter().all(|&b| b == 0) {
66            return true;
67        }
68
69        // All ones
70        if key.iter().all(|&b| b == 0xFF) {
71            return true;
72        }
73
74        // All same byte
75        let first = key[0];
76        if key.iter().all(|&b| b == first) {
77            return true;
78        }
79
80        false
81    }
82
83    /// Check if key has sufficient entropy (basic check)
84    fn has_sufficient_entropy(&self, key: &[u8]) -> bool {
85        if key.len() < 16 {
86            return false;
87        }
88
89        // Count unique bytes
90        let mut seen = [false; 256];
91        let mut unique_count = 0;
92
93        for &byte in key {
94            if !seen[byte as usize] {
95                seen[byte as usize] = true;
96                unique_count += 1;
97            }
98        }
99
100        // Should have at least 50% unique bytes
101        let min_unique = key.len() / 2;
102        unique_count >= min_unique
103    }
104
105    /// Validate password strength
106    pub fn validate_password(&self, password: &str, min_length: usize) -> SecurityResult<()> {
107        // Check length
108        if password.len() < min_length {
109            return Err(SecurityError::WeakPassword(format!(
110                "Password too short (minimum {} characters)",
111                min_length
112            )));
113        }
114
115        if password.len() > 128 {
116            return Err(SecurityError::WeakPassword(
117                "Password too long (maximum 128 characters)".to_string(),
118            ));
119        }
120
121        // Check for common passwords
122        if self.is_common_password(password) {
123            return Err(SecurityError::WeakPassword(
124                "Common password detected".to_string(),
125            ));
126        }
127
128        // Check complexity in strict mode
129        if self.strict_mode {
130            self.check_password_complexity(password)?;
131        }
132
133        Ok(())
134    }
135
136    /// Check password complexity
137    fn check_password_complexity(&self, password: &str) -> SecurityResult<()> {
138        let has_lowercase = password.chars().any(|c| c.is_lowercase());
139        let has_uppercase = password.chars().any(|c| c.is_uppercase());
140        let has_digit = password.chars().any(|c| c.is_ascii_digit());
141        let has_special = password.chars().any(|c| !c.is_alphanumeric());
142
143        let complexity_score = [has_lowercase, has_uppercase, has_digit, has_special]
144            .iter()
145            .filter(|&&x| x)
146            .count();
147
148        if complexity_score < 3 {
149            return Err(SecurityError::WeakPassword(
150                "Password must contain at least 3 of: lowercase, uppercase, digits, special characters".to_string(),
151            ));
152        }
153
154        Ok(())
155    }
156
157    /// Check if password is in common password list
158    fn is_common_password(&self, password: &str) -> bool {
159        // Top 100 most common passwords
160        const COMMON_PASSWORDS: &[&str] = &[
161            "password", "123456", "123456789", "12345678", "12345", "1234567", "password1",
162            "123123", "1234567890", "000000", "admin", "qwerty", "abc123", "letmein",
163            "welcome", "monkey", "dragon", "master", "sunshine", "princess",
164        ];
165
166        let lower = password.to_lowercase();
167        COMMON_PASSWORDS.contains(&lower.as_str())
168    }
169
170    /// Hash password using Argon2
171    pub fn hash_password(&self, password: &str) -> SecurityResult<String> {
172        use argon2::{
173            password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
174            Argon2,
175        };
176
177        let salt = SaltString::generate(&mut OsRng);
178        let argon2 = Argon2::default();
179
180        argon2
181            .hash_password(password.as_bytes(), &salt)
182            .map(|hash| hash.to_string())
183            .map_err(|e| SecurityError::CryptoError(format!("Password hashing failed: {}", e)))
184    }
185
186    /// Verify password against hash
187    pub fn verify_password(&self, password: &str, hash: &str) -> SecurityResult<bool> {
188        use argon2::{
189            password_hash::{PasswordHash, PasswordVerifier},
190            Argon2,
191        };
192
193        let parsed_hash = PasswordHash::new(hash)
194            .map_err(|e| SecurityError::CryptoError(format!("Invalid password hash: {}", e)))?;
195
196        Ok(Argon2::default()
197            .verify_password(password.as_bytes(), &parsed_hash)
198            .is_ok())
199    }
200
201    /// Constant-time comparison of secrets
202    pub fn constant_time_compare(&self, a: &[u8], b: &[u8]) -> bool {
203        if a.len() != b.len() {
204            return false;
205        }
206        constant_time_eq(a, b)
207    }
208}
209
210/// Key validator for key rotation and management
211pub struct KeyValidator {
212    max_age_days: i64,
213}
214
215impl KeyValidator {
216    /// Create a new key validator
217    pub fn new(max_age_days: i64) -> Self {
218        Self { max_age_days }
219    }
220
221    /// Create with default settings
222    pub fn default() -> Self {
223        Self::new(MAX_KEY_AGE_DAYS)
224    }
225
226    /// Check if a key should be rotated based on age
227    pub fn should_rotate(
228        &self,
229        created_at: chrono::DateTime<chrono::Utc>,
230    ) -> bool {
231        let age = chrono::Utc::now().signed_duration_since(created_at);
232        age.num_days() >= self.max_age_days
233    }
234
235    /// Calculate days until rotation
236    pub fn days_until_rotation(
237        &self,
238        created_at: chrono::DateTime<chrono::Utc>,
239    ) -> i64 {
240        let age = chrono::Utc::now().signed_duration_since(created_at);
241        self.max_age_days - age.num_days()
242    }
243
244    /// Validate key metadata
245    pub fn validate_metadata(
246        &self,
247        created_at: chrono::DateTime<chrono::Utc>,
248        algorithm: &str,
249    ) -> SecurityResult<()> {
250        // Check if key is too old
251        if self.should_rotate(created_at) {
252            return Err(SecurityError::CryptoError(
253                "Key rotation required".to_string(),
254            ));
255        }
256
257        // Validate algorithm
258        match algorithm {
259            "aes-256-gcm" | "chacha20-poly1305" => Ok(()),
260            _ => Err(SecurityError::CryptoError(format!(
261                "Unsupported algorithm: {}",
262                algorithm
263            ))),
264        }
265    }
266}
267
268/// Secure secret wrapper
269pub struct SecureSecret {
270    inner: Secret<Zeroizing<Vec<u8>>>,
271}
272
273impl SecureSecret {
274    /// Create from bytes
275    pub fn new(data: Vec<u8>) -> Self {
276        Self {
277            inner: Secret::new(Zeroizing::new(data)),
278        }
279    }
280
281    /// Expose the secret (use with caution)
282    pub fn expose(&self) -> &[u8] {
283        self.inner.expose_secret()
284    }
285
286    /// Get the length
287    pub fn len(&self) -> usize {
288        self.inner.expose_secret().len()
289    }
290
291    /// Check if empty
292    pub fn is_empty(&self) -> bool {
293        self.inner.expose_secret().is_empty()
294    }
295}
296
297impl Drop for SecureSecret {
298    fn drop(&mut self) {
299        // Zeroizing will handle zeroing the memory
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn test_key_validation() {
309        let validator = CryptoValidator::strict();
310
311        // Valid key
312        let valid_key = vec![1u8; 32];
313        assert!(validator.validate_key(&valid_key).is_ok());
314
315        // Too short
316        let short_key = vec![1u8; 16];
317        assert!(validator.validate_key(&short_key).is_err());
318
319        // All zeros (weak)
320        let weak_key = vec![0u8; 32];
321        assert!(validator.validate_key(&weak_key).is_err());
322
323        // All ones (weak)
324        let weak_key = vec![0xFFu8; 32];
325        assert!(validator.validate_key(&weak_key).is_err());
326    }
327
328    #[test]
329    fn test_password_validation() {
330        let validator = CryptoValidator::strict();
331
332        // Valid password
333        assert!(validator.validate_password("MyP@ssw0rd123!", 12).is_ok());
334
335        // Too short
336        assert!(validator.validate_password("short", 12).is_err());
337
338        // Common password
339        assert!(validator.validate_password("password123", 8).is_err());
340
341        // Weak complexity
342        assert!(validator.validate_password("allowercase", 12).is_err());
343    }
344
345    #[test]
346    fn test_password_hashing() {
347        let validator = CryptoValidator::new(false);
348
349        let password = "MySecurePassword123!";
350        let hash = validator.hash_password(password).unwrap();
351
352        // Verify correct password
353        assert!(validator.verify_password(password, &hash).unwrap());
354
355        // Verify incorrect password
356        assert!(!validator.verify_password("WrongPassword", &hash).unwrap());
357    }
358
359    #[test]
360    fn test_constant_time_compare() {
361        let validator = CryptoValidator::new(false);
362
363        let a = b"secret";
364        let b = b"secret";
365        let c = b"Secret";
366
367        assert!(validator.constant_time_compare(a, b));
368        assert!(!validator.constant_time_compare(a, c));
369    }
370
371    #[test]
372    fn test_key_rotation() {
373        let validator = KeyValidator::default();
374
375        // Recent key
376        let recent = chrono::Utc::now() - chrono::Duration::days(30);
377        assert!(!validator.should_rotate(recent));
378        assert!(validator.days_until_rotation(recent) > 0);
379
380        // Old key
381        let old = chrono::Utc::now() - chrono::Duration::days(100);
382        assert!(validator.should_rotate(old));
383        assert!(validator.days_until_rotation(old) < 0);
384    }
385
386    #[test]
387    fn test_algorithm_validation() {
388        let validator = KeyValidator::default();
389        let now = chrono::Utc::now();
390
391        assert!(validator.validate_metadata(now, "aes-256-gcm").is_ok());
392        assert!(validator
393            .validate_metadata(now, "chacha20-poly1305")
394            .is_ok());
395        assert!(validator.validate_metadata(now, "md5").is_err());
396    }
397
398    #[test]
399    fn test_secure_secret() {
400        let secret = SecureSecret::new(vec![1, 2, 3, 4, 5]);
401        assert_eq!(secret.len(), 5);
402        assert!(!secret.is_empty());
403        assert_eq!(secret.expose(), &[1, 2, 3, 4, 5]);
404    }
405
406    #[test]
407    fn test_password_complexity() {
408        let validator = CryptoValidator::strict();
409
410        // All required character types
411        assert!(validator
412            .check_password_complexity("Abc123!@#")
413            .is_ok());
414
415        // Missing special characters
416        assert!(validator
417            .check_password_complexity("Abc123456")
418            .is_err());
419
420        // Missing uppercase
421        assert!(validator
422            .check_password_complexity("abc123!@#")
423            .is_err());
424    }
425
426    #[test]
427    fn test_entropy_check() {
428        let validator = CryptoValidator::strict();
429
430        // Good entropy
431        let good_key: Vec<u8> = (0..32).map(|i| (i * 7) as u8).collect();
432        assert!(validator.has_sufficient_entropy(&good_key));
433
434        // Poor entropy (repeating pattern)
435        let poor_key = vec![1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2];
436        assert!(!validator.has_sufficient_entropy(&poor_key));
437    }
438}