Skip to main content

claw_spawn/infrastructure/
crypto.rs

1use aes_gcm::{
2    aead::{Aead, KeyInit},
3    Aes256Gcm, Nonce,
4};
5use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
6use rand::RngCore;
7use thiserror::Error;
8use tracing::warn;
9
10#[derive(Error, Debug)]
11pub enum EncryptionError {
12    #[error("Encryption failed: {0}")]
13    EncryptionFailed(String),
14    #[error("Decryption failed: {0}")]
15    DecryptionFailed(String),
16    #[error("Invalid key length")]
17    InvalidKeyLength,
18}
19
20pub struct SecretsEncryption {
21    cipher: Aes256Gcm,
22}
23
24impl SecretsEncryption {
25    pub fn new(key_base64: &str) -> Result<Self, EncryptionError> {
26        let key_bytes = BASE64
27            .decode(key_base64)
28            .map_err(|_| EncryptionError::InvalidKeyLength)?;
29
30        if key_bytes.len() != 32 {
31            return Err(EncryptionError::InvalidKeyLength);
32        }
33
34        let key: [u8; 32] = key_bytes
35            .try_into()
36            .map_err(|_| EncryptionError::InvalidKeyLength)?;
37
38        // MED-005: Check key entropy/strength
39        Self::validate_key_entropy(&key);
40
41        let cipher = Aes256Gcm::new_from_slice(&key)
42            .map_err(|e| EncryptionError::EncryptionFailed(e.to_string()))?;
43
44        Ok(Self { cipher })
45    }
46
47    /// Validate key entropy and warn on weak keys (MED-005)
48    fn validate_key_entropy(key: &[u8; 32]) {
49        // Check for all zeros
50        if key.iter().all(|&b| b == 0) {
51            warn!("CRITICAL: Encryption key is all zeros - this is extremely insecure!");
52            return;
53        }
54
55        // Check for all same byte
56        let first = key[0];
57        if key.iter().all(|&b| b == first) {
58            warn!("CRITICAL: Encryption key has uniform values - this is extremely insecure!");
59            return;
60        }
61
62        // Check for repeating patterns (simple heuristic)
63        let mut unique_bytes = std::collections::HashSet::new();
64        for &b in key.iter() {
65            unique_bytes.insert(b);
66        }
67        let entropy_ratio = unique_bytes.len() as f32 / key.len() as f32;
68
69        if entropy_ratio < 0.5 {
70            warn!(
71                "WARNING: Encryption key has low entropy ({:.1}% unique bytes). Consider using a stronger key.",
72                entropy_ratio * 100.0
73            );
74        }
75
76        // Check for common weak patterns
77        let printable_only = key
78            .iter()
79            .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace());
80        if printable_only {
81            let as_string = String::from_utf8_lossy(key);
82            if as_string.contains("password")
83                || as_string.contains("secret")
84                || as_string.contains("123")
85                || as_string.contains("key")
86            {
87                warn!("WARNING: Encryption key appears to contain dictionary words or common phrases.");
88            }
89        }
90    }
91
92    pub fn encrypt(&self, plaintext: &str) -> Result<Vec<u8>, EncryptionError> {
93        let mut nonce_bytes = [0u8; 12];
94        rand::thread_rng().fill_bytes(&mut nonce_bytes);
95
96        let nonce = Nonce::from_slice(&nonce_bytes);
97
98        let ciphertext = self
99            .cipher
100            .encrypt(nonce, plaintext.as_bytes())
101            .map_err(|e| EncryptionError::EncryptionFailed(e.to_string()))?;
102
103        let mut result = Vec::with_capacity(12 + ciphertext.len());
104        result.extend_from_slice(&nonce_bytes);
105        result.extend_from_slice(&ciphertext);
106
107        Ok(result)
108    }
109
110    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<String, EncryptionError> {
111        if ciphertext.len() < 12 {
112            return Err(EncryptionError::DecryptionFailed(
113                "Ciphertext too short".to_string(),
114            ));
115        }
116
117        let (nonce_bytes, encrypted) = ciphertext.split_at(12);
118        let nonce = Nonce::from_slice(nonce_bytes);
119
120        let plaintext = self
121            .cipher
122            .decrypt(nonce, encrypted)
123            .map_err(|e| EncryptionError::DecryptionFailed(e.to_string()))?;
124
125        String::from_utf8(plaintext).map_err(|e| EncryptionError::DecryptionFailed(e.to_string()))
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_encrypt_decrypt() {
135        let key = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=";
136        let encryption = SecretsEncryption::new(key).unwrap();
137
138        let plaintext = "my-secret-api-key-12345";
139        let encrypted = encryption.encrypt(plaintext).unwrap();
140        let decrypted = encryption.decrypt(&encrypted).unwrap();
141
142        assert_eq!(plaintext, decrypted);
143    }
144}