gestura_core_security/
encryption.rs

1//! Encryption utilities for local data protection
2//!
3//! Provides AES-256-GCM encryption for protecting sensitive configuration
4//! and user data at rest.
5//!
6//! # Security Notes
7//!
8//! - Uses AES-256-GCM for authenticated encryption
9//! - Nonces are randomly generated and prepended to ciphertext
10//! - Keys should be stored securely (e.g., in OS keychain)
11
12use crate::SecureStorage;
13use gestura_core_foundation::AppError;
14use ring::aead::{AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey};
15use ring::rand::{SecureRandom, SystemRandom};
16
17/// AES-256-GCM encryptor for protecting sensitive data
18pub struct Encryptor {
19    key: LessSafeKey,
20    rng: SystemRandom,
21}
22
23impl Encryptor {
24    /// Create a new encryptor with a randomly generated key
25    pub fn new() -> Result<Self, AppError> {
26        let rng = SystemRandom::new();
27        let mut key_bytes = [0u8; 32];
28        rng.fill(&mut key_bytes)
29            .map_err(|_| AppError::Internal("failed to generate encryption key".to_string()))?;
30        Self::from_key(&key_bytes)
31    }
32
33    /// Create an encryptor from an existing 32-byte key
34    pub fn from_key(key_bytes: &[u8; 32]) -> Result<Self, AppError> {
35        let rng = SystemRandom::new();
36        let unbound_key = UnboundKey::new(&AES_256_GCM, key_bytes)
37            .map_err(|_| AppError::Internal("failed to create encryption key".to_string()))?;
38        let key = LessSafeKey::new(unbound_key);
39        Ok(Self { key, rng })
40    }
41
42    /// Encrypt data, prepending the nonce to the ciphertext
43    pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>, AppError> {
44        let mut nonce_bytes = [0u8; 12];
45        self.rng
46            .fill(&mut nonce_bytes)
47            .map_err(|_| AppError::Internal("failed to generate nonce".to_string()))?;
48        let nonce = Nonce::assume_unique_for_key(nonce_bytes);
49
50        let mut in_out = data.to_vec();
51        self.key
52            .seal_in_place_append_tag(nonce, Aad::empty(), &mut in_out)
53            .map_err(|_| AppError::Internal("encryption failed".to_string()))?;
54
55        let mut result = nonce_bytes.to_vec();
56        result.extend_from_slice(&in_out);
57        Ok(result)
58    }
59
60    /// Decrypt data (nonce is expected to be prepended)
61    pub fn decrypt(&self, encrypted_data: &[u8]) -> Result<Vec<u8>, AppError> {
62        if encrypted_data.len() < 12 {
63            return Err(AppError::InvalidInput(
64                "encrypted data too short".to_string(),
65            ));
66        }
67
68        let (nonce_bytes, ciphertext) = encrypted_data.split_at(12);
69        let nonce = Nonce::try_assume_unique_for_key(nonce_bytes)
70            .map_err(|_| AppError::InvalidInput("invalid nonce".to_string()))?;
71
72        let mut in_out = ciphertext.to_vec();
73        let plaintext = self
74            .key
75            .open_in_place(nonce, Aad::empty(), &mut in_out)
76            .map_err(|_| AppError::InvalidInput("decryption failed".to_string()))?;
77        Ok(plaintext.to_vec())
78    }
79}
80
81/// Secure configuration manager with encryption and keychain-backed key storage
82pub struct SecureConfigManager {
83    encryptor: Encryptor,
84    #[allow(dead_code)]
85    storage: Box<dyn SecureStorage>,
86}
87
88impl SecureConfigManager {
89    /// Create a new secure config manager
90    ///
91    /// Loads or generates an encryption key stored in the OS keychain.
92    pub async fn new(storage: Box<dyn SecureStorage>) -> Result<Self, AppError> {
93        let encryptor = match storage.get_secret("config_encryption_key").await? {
94            Some(key_hex) => {
95                let key_bytes = hex::decode(key_hex)
96                    .map_err(|_| AppError::InvalidInput("invalid key format".to_string()))?;
97                if key_bytes.len() != 32 {
98                    return Err(AppError::InvalidInput("invalid key length".to_string()));
99                }
100                let mut key_array = [0u8; 32];
101                key_array.copy_from_slice(&key_bytes);
102                Encryptor::from_key(&key_array)?
103            }
104            None => {
105                let rng = SystemRandom::new();
106                let mut key_bytes = [0u8; 32];
107                rng.fill(&mut key_bytes)
108                    .map_err(|_| AppError::Internal("failed to generate key".to_string()))?;
109                let key_hex = hex::encode(key_bytes);
110                storage
111                    .store_secret("config_encryption_key", &key_hex)
112                    .await?;
113                Encryptor::from_key(&key_bytes)?
114            }
115        };
116
117        Ok(Self { encryptor, storage })
118    }
119
120    /// Create with provided storage (for testing)
121    pub async fn with_storage(storage: Box<dyn SecureStorage>) -> Result<Self, AppError> {
122        Self::new(storage).await
123    }
124
125    /// Encrypt serializable data
126    pub fn encrypt<T: serde::Serialize>(&self, data: &T) -> Result<Vec<u8>, AppError> {
127        let json = serde_json::to_string(data)?;
128        self.encryptor.encrypt(json.as_bytes())
129    }
130
131    /// Decrypt to deserializable data
132    pub fn decrypt<T: serde::de::DeserializeOwned>(&self, encrypted: &[u8]) -> Result<T, AppError> {
133        let decrypted = self.encryptor.decrypt(encrypted)?;
134        let json = String::from_utf8(decrypted)
135            .map_err(|_| AppError::InvalidInput("invalid UTF-8".to_string()))?;
136        serde_json::from_str(&json).map_err(AppError::Json)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::MockSecureStorage;
144
145    #[test]
146    fn test_encryptor_encrypt_decrypt() {
147        let encryptor = Encryptor::new().unwrap();
148        let plaintext = b"Hello, World!";
149        let encrypted = encryptor.encrypt(plaintext).unwrap();
150        let decrypted = encryptor.decrypt(&encrypted).unwrap();
151        assert_eq!(decrypted, plaintext);
152    }
153
154    #[test]
155    fn test_encryptor_from_key() {
156        let key = [0u8; 32];
157        let encryptor = Encryptor::from_key(&key).unwrap();
158        let plaintext = b"test data";
159        let encrypted = encryptor.encrypt(plaintext).unwrap();
160
161        // Decrypt with same key
162        let encryptor2 = Encryptor::from_key(&key).unwrap();
163        let decrypted = encryptor2.decrypt(&encrypted).unwrap();
164        assert_eq!(decrypted, plaintext);
165    }
166
167    #[test]
168    fn test_decrypt_invalid_data() {
169        let encryptor = Encryptor::new().unwrap();
170        let result = encryptor.decrypt(&[0u8; 5]);
171        assert!(result.is_err());
172    }
173
174    #[tokio::test]
175    async fn test_secure_config_manager() {
176        let storage = Box::new(MockSecureStorage::new());
177        let manager = SecureConfigManager::with_storage(storage).await.unwrap();
178
179        #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
180        struct TestConfig {
181            api_key: String,
182            enabled: bool,
183        }
184
185        let config = TestConfig {
186            api_key: "secret123".to_string(),
187            enabled: true,
188        };
189
190        let encrypted = manager.encrypt(&config).unwrap();
191        let decrypted: TestConfig = manager.decrypt(&encrypted).unwrap();
192        assert_eq!(decrypted, config);
193    }
194}