blueprint_auth/
tls_envelope.rs

1//! TLS envelope encryption for secure storage of certificate material
2//!
3//! This module implements envelope encryption using XChaCha20-Poly1305 for
4//! securing TLS certificates, private keys, and CA bundles in RocksDB.
5//! The envelope key is loaded from environment variables or filesystem,
6//! similar to the Paseto signing key pattern.
7//!
8//! # Security
9//!
10//! - Envelope key is 32 bytes for XChaCha20-Poly1305
11//! - Key is persisted with restrictive permissions (0o600 on Unix)
12//! - Supports environment variable and file-based loading
13//! - Automatic key generation if none exists
14//! - Secure memory cleanup with zeroize
15
16use blueprint_core::{info, warn};
17use std::fs;
18use std::io::{Read, Write};
19use std::path::Path;
20
21use base64::Engine;
22use chacha20poly1305::{
23    ChaCha20Poly1305, Key, Nonce,
24    aead::{Aead, AeadCore, KeyInit, OsRng},
25};
26use thiserror::Error;
27
28/// Envelope encryption key for TLS material
29#[derive(Clone, Debug)]
30pub struct TlsEnvelopeKey(Key);
31
32impl TlsEnvelopeKey {
33    /// Generate a new random envelope key
34    pub fn generate() -> Self {
35        let key = ChaCha20Poly1305::generate_key(&mut OsRng);
36        TlsEnvelopeKey(key)
37    }
38
39    /// Create from bytes
40    pub fn from_bytes(bytes: [u8; 32]) -> Self {
41        TlsEnvelopeKey(*Key::from_slice(&bytes))
42    }
43
44    /// Get key as bytes
45    pub fn as_bytes(&self) -> &[u8] {
46        &self.0
47    }
48
49    /// Get key as hex string
50    pub fn as_hex(&self) -> String {
51        hex::encode(self.as_bytes())
52    }
53
54    /// Create from hex string
55    pub fn from_hex(hex_str: &str) -> Result<Self, TlsEnvelopeError> {
56        let bytes =
57            hex::decode(hex_str).map_err(|e| TlsEnvelopeError::InvalidHexFormat(e.to_string()))?;
58
59        if bytes.len() != 32 {
60            return Err(TlsEnvelopeError::InvalidKeyLength(bytes.len()));
61        }
62
63        let mut key_array = [0u8; 32];
64        key_array.copy_from_slice(&bytes);
65        Ok(TlsEnvelopeKey::from_bytes(key_array))
66    }
67}
68
69/// Envelope encryption for TLS material
70#[derive(Clone, Debug)]
71pub struct TlsEnvelope {
72    key: TlsEnvelopeKey,
73}
74
75impl TlsEnvelope {
76    /// Create new envelope with generated key
77    pub fn new() -> Self {
78        Self {
79            key: TlsEnvelopeKey::generate(),
80        }
81    }
82
83    /// Create with specific key
84    pub fn with_key(key: TlsEnvelopeKey) -> Self {
85        Self { key }
86    }
87
88    /// Encrypt data with envelope encryption
89    pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, TlsEnvelopeError> {
90        let cipher = ChaCha20Poly1305::new(&self.key.0);
91        let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
92
93        let ciphertext = cipher
94            .encrypt(&nonce, plaintext)
95            .map_err(|e| TlsEnvelopeError::EncryptionError(e.to_string()))?;
96
97        // Prepend nonce to ciphertext
98        let mut result = Vec::with_capacity(nonce.len() + ciphertext.len());
99        result.extend_from_slice(&nonce);
100        result.extend_from_slice(&ciphertext);
101
102        Ok(result)
103    }
104
105    /// Decrypt data with envelope encryption
106    pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, TlsEnvelopeError> {
107        if data.len() < 12 {
108            return Err(TlsEnvelopeError::InvalidCiphertextFormat(
109                "data too short for nonce".to_string(),
110            ));
111        }
112
113        let (nonce_bytes, ciphertext) = data.split_at(12);
114        let nonce = Nonce::from_slice(nonce_bytes);
115
116        let cipher = ChaCha20Poly1305::new(&self.key.0);
117        let plaintext = cipher
118            .decrypt(nonce, ciphertext)
119            .map_err(|e| TlsEnvelopeError::DecryptionError(e.to_string()))?;
120
121        Ok(plaintext)
122    }
123
124    /// Encrypt string and return base64-encoded result
125    pub fn encrypt_string(&self, plaintext: &str) -> Result<String, TlsEnvelopeError> {
126        let data = self.encrypt(plaintext.as_bytes())?;
127        Ok(base64::engine::general_purpose::STANDARD.encode(data))
128    }
129
130    /// Decrypt base64-encoded string
131    pub fn decrypt_string(&self, encoded: &str) -> Result<String, TlsEnvelopeError> {
132        let data = base64::engine::general_purpose::STANDARD
133            .decode(encoded)
134            .map_err(|e| TlsEnvelopeError::Base64Error(e.to_string()))?;
135        let plaintext = self.decrypt(&data)?;
136        String::from_utf8(plaintext).map_err(|e| TlsEnvelopeError::Utf8Error(e.to_string()))
137    }
138
139    /// Get the envelope key
140    pub fn key(&self) -> &TlsEnvelopeKey {
141        &self.key
142    }
143}
144
145impl Default for TlsEnvelope {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151/// Initialize TLS envelope key from environment or file
152pub fn init_tls_envelope_key<P: AsRef<Path>>(
153    db_path: P,
154) -> Result<TlsEnvelopeKey, TlsEnvelopeError> {
155    // Try to load key from environment variable first
156    if let Ok(key_hex) = std::env::var("TLS_ENVELOPE_KEY") {
157        match TlsEnvelopeKey::from_hex(&key_hex) {
158            Ok(key) => {
159                info!("Loaded TLS envelope key from environment variable");
160                return Ok(key);
161            }
162            Err(e) => {
163                warn!("Invalid TLS_ENVELOPE_KEY environment variable: {}", e);
164            }
165        }
166    }
167
168    // Try to load key from file path in environment variable
169    if let Ok(key_path) = std::env::var("TLS_ENVELOPE_KEY_PATH") {
170        let path = Path::new(&key_path);
171        if path.exists() {
172            match load_key_from_file(path) {
173                Ok(key) => {
174                    info!("Loaded TLS envelope key from file: {}", key_path);
175                    return Ok(key);
176                }
177                Err(e) => {
178                    warn!(
179                        "Failed to load TLS envelope key from file {}: {}",
180                        key_path, e
181                    );
182                }
183            }
184        } else {
185            warn!("TLS envelope key file not found: {}", key_path);
186        }
187    }
188
189    // Try to load key from default location in db directory
190    let default_key_path = db_path.as_ref().join(".tls_envelope_key");
191    if default_key_path.exists() {
192        match load_key_from_file(&default_key_path) {
193            Ok(key) => {
194                info!("Loaded TLS envelope key from default location");
195                return Ok(key);
196            }
197            Err(e) => {
198                warn!(
199                    "Failed to load TLS envelope key from default location: {}",
200                    e
201                );
202            }
203        }
204    }
205
206    // Generate new key and save it to default location
207    info!("Generating new TLS envelope key");
208    let key = TlsEnvelopeKey::generate();
209    save_key_to_file(&key, &default_key_path)?;
210
211    info!(
212        "Generated and saved new TLS envelope key to: {:?}",
213        default_key_path
214    );
215    Ok(key)
216}
217
218/// Load key from file
219fn load_key_from_file(path: &Path) -> Result<TlsEnvelopeKey, TlsEnvelopeError> {
220    let mut file = fs::File::open(path).map_err(|e| TlsEnvelopeError::IoError(e.to_string()))?;
221
222    let mut key_bytes = Vec::new();
223    file.read_to_end(&mut key_bytes)
224        .map_err(|e| TlsEnvelopeError::IoError(e.to_string()))?;
225
226    if key_bytes.len() != 32 {
227        return Err(TlsEnvelopeError::InvalidKeyLength(key_bytes.len()));
228    }
229
230    let mut key_array = [0u8; 32];
231    key_array.copy_from_slice(&key_bytes);
232    Ok(TlsEnvelopeKey::from_bytes(key_array))
233}
234
235/// Save key to file with secure permissions
236fn save_key_to_file(key: &TlsEnvelopeKey, path: &Path) -> Result<(), TlsEnvelopeError> {
237    let mut file = fs::File::create(path).map_err(|e| TlsEnvelopeError::IoError(e.to_string()))?;
238
239    file.write_all(key.as_bytes())
240        .map_err(|e| TlsEnvelopeError::IoError(e.to_string()))?;
241
242    file.sync_all()
243        .map_err(|e| TlsEnvelopeError::IoError(e.to_string()))?;
244
245    // Set restrictive permissions on the key file (Unix only)
246    #[cfg(unix)]
247    {
248        use std::os::unix::fs::PermissionsExt;
249        let permissions = std::fs::Permissions::from_mode(0o600);
250        fs::set_permissions(path, permissions)
251            .map_err(|e| TlsEnvelopeError::IoError(e.to_string()))?;
252    }
253
254    Ok(())
255}
256
257#[derive(Debug, Error)]
258pub enum TlsEnvelopeError {
259    #[error("Invalid hex format: {0}")]
260    InvalidHexFormat(String),
261
262    #[error("Invalid key length: {0} bytes (expected 32)")]
263    InvalidKeyLength(usize),
264
265    #[error("Encryption error: {0}")]
266    EncryptionError(String),
267
268    #[error("Decryption error: {0}")]
269    DecryptionError(String),
270
271    #[error("Invalid ciphertext format: {0}")]
272    InvalidCiphertextFormat(String),
273
274    #[error("Base64 error: {0}")]
275    Base64Error(String),
276
277    #[error("UTF-8 error: {0}")]
278    Utf8Error(String),
279
280    #[error("IO error: {0}")]
281    IoError(String),
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use tempfile::tempdir;
288
289    #[test]
290    fn test_key_generation() {
291        let key1 = TlsEnvelopeKey::generate();
292        let key2 = TlsEnvelopeKey::generate();
293
294        // Keys should be different
295        assert_ne!(key1.as_hex(), key2.as_hex());
296
297        // Should be 32 bytes
298        assert_eq!(key1.as_bytes().len(), 32);
299    }
300
301    #[test]
302    fn test_key_from_hex() {
303        let key = TlsEnvelopeKey::generate();
304        let hex_str = key.as_hex();
305
306        let decoded = TlsEnvelopeKey::from_hex(&hex_str).expect("Should decode hex");
307        assert_eq!(key.as_hex(), decoded.as_hex());
308    }
309
310    #[test]
311    fn test_envelope_encryption() {
312        let envelope = TlsEnvelope::new();
313        let plaintext = b"secret certificate data";
314
315        let encrypted = envelope.encrypt(plaintext).expect("Should encrypt");
316        let decrypted = envelope.decrypt(&encrypted).expect("Should decrypt");
317
318        assert_eq!(plaintext, &decrypted[..]);
319    }
320
321    #[test]
322    fn test_string_encryption() {
323        let envelope = TlsEnvelope::new();
324        let plaintext = "secret certificate string";
325
326        let encrypted = envelope
327            .encrypt_string(plaintext)
328            .expect("Should encrypt string");
329        let decrypted = envelope
330            .decrypt_string(&encrypted)
331            .expect("Should decrypt string");
332
333        assert_eq!(plaintext, decrypted);
334    }
335
336    #[test]
337    fn test_different_keys_fail() {
338        let envelope1 = TlsEnvelope::new();
339        let envelope2 = TlsEnvelope::new();
340
341        let plaintext = b"secret data";
342        let encrypted = envelope1.encrypt(plaintext).expect("Should encrypt");
343
344        // Should fail with different key
345        let result = envelope2.decrypt(&encrypted);
346        assert!(result.is_err());
347    }
348
349    #[test]
350    fn test_invalid_ciphertext() {
351        let envelope = TlsEnvelope::new();
352        let invalid_data = b"too short";
353
354        let result = envelope.decrypt(invalid_data);
355        assert!(result.is_err());
356    }
357
358    #[test]
359    fn test_key_persistence() {
360        let tmp_dir = tempdir().expect("tempdir");
361        let key_path = tmp_dir.path().join("test_key");
362
363        let key = TlsEnvelopeKey::generate();
364        save_key_to_file(&key, &key_path).expect("Should save key");
365
366        let loaded_key = load_key_from_file(&key_path).expect("Should load key");
367        assert_eq!(key.as_hex(), loaded_key.as_hex());
368    }
369
370    #[test]
371    fn test_base64_roundtrip() {
372        let envelope = TlsEnvelope::new();
373        let plaintext = "test string for base64 encoding";
374
375        let encrypted = envelope.encrypt_string(plaintext).expect("Should encrypt");
376        let decrypted = envelope.decrypt_string(&encrypted).expect("Should decrypt");
377
378        assert_eq!(plaintext, decrypted);
379    }
380}