Skip to main content

kindly_guard_server/scanner/
crypto.rs

1//! Cryptographic security scanner for detecting weak crypto patterns
2//!
3//! This module detects:
4//! - Deprecated algorithms (MD5, SHA1, DES, etc.)
5//! - Insecure random number generation
6//! - Weak key sizes
7//! - Insecure encryption modes (ECB)
8//! - IV reuse patterns
9//! - Bad key derivation practices
10
11use crate::scanner::{Location, ScanResult, Severity, Threat, ThreatType};
12use regex::Regex;
13use tracing::{debug, error, trace};
14
15#[derive(Debug, Clone)]
16pub struct CryptoScanner {
17    /// Patterns for deprecated hash algorithms
18    deprecated_hash_patterns: Vec<Regex>,
19
20    /// Patterns for weak encryption algorithms
21    weak_encryption_patterns: Vec<Regex>,
22
23    /// Patterns for insecure random number generation
24    insecure_rng_patterns: Vec<Regex>,
25
26    /// Patterns for weak key sizes
27    weak_key_patterns: Vec<Regex>,
28
29    /// Patterns for insecure encryption modes
30    insecure_mode_patterns: Vec<Regex>,
31
32    /// Patterns for bad key derivation
33    bad_kdf_patterns: Vec<Regex>,
34}
35
36impl Default for CryptoScanner {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl CryptoScanner {
43    /// Helper function to compile regex patterns with error handling
44    fn compile_patterns(patterns: &[&str]) -> Vec<Regex> {
45        patterns
46            .iter()
47            .filter_map(|pattern| match Regex::new(pattern) {
48                Ok(regex) => Some(regex),
49                Err(e) => {
50                    error!(
51                        "Failed to compile crypto scanner regex '{}': {}",
52                        pattern, e
53                    );
54                    None
55                },
56            })
57            .collect()
58    }
59
60    pub fn new() -> Self {
61        // Initialize deprecated hash patterns
62        let deprecated_hash_patterns = Self::compile_patterns(&[
63            // MD5 usage
64            r"(?i)\b(md5|Md5Hash|md5sum|MD5_)\b",
65            r"(?i)use\s+md5(?:::|\s|;)",
66            r"(?i)md5::compute",
67            // SHA1 usage
68            r"(?i)\b(sha1|Sha1|SHA1_|sha1sum)\b",
69            r"(?i)use\s+sha1(?:::|\s|;)",
70            r"(?i)sha1::Sha1",
71            // MD4 usage
72            r"(?i)\b(md4|Md4Hash|MD4_)\b",
73            // Other deprecated hashes
74            r"(?i)\b(md2|ripemd|RIPEMD)\b",
75        ]);
76
77        // Initialize weak encryption patterns
78        let weak_encryption_patterns = Self::compile_patterns(&[
79            // DES usage
80            r"(?i)\b(des|DES|DesKey|des_key)\b",
81            r"(?i)use\s+des(?:::|\s|;)",
82            // 3DES (except when properly keyed)
83            r"(?i)\b(3des|triple_?des|TDES)\b",
84            // RC4 usage
85            r"(?i)\b(rc4|RC4|arcfour)\b",
86            // RC2 usage
87            r"(?i)\b(rc2|RC2)\b",
88            // Other weak ciphers
89            r"(?i)\b(skipjack|blowfish|CAST5?)\b",
90        ]);
91
92        // Initialize insecure RNG patterns
93        let insecure_rng_patterns = Self::compile_patterns(&[
94            // Non-crypto RNG usage in crypto context
95            r"(?i)(rand::random|thread_rng)\s*\(\s*\)",
96            // Fastrand usage (not crypto secure)
97            r"(?i)fastrand::",
98            // Oorandom usage (not crypto secure)
99            r"(?i)oorandom::",
100            // Using system time as seed
101            r"(?i)(SystemTime::now|time::now).*seed",
102            // Predictable seeds
103            r"(?i)seed\s*=\s*(\d+|0x[0-9a-fA-F]+)",
104            // Non-crypto PRNGs
105            r"(?i)\b(SmallRng|StdRng|Xorshift|Pcg32)\b.*(?:key|crypto|secure)",
106        ]);
107
108        // Initialize weak key size patterns
109        let weak_key_patterns = Self::compile_patterns(&[
110            // RSA with small keys
111            r"(?i)rsa.*(?:512|768|1024)",
112            r"(?i)RsaKeySize::Rsa(?:512|768|1024)",
113            r"(?i)RsaPrivateKey::new.*1024",
114            // ECC with small keys
115            r"(?i)(?:ecc|ecdsa|ecdh).*(?:112|128|160|192)\s*(?:bit|_bit)",
116            r"(?i)(?:P-?112|P-?128|P-?160|P-?192|secp112|secp128|secp160|secp192)",
117            // AES with weak keys
118            r"(?i)aes.*(?:64|80|96)\s*(?:bit|_bit)",
119            // DH with small primes
120            r"(?i)(?:dh|diffie).*(?:512|768|1024)\s*(?:bit|_bit)",
121        ]);
122
123        // Initialize insecure mode patterns
124        let insecure_mode_patterns = Self::compile_patterns(&[
125            // ECB mode usage
126            r"(?i)\b(ecb|ECB|EcbMode|ecb_mode)\b",
127            r#"(?i)mode\s*=\s*["']?ecb["']?"#,
128            r#"(?i)cipher.*ecb"#,
129            r#"(?i)encrypt_ecb"#,
130            // Static IV usage
131            r#"(?i)iv\s*=\s*(\[0(?:,\s*0)*\]|vec!\[0(?:;\s*\d+)?\])"#,
132            r#"(?i)const\s+IV\s*:\s*\[u8"#,
133            r#"(?i)static\s+IV\s*:\s*\[u8"#,
134            // IV reuse patterns
135            r#"(?i)self\.iv|cached_iv|reuse.*iv"#,
136            // No IV usage with CBC
137            r#"(?i)cbc.*encrypt.*\(\s*[^,)]+\s*\)"#,
138        ]);
139
140        // Initialize bad KDF patterns
141        let bad_kdf_patterns = Self::compile_patterns(&[
142            // Simple hashing for passwords
143            r#"(?i)(sha256|sha512|md5|sha1)\s*\(\s*password"#,
144            r#"(?i)(Sha256|Sha512|Md5|Sha1)::new\(\)"#,
145            r#"(?i)hasher\.update\(password"#,
146            // Low iteration counts for PBKDF2
147            r#"(?i)pbkdf2.*iterations?\s*[=:]\s*(?:[1-9]\d{0,3}|10000)\b"#,
148            // Missing salt - simplified pattern without lookahead
149            r#"(?i)simple_hash_password\s*\("#,
150            // Hardcoded salts
151            r#"(?i)salt\s*=\s*["'][\w\s]+["']"#,
152            r#"(?i)const\s+SALT\s*:\s*(?:&str|&\[u8\])"#,
153        ]);
154
155        Self {
156            deprecated_hash_patterns,
157            weak_encryption_patterns,
158            insecure_rng_patterns,
159            weak_key_patterns,
160            insecure_mode_patterns,
161            bad_kdf_patterns,
162        }
163    }
164
165    fn check_deprecated_algorithms(&self, text: &str) -> Vec<Threat> {
166        let mut threats = Vec::new();
167        let mut line_start = 0;
168
169        for (_line_num, line) in text.lines().enumerate() {
170            // Skip comments
171            if line.trim_start().starts_with("//") {
172                line_start += line.len() + 1; // +1 for newline
173                continue;
174            }
175
176            // Check deprecated hash algorithms
177            for pattern in &self.deprecated_hash_patterns {
178                if let Some(m) = pattern.find(line) {
179                    let algo = m.as_str();
180                    let (name, recommendation) = if algo.to_lowercase().contains("md5") {
181                        ("MD5", "Use SHA-256, SHA-3, or BLAKE3 instead")
182                    } else if algo.to_lowercase().contains("sha1") {
183                        ("SHA-1", "Use SHA-256, SHA-3, or BLAKE3 instead")
184                    } else if algo.to_lowercase().contains("md4") {
185                        ("MD4", "Use SHA-256, SHA-3, or BLAKE3 instead")
186                    } else {
187                        ("deprecated hash", "Use SHA-256, SHA-3, or BLAKE3 instead")
188                    };
189
190                    threats.push(Threat {
191                        threat_type: ThreatType::Custom("crypto_deprecated_hash".to_string()),
192                        severity: Severity::High,
193                        location: Location::Text {
194                            offset: line_start + m.start(),
195                            length: m.len(),
196                        },
197                        description: format!(
198                            "Deprecated hash algorithm {} detected. {}",
199                            name, recommendation
200                        ),
201                        remediation: Some(recommendation.to_string()),
202                    });
203                }
204            }
205
206            // Check weak encryption algorithms
207            for pattern in &self.weak_encryption_patterns {
208                if let Some(m) = pattern.find(line) {
209                    let algo = m.as_str();
210                    let (name, recommendation) = if algo.to_lowercase().contains("des") {
211                        ("DES/3DES", "Use AES-256-GCM or ChaCha20-Poly1305")
212                    } else if algo.to_lowercase().contains("rc4") {
213                        ("RC4", "Use AES-256-GCM or ChaCha20-Poly1305")
214                    } else {
215                        ("weak cipher", "Use AES-256-GCM or ChaCha20-Poly1305")
216                    };
217
218                    threats.push(Threat {
219                        threat_type: ThreatType::Custom("crypto_weak_cipher".to_string()),
220                        severity: Severity::Critical,
221                        location: Location::Text {
222                            offset: line_start + m.start(),
223                            length: m.len(),
224                        },
225                        description: format!(
226                            "Weak encryption algorithm {} detected. {}",
227                            name, recommendation
228                        ),
229                        remediation: Some(recommendation.to_string()),
230                    });
231                }
232            }
233
234            line_start += line.len() + 1; // +1 for newline
235        }
236
237        threats
238    }
239
240    fn check_insecure_rng(&self, text: &str) -> Vec<Threat> {
241        let mut threats = Vec::new();
242
243        // Check if this looks like crypto code
244        let is_crypto_context = text.contains("key")
245            || text.contains("Key")
246            || text.contains("encrypt")
247            || text.contains("decrypt")
248            || text.contains("hash")
249            || text.contains("sign")
250            || text.contains("nonce")
251            || text.contains("salt")
252            || text.contains("iv")
253            || text.contains("IV");
254
255        if !is_crypto_context {
256            return threats;
257        }
258
259        let mut line_start = 0;
260
261        for (_line_num, line) in text.lines().enumerate() {
262            if line.trim_start().starts_with("//") {
263                line_start += line.len() + 1;
264                continue;
265            }
266
267            for pattern in &self.insecure_rng_patterns {
268                if let Some(m) = pattern.find(line) {
269                    threats.push(Threat {
270                        threat_type: ThreatType::Custom("crypto_insecure_rng".to_string()),
271                        severity: Severity::Critical,
272                        location: Location::Text {
273                            offset: line_start + m.start(),
274                            length: m.len(),
275                        },
276                        description: "Insecure random number generation for cryptographic use. Use rand::rngs::OsRng or ring::rand::SystemRandom for cryptographic randomness".to_string(),
277                        remediation: Some(
278                            "Use rand::rngs::OsRng or ring::rand::SystemRandom for cryptographic randomness".to_string()
279                        ),
280                    });
281                }
282            }
283
284            line_start += line.len() + 1;
285        }
286
287        threats
288    }
289
290    fn check_weak_key_sizes(&self, text: &str) -> Vec<Threat> {
291        let mut threats = Vec::new();
292        let mut line_start = 0;
293
294        for (_line_num, line) in text.lines().enumerate() {
295            if line.trim_start().starts_with("//") {
296                line_start += line.len() + 1;
297                continue;
298            }
299
300            for pattern in &self.weak_key_patterns {
301                if let Some(m) = pattern.find(line) {
302                    threats.push(Threat {
303                        threat_type: ThreatType::Custom("crypto_weak_key_size".to_string()),
304                        severity: Severity::High,
305                        location: Location::Text {
306                            offset: line_start + m.start(),
307                            length: m.len(),
308                        },
309                        description: "Weak key size detected - insufficient for 2025 standards. Use RSA-3072+, ECC P-256+, or AES-256 for 2025 compliance".to_string(),
310                        remediation: Some(
311                            "Use RSA-3072+, ECC P-256+, or AES-256 for 2025 compliance".to_string()
312                        ),
313                    });
314                }
315            }
316
317            line_start += line.len() + 1;
318        }
319
320        threats
321    }
322
323    fn check_insecure_modes(&self, text: &str) -> Vec<Threat> {
324        let mut threats = Vec::new();
325        let mut line_start = 0;
326
327        for (_line_num, line) in text.lines().enumerate() {
328            if line.trim_start().starts_with("//") {
329                line_start += line.len() + 1;
330                continue;
331            }
332
333            for pattern in &self.insecure_mode_patterns {
334                if let Some(m) = pattern.find(line) {
335                    let detail = m.as_str();
336                    let (message, recommendation) = if detail.to_lowercase().contains("ecb") {
337                        (
338                            "ECB mode encryption is insecure - reveals patterns",
339                            "Use authenticated encryption: AES-GCM, ChaCha20-Poly1305, or AES-GCM-SIV"
340                        )
341                    } else if detail.to_lowercase().contains("iv") {
342                        (
343                            "Static or reused IV detected - breaks semantic security",
344                            "Generate a unique random IV for each encryption operation",
345                        )
346                    } else {
347                        (
348                            "Insecure encryption mode detected",
349                            "Use authenticated encryption modes",
350                        )
351                    };
352
353                    threats.push(Threat {
354                        threat_type: ThreatType::Custom("crypto_insecure_mode".to_string()),
355                        severity: Severity::Critical,
356                        location: Location::Text {
357                            offset: line_start + m.start(),
358                            length: m.len(),
359                        },
360                        description: format!("{}. {}", message, recommendation),
361                        remediation: Some(recommendation.to_string()),
362                    });
363                }
364            }
365
366            line_start += line.len() + 1;
367        }
368
369        threats
370    }
371
372    fn check_bad_kdf(&self, text: &str) -> Vec<Threat> {
373        let mut threats = Vec::new();
374
375        // Don't flag if using proper KDF libraries
376        let has_good_kdf = text.contains("argon2")
377            || text.contains("Argon2")
378            || text.contains("scrypt")
379            || text.contains("bcrypt")
380            || text.contains("pbkdf2") && text.contains("100000");
381
382        let mut line_start = 0;
383
384        for (_line_num, line) in text.lines().enumerate() {
385            if line.trim_start().starts_with("//") {
386                line_start += line.len() + 1;
387                continue;
388            }
389
390            for pattern in &self.bad_kdf_patterns {
391                if let Some(m) = pattern.find(line) {
392                    // Skip if it's using a good KDF in the same line
393                    if has_good_kdf
394                        && (line.contains("argon")
395                            || line.contains("scrypt")
396                            || line.contains("bcrypt"))
397                    {
398                        continue;
399                    }
400
401                    threats.push(Threat {
402                        threat_type: ThreatType::Custom("crypto_bad_kdf".to_string()),
403                        severity: Severity::High,
404                        location: Location::Text {
405                            offset: line_start + m.start(),
406                            length: m.len(),
407                        },
408                        description: "Insecure key derivation detected. Use Argon2id, scrypt, or bcrypt with proper parameters for password hashing".to_string(),
409                        remediation: Some(
410                            "Use Argon2id, scrypt, or bcrypt with proper parameters for password hashing".to_string()
411                        ),
412                    });
413                }
414            }
415
416            line_start += line.len() + 1;
417        }
418
419        threats
420    }
421
422    fn check_post_quantum_readiness(&self, text: &str) -> Vec<Threat> {
423        let mut threats = Vec::new();
424
425        // Check for RSA/ECC usage without migration plan
426        let has_rsa = text.contains("RSA") || text.contains("rsa");
427        let has_ecc = text.contains("ECC") || text.contains("ECDSA") || text.contains("ECDH");
428        let has_pqc = text.contains("ML-KEM")
429            || text.contains("ML-DSA")
430            || text.contains("SPHINCS")
431            || text.contains("Dilithium")
432            || text.contains("Kyber")
433            || text.contains("post-quantum")
434            || text.contains("pqcrypto");
435
436        if (has_rsa || has_ecc) && !has_pqc {
437            threats.push(Threat {
438                threat_type: ThreatType::Custom("crypto_no_pqc_plan".to_string()),
439                severity: Severity::Medium,
440                location: Location::Text {
441                    offset: 0,
442                    length: text.len(),
443                },
444                description: "No post-quantum cryptography migration detected. Plan migration to NIST PQC standards (ML-KEM, ML-DSA, SLH-DSA) by 2035".to_string(),
445                remediation: Some(
446                    "Plan migration to NIST PQC standards (ML-KEM, ML-DSA, SLH-DSA) by 2035".to_string()
447                ),
448            });
449        }
450
451        threats
452    }
453
454    pub fn scan_text(&self, text: &str) -> ScanResult {
455        trace!("Starting cryptographic security scan");
456
457        let mut all_threats = Vec::new();
458
459        // Run all checks
460        all_threats.extend(self.check_deprecated_algorithms(text));
461        all_threats.extend(self.check_insecure_rng(text));
462        all_threats.extend(self.check_weak_key_sizes(text));
463        all_threats.extend(self.check_insecure_modes(text));
464        all_threats.extend(self.check_bad_kdf(text));
465        all_threats.extend(self.check_post_quantum_readiness(text));
466
467        debug!("Crypto scan found {} threats", all_threats.len());
468
469        Ok(all_threats)
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn test_detect_md5() {
479        let scanner = CryptoScanner::new();
480        let code = r#"
481use md5;
482
483fn hash_password(password: &str) -> String {
484    let digest = md5::compute(password);
485    format!("{:x}", digest)
486}
487"#;
488
489        let threats = scanner.scan_text(code).unwrap();
490        assert!(!threats.is_empty());
491        assert!(
492            matches!(threats[0].threat_type, ThreatType::Custom(ref s) if s == "crypto_deprecated_hash")
493        );
494        assert!(threats[0].description.contains("MD5"));
495    }
496
497    #[test]
498    fn test_detect_weak_rsa_key() {
499        let scanner = CryptoScanner::new();
500        let code = r#"
501const RSA_KEY_SIZE: usize = 1024;
502
503fn generate_rsa_key() {
504    let key = RsaPrivateKey::new(&mut rng, 1024).unwrap();
505}
506"#;
507
508        let threats = scanner.scan_text(code).unwrap();
509        assert!(!threats.is_empty());
510        assert!(
511            matches!(threats[0].threat_type, ThreatType::Custom(ref s) if s == "crypto_weak_key_size")
512        );
513    }
514
515    #[test]
516    fn test_detect_ecb_mode() {
517        let scanner = CryptoScanner::new();
518        let code = r#"
519use aes::cipher::{BlockEncrypt, KeyInit};
520use aes::Aes256;
521
522fn encrypt_ecb(key: &[u8], data: &[u8]) -> Vec<u8> {
523    let cipher = Aes256::new_from_slice(key).unwrap();
524    // ECB mode encryption
525    let mut output = data.to_vec();
526    cipher.encrypt_block((&mut output).into());
527    output
528}
529"#;
530
531        let threats = scanner.scan_text(code).unwrap();
532        assert!(!threats.is_empty());
533        assert!(threats.iter().any(
534            |t| matches!(t.threat_type, ThreatType::Custom(ref s) if s == "crypto_insecure_mode")
535        ));
536    }
537
538    #[test]
539    fn test_detect_insecure_rng() {
540        let scanner = CryptoScanner::new();
541        let code = r#"
542use rand::prelude::*;
543
544fn generate_key() -> [u8; 32] {
545    let mut key = [0u8; 32];
546    let mut rng = thread_rng();
547    rng.fill_bytes(&mut key);
548    key
549}
550"#;
551
552        let threats = scanner.scan_text(code).unwrap();
553        assert!(!threats.is_empty());
554        assert!(
555            matches!(threats[0].threat_type, ThreatType::Custom(ref s) if s == "crypto_insecure_rng")
556        );
557    }
558
559    #[test]
560    fn test_detect_static_iv() {
561        let scanner = CryptoScanner::new();
562        let code = r#"
563const IV: [u8; 16] = [0; 16];
564
565fn encrypt_data(key: &[u8], plaintext: &[u8]) -> Vec<u8> {
566    let cipher = Aes256Cbc::new_from_slices(key, &IV).unwrap();
567    cipher.encrypt_vec(plaintext)
568}
569"#;
570
571        let threats = scanner.scan_text(code).unwrap();
572        assert!(!threats.is_empty());
573        assert!(
574            matches!(threats[0].threat_type, ThreatType::Custom(ref s) if s == "crypto_insecure_mode")
575        );
576        assert!(threats[0].description.contains("IV"));
577    }
578
579    #[test]
580    fn test_detect_bad_password_hashing() {
581        let scanner = CryptoScanner::new();
582        let code = r#"
583use sha2::{Sha256, Digest};
584
585fn hash_password(password: &str) -> String {
586    let mut hasher = Sha256::new();
587    hasher.update(password);
588    format!("{:x}", hasher.finalize())
589}
590"#;
591
592        let threats = scanner.scan_text(code).unwrap();
593        assert!(!threats.is_empty());
594        assert!(
595            matches!(threats[0].threat_type, ThreatType::Custom(ref s) if s == "crypto_bad_kdf")
596        );
597    }
598
599    #[test]
600    fn test_no_false_positives_for_secure_code() {
601        let scanner = CryptoScanner::new();
602        let code = r#"
603use ring::rand::{SecureRandom, SystemRandom};
604use argon2::{Argon2, PasswordHasher, PasswordHash, PasswordVerifier};
605
606fn generate_secure_key() -> Result<[u8; 32], ring::error::Unspecified> {
607    let rng = SystemRandom::new();
608    let mut key = [0u8; 32];
609    rng.fill(&mut key)?;
610    Ok(key)
611}
612
613fn hash_password_secure(password: &str) -> Result<String, argon2::password_hash::Error> {
614    let salt = SaltString::generate(&mut OsRng);
615    let argon2 = Argon2::default();
616    let password_hash = argon2.hash_password(password.as_bytes(), &salt)?;
617    Ok(password_hash.to_string())
618}
619
620// Using AES-256-GCM for authenticated encryption
621use aes_gcm::{Aes256Gcm, Key, Nonce};
622use aes_gcm::aead::{Aead, NewAead};
623"#;
624
625        let threats = scanner.scan_text(code).unwrap();
626        // Should only potentially flag lack of PQC, not the secure implementations
627        assert!(threats.iter().all(|t| {
628            if let ThreatType::Custom(ref s) = t.threat_type {
629                s != "crypto_insecure_rng" && s != "crypto_bad_kdf"
630            } else {
631                true
632            }
633        }));
634    }
635}