blueprint_auth/
tls_envelope.rs1use 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#[derive(Clone, Debug)]
30pub struct TlsEnvelopeKey(Key);
31
32impl TlsEnvelopeKey {
33 pub fn generate() -> Self {
35 let key = ChaCha20Poly1305::generate_key(&mut OsRng);
36 TlsEnvelopeKey(key)
37 }
38
39 pub fn from_bytes(bytes: [u8; 32]) -> Self {
41 TlsEnvelopeKey(*Key::from_slice(&bytes))
42 }
43
44 pub fn as_bytes(&self) -> &[u8] {
46 &self.0
47 }
48
49 pub fn as_hex(&self) -> String {
51 hex::encode(self.as_bytes())
52 }
53
54 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#[derive(Clone, Debug)]
71pub struct TlsEnvelope {
72 key: TlsEnvelopeKey,
73}
74
75impl TlsEnvelope {
76 pub fn new() -> Self {
78 Self {
79 key: TlsEnvelopeKey::generate(),
80 }
81 }
82
83 pub fn with_key(key: TlsEnvelopeKey) -> Self {
85 Self { key }
86 }
87
88 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 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 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 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 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 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
151pub fn init_tls_envelope_key<P: AsRef<Path>>(
153 db_path: P,
154) -> Result<TlsEnvelopeKey, TlsEnvelopeError> {
155 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 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 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 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
218fn 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
235fn 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 #[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 assert_ne!(key1.as_hex(), key2.as_hex());
296
297 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 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}