oxify_storage/
encryption.rs

1//! Encryption service for secrets
2//!
3//! Provides AES-256-GCM encryption for secure secret storage
4
5use anyhow::{Context, Result};
6use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
7use oxify_model::EncryptionMetadata;
8use rand::Rng;
9
10/// Encryption service for secrets
11pub struct EncryptionService {
12    master_key: Vec<u8>,
13    key_version: u32,
14}
15
16impl EncryptionService {
17    /// Create a new encryption service with a master key
18    pub fn new(master_key: Vec<u8>) -> Self {
19        Self {
20            master_key,
21            key_version: 1,
22        }
23    }
24
25    /// Create encryption service from environment variable
26    pub fn from_env() -> Result<Self> {
27        let key_b64 = std::env::var("OXIFY_MASTER_KEY")
28            .context("OXIFY_MASTER_KEY environment variable not set")?;
29
30        let master_key = BASE64
31            .decode(key_b64.as_bytes())
32            .context("Failed to decode master key from base64")?;
33
34        if master_key.len() != 32 {
35            anyhow::bail!("Master key must be 32 bytes (256 bits)");
36        }
37
38        Ok(Self {
39            master_key,
40            key_version: 1,
41        })
42    }
43
44    /// Generate a random master key (for testing/initialization)
45    pub fn generate_master_key() -> Vec<u8> {
46        let mut key = vec![0u8; 32];
47        rand::rng().fill(&mut key[..]);
48        key
49    }
50
51    /// Encrypt a plaintext value
52    pub fn encrypt(&self, plaintext: &str) -> Result<(Vec<u8>, EncryptionMetadata)> {
53        use aes_gcm::{
54            aead::{Aead, KeyInit},
55            Aes256Gcm, Nonce,
56        };
57
58        // Generate random IV (nonce)
59        let mut iv = vec![0u8; 12];
60        rand::rng().fill(&mut iv[..]);
61
62        // Generate random salt for key derivation
63        let mut salt = vec![0u8; 32];
64        rand::rng().fill(&mut salt[..]);
65
66        // Derive encryption key from master key and salt using PBKDF2
67        let mut derived_key = [0u8; 32];
68        pbkdf2::pbkdf2_hmac::<sha2::Sha256>(&self.master_key, &salt, 100_000, &mut derived_key);
69
70        // Create cipher
71        let cipher = Aes256Gcm::new(&derived_key.into());
72        let nonce = Nonce::from_slice(&iv);
73
74        // Encrypt
75        let ciphertext = cipher
76            .encrypt(nonce, plaintext.as_bytes())
77            .map_err(|e| anyhow::anyhow!("Encryption failed: {e}"))?;
78
79        // Create metadata
80        let metadata = EncryptionMetadata {
81            algorithm: "AES-256-GCM".to_string(),
82            kdf: "PBKDF2-HMAC-SHA256".to_string(),
83            salt: BASE64.encode(&salt),
84            iv: BASE64.encode(&iv),
85            key_version: self.key_version,
86        };
87
88        Ok((ciphertext, metadata))
89    }
90
91    /// Decrypt a ciphertext value
92    pub fn decrypt(&self, ciphertext: &[u8], metadata: &EncryptionMetadata) -> Result<String> {
93        use aes_gcm::{
94            aead::{Aead, KeyInit},
95            Aes256Gcm, Nonce,
96        };
97
98        // Validate algorithm
99        if metadata.algorithm != "AES-256-GCM" {
100            anyhow::bail!("Unsupported encryption algorithm: {}", metadata.algorithm);
101        }
102
103        // Decode salt and IV
104        let salt = BASE64
105            .decode(metadata.salt.as_bytes())
106            .context("Failed to decode salt")?;
107        let iv = BASE64
108            .decode(metadata.iv.as_bytes())
109            .context("Failed to decode IV")?;
110
111        // Derive decryption key
112        let mut derived_key = [0u8; 32];
113        pbkdf2::pbkdf2_hmac::<sha2::Sha256>(&self.master_key, &salt, 100_000, &mut derived_key);
114
115        // Create cipher
116        let cipher = Aes256Gcm::new(&derived_key.into());
117        let nonce = Nonce::from_slice(&iv);
118
119        // Decrypt
120        let plaintext = cipher
121            .decrypt(nonce, ciphertext)
122            .map_err(|e| anyhow::anyhow!("Decryption failed: {e}"))?;
123
124        String::from_utf8(plaintext).context("Decrypted value is not valid UTF-8")
125    }
126
127    /// Rotate encryption key (re-encrypt with new key version)
128    pub fn rotate_key(&mut self, new_master_key: Vec<u8>) {
129        self.master_key = new_master_key;
130        self.key_version += 1;
131    }
132
133    /// Get current key version
134    pub fn key_version(&self) -> u32 {
135        self.key_version
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_encrypt_decrypt() {
145        let master_key = EncryptionService::generate_master_key();
146        let service = EncryptionService::new(master_key);
147
148        let plaintext = "my-secret-api-key";
149        let (ciphertext, metadata) = service.encrypt(plaintext).unwrap();
150
151        // Verify encrypted value is different
152        assert_ne!(ciphertext, plaintext.as_bytes());
153
154        // Decrypt and verify
155        let decrypted = service.decrypt(&ciphertext, &metadata).unwrap();
156        assert_eq!(decrypted, plaintext);
157    }
158
159    #[test]
160    fn test_different_ivs() {
161        let master_key = EncryptionService::generate_master_key();
162        let service = EncryptionService::new(master_key);
163
164        let plaintext = "same-plaintext";
165
166        let (ciphertext1, metadata1) = service.encrypt(plaintext).unwrap();
167        let (ciphertext2, metadata2) = service.encrypt(plaintext).unwrap();
168
169        // Different IVs should produce different ciphertexts
170        assert_ne!(metadata1.iv, metadata2.iv);
171        assert_ne!(ciphertext1, ciphertext2);
172
173        // Both should decrypt correctly
174        let decrypted1 = service.decrypt(&ciphertext1, &metadata1).unwrap();
175        let decrypted2 = service.decrypt(&ciphertext2, &metadata2).unwrap();
176
177        assert_eq!(decrypted1, plaintext);
178        assert_eq!(decrypted2, plaintext);
179    }
180
181    #[test]
182    fn test_wrong_key_fails() {
183        let master_key1 = EncryptionService::generate_master_key();
184        let master_key2 = EncryptionService::generate_master_key();
185
186        let service1 = EncryptionService::new(master_key1);
187        let service2 = EncryptionService::new(master_key2);
188
189        let plaintext = "secret-data";
190        let (ciphertext, metadata) = service1.encrypt(plaintext).unwrap();
191
192        // Decryption with wrong key should fail
193        let result = service2.decrypt(&ciphertext, &metadata);
194        assert!(result.is_err());
195    }
196}