1pub use errors::*;
16pub use key_management::{FileKeyStorage, KeyStorage, KeyStore as KeyManagementStore};
17
18use crate::workspace_persistence::WorkspacePersistence;
19use aes_gcm::{
20 aead::{Aead, KeyInit},
21 Aes256Gcm, Nonce,
22};
23use argon2::{
24 password_hash::{PasswordHasher, SaltString},
25 Argon2, Params,
26};
27use base64::{engine::general_purpose, Engine as _};
28use chacha20poly1305::{ChaCha20Poly1305, Key as ChaChaKey};
29use pbkdf2::pbkdf2_hmac;
30use rand::{rng, Rng};
31use serde::{Deserialize, Serialize};
32use sha2::Sha256;
33use std::fmt;
34use thiserror::Error;
35use tracing;
36
37#[cfg(target_os = "windows")]
38use windows::Win32::Security::Credentials::{
39 CredDeleteW, CredReadW, CredWriteW, CREDENTIALW, CRED_PERSIST_LOCAL_MACHINE, CRED_TYPE_GENERIC,
40};
41
42#[derive(Error, Debug)]
44pub enum EncryptionError {
45 #[error("Encryption failure: {0}")]
47 Encryption(String),
48 #[error("Decryption failure: {0}")]
50 Decryption(String),
51 #[error("Invalid key: {0}")]
53 InvalidKey(String),
54 #[error("Invalid ciphertext: {0}")]
56 InvalidCiphertext(String),
57 #[error("Key derivation failure: {0}")]
59 KeyDerivation(String),
60 #[error("Generic encryption error: {message}")]
62 Generic {
63 message: String,
65 },
66}
67
68pub type Result<T> = std::result::Result<T, EncryptionError>;
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum EncryptionAlgorithm {
74 Aes256Gcm,
76 ChaCha20Poly1305,
78}
79
80impl fmt::Display for EncryptionAlgorithm {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 match self {
83 EncryptionAlgorithm::Aes256Gcm => write!(f, "aes256-gcm"),
84 EncryptionAlgorithm::ChaCha20Poly1305 => write!(f, "chacha20-poly1305"),
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum KeyDerivationMethod {
92 Pbkdf2,
94 Argon2,
96}
97
98impl fmt::Display for KeyDerivationMethod {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 KeyDerivationMethod::Pbkdf2 => write!(f, "pbkdf2"),
102 KeyDerivationMethod::Argon2 => write!(f, "argon2"),
103 }
104 }
105}
106
107pub struct EncryptionKey {
109 algorithm: EncryptionAlgorithm,
110 key_data: Vec<u8>,
111}
112
113impl EncryptionKey {
114 pub fn new(algorithm: EncryptionAlgorithm, key_data: Vec<u8>) -> Result<Self> {
116 let expected_len = match algorithm {
117 EncryptionAlgorithm::Aes256Gcm => 32, EncryptionAlgorithm::ChaCha20Poly1305 => 32, };
120
121 if key_data.len() != expected_len {
122 return Err(EncryptionError::InvalidKey(format!(
123 "Key must be {} bytes for {}, got {}",
124 expected_len,
125 algorithm,
126 key_data.len()
127 )));
128 }
129
130 Ok(Self {
131 algorithm,
132 key_data,
133 })
134 }
135
136 pub fn from_password_pbkdf2(
138 password: &str,
139 salt: Option<&[u8]>,
140 algorithm: EncryptionAlgorithm,
141 ) -> Result<Self> {
142 let salt = salt.map(|s| s.to_vec()).unwrap_or_else(|| rng().random::<[u8; 32]>().to_vec());
143
144 let mut key = vec![0u8; 32];
145 pbkdf2_hmac::<Sha256>(password.as_bytes(), &salt, 100_000, &mut key);
146
147 Self::new(algorithm, key)
148 }
149
150 pub fn from_password_argon2(
152 password: &str,
153 salt: Option<&[u8]>,
154 algorithm: EncryptionAlgorithm,
155 ) -> Result<Self> {
156 let salt_string = if let Some(salt) = salt {
157 SaltString::encode_b64(salt)
158 .map_err(|e| EncryptionError::KeyDerivation(e.to_string()))?
159 } else {
160 let mut salt_bytes = [0u8; 32];
162 rng().fill(&mut salt_bytes);
163 SaltString::encode_b64(&salt_bytes)
164 .map_err(|e| EncryptionError::KeyDerivation(e.to_string()))?
165 };
166
167 let argon2 = Argon2::new(
168 argon2::Algorithm::Argon2id,
169 argon2::Version::V0x13,
170 Params::new(65536, 3, 1, Some(32))
171 .map_err(|e| EncryptionError::KeyDerivation(e.to_string()))?,
172 );
173
174 let hash = argon2
175 .hash_password(password.as_bytes(), &salt_string)
176 .map_err(|e| EncryptionError::KeyDerivation(e.to_string()))?;
177
178 let key_bytes = hash
179 .hash
180 .ok_or_else(|| EncryptionError::KeyDerivation("Failed to derive key hash".to_string()))?
181 .as_bytes()
182 .to_vec();
183 Self::new(algorithm, key_bytes)
184 }
185
186 pub fn encrypt(&self, plaintext: &str, associated_data: Option<&[u8]>) -> Result<String> {
188 match self.algorithm {
189 EncryptionAlgorithm::Aes256Gcm => self.encrypt_aes_gcm(plaintext, associated_data),
190 EncryptionAlgorithm::ChaCha20Poly1305 => {
191 self.encrypt_chacha20(plaintext, associated_data)
192 }
193 }
194 }
195
196 pub fn decrypt(&self, ciphertext: &str, associated_data: Option<&[u8]>) -> Result<String> {
198 match self.algorithm {
199 EncryptionAlgorithm::Aes256Gcm => self.decrypt_aes_gcm(ciphertext, associated_data),
200 EncryptionAlgorithm::ChaCha20Poly1305 => {
201 self.decrypt_chacha20(ciphertext, associated_data)
202 }
203 }
204 }
205
206 fn encrypt_aes_gcm(&self, plaintext: &str, associated_data: Option<&[u8]>) -> Result<String> {
207 let key_array: [u8; 32] =
209 self.key_data.as_slice().try_into().map_err(|_| {
210 EncryptionError::InvalidKey("Key length must be 32 bytes".to_string())
211 })?;
212 let cipher = Aes256Gcm::new(&key_array.into());
213 let nonce: [u8; 12] = rng().random(); let nonce = Nonce::from(nonce);
215
216 let ciphertext = cipher
217 .encrypt(&nonce, plaintext.as_bytes())
218 .map_err(|e| EncryptionError::Encryption(e.to_string()))?;
219
220 let mut result = nonce.to_vec();
221 result.extend_from_slice(&ciphertext);
222
223 if let Some(aad) = associated_data {
225 result.extend_from_slice(aad);
226 }
227
228 Ok(general_purpose::STANDARD.encode(&result))
229 }
230
231 fn decrypt_aes_gcm(&self, ciphertext: &str, associated_data: Option<&[u8]>) -> Result<String> {
232 let data = general_purpose::STANDARD
233 .decode(ciphertext)
234 .map_err(|e| EncryptionError::InvalidCiphertext(e.to_string()))?;
235
236 if data.len() < 12 {
237 return Err(EncryptionError::InvalidCiphertext("Ciphertext too short".to_string()));
238 }
239
240 let nonce_array: [u8; 12] = data[0..12].try_into().map_err(|_| {
242 EncryptionError::InvalidCiphertext("Nonce must be 12 bytes".to_string())
243 })?;
244 let nonce = Nonce::from(nonce_array);
245
246 let ciphertext_len = if let Some(aad) = &associated_data {
247 let aad_len = aad.len();
249 data.len() - 12 - aad_len
250 } else {
251 data.len() - 12
252 };
253
254 let ciphertext = &data[12..12 + ciphertext_len];
255
256 let key_array: [u8; 32] =
258 self.key_data.as_slice().try_into().map_err(|_| {
259 EncryptionError::InvalidKey("Key length must be 32 bytes".to_string())
260 })?;
261 let cipher = Aes256Gcm::new(&key_array.into());
262
263 let plaintext = cipher
264 .decrypt(&nonce, ciphertext.as_ref())
265 .map_err(|e| EncryptionError::Decryption(e.to_string()))?;
266
267 String::from_utf8(plaintext)
268 .map_err(|e| EncryptionError::Decryption(format!("Invalid UTF-8: {}", e)))
269 }
270
271 pub fn encrypt_chacha20(
280 &self,
281 plaintext: &str,
282 _associated_data: Option<&[u8]>,
283 ) -> Result<String> {
284 let key = ChaChaKey::from_slice(&self.key_data);
285 let cipher = ChaCha20Poly1305::new(key);
286 let nonce: [u8; 12] = rng().random(); let nonce = chacha20poly1305::Nonce::from_slice(&nonce);
288
289 let ciphertext = cipher
290 .encrypt(nonce, plaintext.as_bytes())
291 .map_err(|e| EncryptionError::Encryption(e.to_string()))?;
292
293 let mut result = nonce.to_vec();
294 result.extend_from_slice(&ciphertext);
295
296 Ok(general_purpose::STANDARD.encode(&result))
297 }
298
299 pub fn decrypt_chacha20(
308 &self,
309 ciphertext: &str,
310 _associated_data: Option<&[u8]>,
311 ) -> Result<String> {
312 let data = general_purpose::STANDARD
313 .decode(ciphertext)
314 .map_err(|e| EncryptionError::InvalidCiphertext(e.to_string()))?;
315
316 if data.len() < 12 {
317 return Err(EncryptionError::InvalidCiphertext("Ciphertext too short".to_string()));
318 }
319
320 let nonce = chacha20poly1305::Nonce::from_slice(&data[0..12]);
321 let ciphertext_data = &data[12..];
322 let key = ChaChaKey::from_slice(&self.key_data);
323 let cipher = ChaCha20Poly1305::new(key);
324
325 let plaintext = cipher
326 .decrypt(nonce, ciphertext_data.as_ref())
327 .map_err(|e| EncryptionError::Decryption(e.to_string()))?;
328
329 String::from_utf8(plaintext)
330 .map_err(|e| EncryptionError::Decryption(format!("Invalid UTF-8: {}", e)))
331 }
332}
333
334pub struct KeyStore {
336 keys: std::collections::HashMap<String, EncryptionKey>,
337}
338
339impl KeyStore {
340 pub fn new() -> Self {
342 Self {
343 keys: std::collections::HashMap::new(),
344 }
345 }
346
347 pub fn store_key(&mut self, id: String, key: EncryptionKey) {
349 self.keys.insert(id, key);
350 }
351
352 pub fn get_key(&self, id: &str) -> Option<&EncryptionKey> {
354 self.keys.get(id)
355 }
356
357 pub fn remove_key(&mut self, id: &str) -> bool {
359 self.keys.remove(id).is_some()
360 }
361
362 pub fn list_keys(&self) -> Vec<String> {
364 self.keys.keys().cloned().collect()
365 }
366
367 pub fn derive_and_store_key(
369 &mut self,
370 id: String,
371 password: &str,
372 algorithm: EncryptionAlgorithm,
373 method: KeyDerivationMethod,
374 ) -> Result<()> {
375 let key = match method {
376 KeyDerivationMethod::Pbkdf2 => {
377 EncryptionKey::from_password_pbkdf2(password, None, algorithm)?
378 }
379 KeyDerivationMethod::Argon2 => {
380 EncryptionKey::from_password_argon2(password, None, algorithm)?
381 }
382 };
383 self.store_key(id, key);
384 Ok(())
385 }
386}
387
388impl Default for KeyStore {
389 fn default() -> Self {
390 Self::new()
391 }
392}
393
394static KEY_STORE: once_cell::sync::OnceCell<KeyStore> = once_cell::sync::OnceCell::new();
396
397pub fn init_key_store() -> &'static KeyStore {
406 KEY_STORE.get_or_init(KeyStore::default)
407}
408
409pub fn get_key_store() -> Option<&'static KeyStore> {
416 KEY_STORE.get()
417}
418
419pub struct MasterKeyManager {
426 _service_name: String,
428 _account_name: String,
430}
431
432impl MasterKeyManager {
433 pub fn new() -> Self {
435 Self {
436 _service_name: "com.mockforge.encryption".to_string(),
437 _account_name: "master_key".to_string(),
438 }
439 }
440
441 pub fn generate_master_key(&self) -> Result<()> {
443 let master_key_bytes: [u8; 32] = rand::random();
444 let master_key_b64 = general_purpose::STANDARD.encode(master_key_bytes);
445
446 #[cfg(target_os = "macos")]
449 {
450 self.store_in_macos_keychain(&master_key_b64)?;
452 }
453 #[cfg(target_os = "linux")]
454 {
455 self.store_in_linux_keyring(&master_key_b64)?;
457 }
458 #[cfg(target_os = "windows")]
459 {
460 self.store_in_windows_credential_manager(&master_key_b64)?;
462 }
463
464 Ok(())
465 }
466
467 pub fn get_master_key(&self) -> Result<EncryptionKey> {
469 let master_key_b64 = self.retrieve_from_keychain()?;
470 let master_key_bytes = general_purpose::STANDARD
471 .decode(master_key_b64)
472 .map_err(|e| EncryptionError::InvalidKey(e.to_string()))?;
473
474 if master_key_bytes.len() != 32 {
475 return Err(EncryptionError::InvalidKey("Invalid master key length".to_string()));
476 }
477
478 EncryptionKey::new(EncryptionAlgorithm::ChaCha20Poly1305, master_key_bytes)
479 }
480
481 pub fn has_master_key(&self) -> bool {
483 self.retrieve_from_keychain().is_ok()
484 }
485
486 #[cfg(target_os = "macos")]
488 fn store_in_macos_keychain(&self, key: &str) -> Result<()> {
489 use std::os::unix::fs::PermissionsExt;
490
491 let home = std::env::var("HOME").map_err(|_| {
492 EncryptionError::InvalidKey("HOME environment variable not set".to_string())
493 })?;
494 let key_path = std::path::Path::new(&home).join(".mockforge").join("master_key");
495
496 if let Some(parent) = key_path.parent() {
498 std::fs::create_dir_all(parent).map_err(|e| {
499 EncryptionError::InvalidKey(format!("Failed to create directory: {}", e))
500 })?;
501 }
502
503 std::fs::write(&key_path, key).map_err(|e| {
505 EncryptionError::InvalidKey(format!("Failed to write master key: {}", e))
506 })?;
507
508 let mut perms = std::fs::metadata(&key_path)
510 .map_err(|e| EncryptionError::InvalidKey(format!("Failed to get metadata: {}", e)))?
511 .permissions();
512 perms.set_mode(0o600);
513 std::fs::set_permissions(&key_path, perms).map_err(|e| {
514 EncryptionError::InvalidKey(format!("Failed to set permissions: {}", e))
515 })?;
516
517 Ok(())
518 }
519
520 #[cfg(target_os = "linux")]
521 fn store_in_linux_keyring(&self, key: &str) -> Result<()> {
522 use std::os::unix::fs::PermissionsExt;
523
524 let home = std::env::var("HOME").map_err(|_| {
525 EncryptionError::InvalidKey("HOME environment variable not set".to_string())
526 })?;
527 let key_path = std::path::Path::new(&home).join(".mockforge").join("master_key");
528
529 if let Some(parent) = key_path.parent() {
531 std::fs::create_dir_all(parent).map_err(|e| {
532 EncryptionError::InvalidKey(format!("Failed to create directory: {}", e))
533 })?;
534 }
535
536 std::fs::write(&key_path, key).map_err(|e| {
538 EncryptionError::InvalidKey(format!("Failed to write master key: {}", e))
539 })?;
540
541 let mut perms = std::fs::metadata(&key_path)
543 .map_err(|e| EncryptionError::InvalidKey(format!("Failed to get metadata: {}", e)))?
544 .permissions();
545 perms.set_mode(0o600);
546 std::fs::set_permissions(&key_path, perms).map_err(|e| {
547 EncryptionError::InvalidKey(format!("Failed to set permissions: {}", e))
548 })?;
549
550 Ok(())
551 }
552
553 #[cfg(target_os = "windows")]
554 fn store_in_windows_credential_manager(&self, key: &str) -> Result<()> {
555 use std::ffi::OsString;
556 use std::os::windows::ffi::OsStringExt;
557 use windows::core::PCWSTR;
558 use windows::Win32::Foundation::ERROR_NO_SUCH_LOGON_SESSION;
559 use windows::Win32::Security::Credentials::{
560 CredWriteW, CREDENTIALW, CRED_PERSIST_LOCAL_MACHINE, CRED_TYPE_GENERIC,
561 };
562
563 let target_name = "MockForge/MasterKey";
564 let target_name_wide: Vec<u16> =
565 OsString::from(target_name).encode_wide().chain(std::iter::once(0)).collect();
566
567 let credential_blob: Vec<u16> =
568 OsString::from(key).encode_wide().chain(std::iter::once(0)).collect();
569
570 let mut credential = CREDENTIALW {
571 Flags: 0,
572 Type: CRED_TYPE_GENERIC,
573 TargetName: PCWSTR::from_raw(target_name_wide.as_ptr()),
574 Comment: PCWSTR::null(),
575 LastWritten: windows::Win32::Foundation::FILETIME::default(),
576 CredentialBlobSize: (credential_blob.len() * 2) as u32,
577 CredentialBlob: credential_blob.as_ptr() as *mut u8,
578 Persist: CRED_PERSIST_LOCAL_MACHINE,
579 AttributeCount: 0,
580 Attributes: std::ptr::null_mut(),
581 TargetAlias: PCWSTR::null(),
582 UserName: PCWSTR::null(),
583 };
584
585 unsafe {
589 CredWriteW(&mut credential, 0).map_err(|e| {
590 EncryptionError::InvalidKey(format!("Failed to store credential: {:?}", e))
591 })?;
592 }
593
594 Ok(())
595 }
596
597 fn retrieve_from_keychain(&self) -> Result<String> {
598 #[cfg(target_os = "macos")]
600 {
601 let home = std::env::var("HOME").map_err(|_| {
602 EncryptionError::InvalidKey("HOME environment variable not set".to_string())
603 })?;
604 let key_path = std::path::Path::new(&home).join(".mockforge").join("master_key");
605 let key_str = std::fs::read_to_string(&key_path).map_err(|_| {
606 EncryptionError::InvalidKey("Master key not found in keychain".to_string())
607 })?;
608 Ok(key_str.trim().to_string())
610 }
611
612 #[cfg(target_os = "linux")]
613 {
614 let home = std::env::var("HOME").map_err(|_| {
615 EncryptionError::InvalidKey("HOME environment variable not set".to_string())
616 })?;
617 let key_path = std::path::Path::new(&home).join(".mockforge").join("master_key");
618 let key_str = std::fs::read_to_string(&key_path).map_err(|_| {
619 EncryptionError::InvalidKey("Master key not found in keychain".to_string())
620 })?;
621 Ok(key_str.trim().to_string())
623 }
624
625 #[cfg(target_os = "windows")]
626 {
627 self.retrieve_from_windows_credential_manager()
629 }
630
631 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
632 {
633 let key_str = std::env::var("MOCKFORGE_MASTER_KEY").map_err(|_| {
635 EncryptionError::InvalidKey("Master key not found in keychain".to_string())
636 })?;
637 Ok(key_str.trim().to_string())
639 }
640 }
641
642 #[cfg(target_os = "windows")]
643 fn retrieve_from_windows_credential_manager(&self) -> Result<String> {
644 use std::ffi::OsString;
645 use std::os::windows::ffi::OsStringExt;
646 use windows::core::PCWSTR;
647 use windows::Win32::Security::Credentials::{
648 CredFree, CredReadW, CREDENTIALW, CRED_TYPE_GENERIC,
649 };
650
651 let target_name = "MockForge/MasterKey";
652 let target_name_wide: Vec<u16> =
653 OsString::from(target_name).encode_wide().chain(std::iter::once(0)).collect();
654
655 let mut credential_ptr: *mut CREDENTIALW = std::ptr::null_mut();
656
657 unsafe {
662 CredReadW(
663 PCWSTR::from_raw(target_name_wide.as_ptr()),
664 CRED_TYPE_GENERIC,
665 0,
666 &mut credential_ptr,
667 )
668 .map_err(|e| {
669 EncryptionError::InvalidKey(format!("Failed to read credential: {:?}", e))
670 })?;
671
672 if credential_ptr.is_null() {
673 return Err(EncryptionError::InvalidKey("Credential not found".to_string()));
674 }
675
676 let credential = &*credential_ptr;
678
679 let blob_slice = std::slice::from_raw_parts(
682 credential.CredentialBlob as *const u16,
683 credential.CredentialBlobSize as usize / 2, );
685
686 let credential_str = OsString::from_wide(blob_slice)
687 .to_string_lossy()
688 .trim_end_matches('\0')
689 .to_string();
690
691 CredFree(credential_ptr as *mut std::ffi::c_void);
693
694 Ok(credential_str)
695 }
696 }
697}
698
699impl Default for MasterKeyManager {
700 fn default() -> Self {
701 Self::new()
702 }
703}
704
705pub struct WorkspaceKeyManager {
710 master_key_manager: MasterKeyManager,
712 key_storage: std::cell::RefCell<FileKeyStorage>,
714}
715
716impl WorkspaceKeyManager {
717 pub fn new() -> Self {
719 Self {
720 master_key_manager: MasterKeyManager::new(),
721 key_storage: std::cell::RefCell::new(FileKeyStorage::new()),
722 }
723 }
724
725 pub fn with_storage_path<P: AsRef<std::path::Path>>(path: P) -> Self {
727 Self {
728 master_key_manager: MasterKeyManager::new(),
729 key_storage: std::cell::RefCell::new(FileKeyStorage::with_path(path)),
730 }
731 }
732
733 pub fn generate_workspace_key(&self, workspace_id: &str) -> Result<String> {
735 let workspace_key_bytes: [u8; 32] = rand::random();
737
738 let master_key = self.master_key_manager.get_master_key()?;
740
741 let workspace_key_b64 = master_key.encrypt_chacha20(
743 &general_purpose::STANDARD.encode(workspace_key_bytes),
744 Some(workspace_id.as_bytes()),
745 )?;
746
747 self.store_workspace_key(workspace_id, &workspace_key_b64)?;
749
750 Ok(workspace_key_b64)
751 }
752
753 pub fn get_workspace_key(&self, workspace_id: &str) -> Result<EncryptionKey> {
755 let encrypted_key_b64 = self.retrieve_workspace_key(workspace_id)?;
756 let master_key = self.master_key_manager.get_master_key()?;
757
758 let decrypted_key_b64 =
759 master_key.decrypt_chacha20(&encrypted_key_b64, Some(workspace_id.as_bytes()))?;
760
761 let workspace_key_bytes = general_purpose::STANDARD
762 .decode(decrypted_key_b64)
763 .map_err(|e| EncryptionError::InvalidKey(e.to_string()))?;
764
765 if workspace_key_bytes.len() != 32 {
766 return Err(EncryptionError::InvalidKey("Invalid workspace key length".to_string()));
767 }
768
769 EncryptionKey::new(EncryptionAlgorithm::ChaCha20Poly1305, workspace_key_bytes)
770 }
771
772 pub fn has_workspace_key(&self, workspace_id: &str) -> bool {
774 self.retrieve_workspace_key(workspace_id).is_ok()
775 }
776
777 pub fn generate_workspace_key_backup(&self, workspace_id: &str) -> Result<String> {
779 let encrypted_key = self.retrieve_workspace_key(workspace_id)?;
780
781 let backup_string = self.format_backup_string(&encrypted_key);
784
785 Ok(backup_string)
786 }
787
788 pub fn restore_workspace_key_from_backup(
790 &self,
791 workspace_id: &str,
792 backup_string: &str,
793 ) -> Result<()> {
794 let encrypted_key = self.parse_backup_string(backup_string)?;
795 self.store_workspace_key(workspace_id, &encrypted_key)
796 }
797
798 fn store_workspace_key(&self, workspace_id: &str, encrypted_key: &str) -> Result<()> {
800 self.key_storage
801 .borrow_mut()
802 .store_key(&workspace_id.to_string(), encrypted_key.as_bytes())
803 .map_err(|e| {
804 EncryptionError::InvalidKey(format!("Failed to store workspace key: {:?}", e))
805 })
806 }
807
808 fn retrieve_workspace_key(&self, workspace_id: &str) -> Result<String> {
809 match self.key_storage.borrow().retrieve_key(&workspace_id.to_string()) {
811 Ok(encrypted_bytes) => String::from_utf8(encrypted_bytes).map_err(|e| {
812 EncryptionError::InvalidKey(format!("Invalid UTF-8 in stored key: {}", e))
813 }),
814 Err(_) => {
815 let old_key_file = format!("workspace_{}_key.enc", workspace_id);
817 match std::fs::read_to_string(&old_key_file) {
818 Ok(encrypted_key) => {
819 if let Err(e) = self
821 .key_storage
822 .borrow_mut()
823 .store_key(&workspace_id.to_string(), encrypted_key.as_bytes())
824 {
825 tracing::warn!(
826 "Failed to migrate workspace key to new storage: {:?}",
827 e
828 );
829 } else {
830 let _ = std::fs::remove_file(&old_key_file);
832 }
833 Ok(encrypted_key)
834 }
835 Err(_) => Err(EncryptionError::InvalidKey(format!(
836 "Workspace key not found for: {}",
837 workspace_id
838 ))),
839 }
840 }
841 }
842 }
843
844 fn format_backup_string(&self, encrypted_key: &str) -> String {
845 let chars: Vec<char> = encrypted_key.chars().collect();
847 let mut result = String::new();
848
849 for (i, &ch) in chars.iter().enumerate() {
850 if i > 0 && i % 6 == 0 && i < chars.len() - 1 {
851 result.push('-');
852 }
853 result.push(ch);
854 }
855
856 if result.len() > 59 {
858 result.truncate(59);
860 }
861
862 result
863 }
864
865 fn parse_backup_string(&self, backup_string: &str) -> Result<String> {
866 Ok(backup_string.replace("-", ""))
868 }
869}
870
871impl Default for WorkspaceKeyManager {
872 fn default() -> Self {
873 Self::new()
874 }
875}
876
877#[derive(Debug, Clone, Serialize, Deserialize)]
879pub struct AutoEncryptionConfig {
880 pub enabled: bool,
882 pub sensitive_headers: Vec<String>,
884 pub sensitive_fields: Vec<String>,
886 pub sensitive_env_vars: Vec<String>,
888 pub sensitive_patterns: Vec<String>,
890}
891
892impl Default for AutoEncryptionConfig {
893 fn default() -> Self {
894 Self {
895 enabled: false,
896 sensitive_headers: vec![
897 "authorization".to_string(),
898 "x-api-key".to_string(),
899 "x-auth-token".to_string(),
900 "cookie".to_string(),
901 "set-cookie".to_string(),
902 ],
903 sensitive_fields: vec![
904 "password".to_string(),
905 "token".to_string(),
906 "secret".to_string(),
907 "key".to_string(),
908 "credentials".to_string(),
909 ],
910 sensitive_env_vars: vec![
911 "API_KEY".to_string(),
912 "SECRET_KEY".to_string(),
913 "PASSWORD".to_string(),
914 "TOKEN".to_string(),
915 "DATABASE_URL".to_string(),
916 ],
917 sensitive_patterns: vec![
918 r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b".to_string(), r"\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b".to_string(), ],
921 }
922 }
923}
924
925pub struct AutoEncryptionProcessor {
930 config: AutoEncryptionConfig,
932 workspace_manager: WorkspaceKeyManager,
934 workspace_id: String,
936}
937
938impl AutoEncryptionProcessor {
939 pub fn new(workspace_id: &str, config: AutoEncryptionConfig) -> Self {
941 Self {
942 config,
943 workspace_manager: WorkspaceKeyManager::new(),
944 workspace_id: workspace_id.to_string(),
945 }
946 }
947
948 pub fn process_headers(
950 &self,
951 headers: &mut std::collections::HashMap<String, String>,
952 ) -> Result<()> {
953 if !self.config.enabled {
954 return Ok(());
955 }
956
957 let workspace_key = self.workspace_manager.get_workspace_key(&self.workspace_id)?;
958
959 for (key, value) in headers.iter_mut() {
960 if self.is_sensitive_header(key) && !self.is_already_encrypted(value) {
961 *value = workspace_key.encrypt_chacha20(value, Some(key.as_bytes()))?;
962 }
963 }
964
965 Ok(())
966 }
967
968 pub fn process_json(&self, json: &mut serde_json::Value) -> Result<()> {
970 if !self.config.enabled {
971 return Ok(());
972 }
973
974 let workspace_key = self.workspace_manager.get_workspace_key(&self.workspace_id)?;
975 self.process_json_recursive(json, &workspace_key, Vec::new())?;
976
977 Ok(())
978 }
979
980 pub fn process_env_vars(
982 &self,
983 env_vars: &mut std::collections::HashMap<String, String>,
984 ) -> Result<()> {
985 if !self.config.enabled {
986 return Ok(());
987 }
988
989 let workspace_key = self.workspace_manager.get_workspace_key(&self.workspace_id)?;
990
991 for (key, value) in env_vars.iter_mut() {
992 if self.is_sensitive_env_var(key) && !self.is_already_encrypted(value) {
993 *value = workspace_key.encrypt_chacha20(value, Some(key.as_bytes()))?;
994 }
995 }
996
997 Ok(())
998 }
999
1000 fn is_sensitive_header(&self, header_name: &str) -> bool {
1002 self.config
1003 .sensitive_headers
1004 .iter()
1005 .any(|h| h.eq_ignore_ascii_case(header_name))
1006 }
1007
1008 fn is_sensitive_env_var(&self, var_name: &str) -> bool {
1010 self.config.sensitive_env_vars.iter().any(|v| v.eq_ignore_ascii_case(var_name))
1011 }
1012
1013 fn is_sensitive_field(&self, field_path: &[String]) -> bool {
1015 let default_field = String::new();
1016 let field_name = field_path.last().unwrap_or(&default_field);
1017
1018 if self.config.sensitive_fields.iter().any(|f| f.eq_ignore_ascii_case(field_name)) {
1020 return true;
1021 }
1022
1023 let path_str = field_path.join(".");
1025 for pattern in &self.config.sensitive_patterns {
1026 if let Ok(regex) = regex::Regex::new(pattern) {
1027 if regex.is_match(&path_str) || regex.is_match(field_name) {
1028 return true;
1029 }
1030 }
1031 }
1032
1033 false
1034 }
1035
1036 fn is_already_encrypted(&self, value: &str) -> bool {
1038 value.len() > 100 && general_purpose::STANDARD.decode(value).is_ok()
1040 }
1041
1042 fn process_json_recursive(
1044 &self,
1045 json: &mut serde_json::Value,
1046 workspace_key: &EncryptionKey,
1047 current_path: Vec<String>,
1048 ) -> Result<()> {
1049 match json {
1050 serde_json::Value::Object(obj) => {
1051 for (key, value) in obj.iter_mut() {
1052 let mut new_path = current_path.clone();
1053 new_path.push(key.clone());
1054
1055 if let serde_json::Value::String(ref mut s) = value {
1056 if self.is_sensitive_field(&new_path) && !self.is_already_encrypted(s) {
1057 let path_str = new_path.join(".");
1058 let path_bytes = path_str.as_bytes();
1059 *s = workspace_key.encrypt_chacha20(s, Some(path_bytes))?;
1060 }
1061 } else {
1062 self.process_json_recursive(value, workspace_key, new_path)?;
1063 }
1064 }
1065 }
1066 serde_json::Value::Array(arr) => {
1067 for (index, item) in arr.iter_mut().enumerate() {
1068 let mut new_path = current_path.clone();
1069 new_path.push(index.to_string());
1070 self.process_json_recursive(item, workspace_key, new_path)?;
1071 }
1072 }
1073 _ => {} }
1075
1076 Ok(())
1077 }
1078}
1079
1080pub mod utils {
1082 use super::*;
1083
1084 pub async fn is_encryption_enabled_for_workspace(
1086 persistence: &WorkspacePersistence,
1087 workspace_id: &str,
1088 ) -> Result<bool> {
1089 if let Ok(workspace) = persistence.load_workspace(workspace_id).await {
1091 return Ok(workspace.config.auto_encryption.enabled);
1092 }
1093 let manager = WorkspaceKeyManager::new();
1095 Ok(manager.has_workspace_key(workspace_id))
1096 }
1097
1098 pub async fn get_auto_encryption_config(
1100 persistence: &WorkspacePersistence,
1101 workspace_id: &str,
1102 ) -> Result<AutoEncryptionConfig> {
1103 let workspace = persistence.load_workspace(workspace_id).await.map_err(|e| {
1104 EncryptionError::Generic {
1105 message: format!("Failed to load workspace: {}", e),
1106 }
1107 })?;
1108 Ok(workspace.config.auto_encryption)
1109 }
1110
1111 pub fn encrypt_for_workspace(workspace_id: &str, data: &str) -> Result<String> {
1113 let manager = WorkspaceKeyManager::new();
1114 let key = manager.get_workspace_key(workspace_id)?;
1115 key.encrypt_chacha20(data, None)
1116 }
1117
1118 pub fn decrypt_for_workspace(workspace_id: &str, encrypted_data: &str) -> Result<String> {
1120 let manager = WorkspaceKeyManager::new();
1121 let key = manager.get_workspace_key(workspace_id)?;
1122 key.decrypt_chacha20(encrypted_data, None)
1123 }
1124}
1125
1126pub fn encrypt_with_key(
1139 key_id: &str,
1140 plaintext: &str,
1141 associated_data: Option<&[u8]>,
1142) -> Result<String> {
1143 let store = get_key_store()
1144 .ok_or_else(|| EncryptionError::InvalidKey("Key store not initialized".to_string()))?;
1145
1146 let key = store
1147 .get_key(key_id)
1148 .ok_or_else(|| EncryptionError::InvalidKey(format!("Key '{}' not found", key_id)))?;
1149
1150 key.encrypt(plaintext, associated_data)
1151}
1152
1153pub fn decrypt_with_key(
1166 key_id: &str,
1167 ciphertext: &str,
1168 associated_data: Option<&[u8]>,
1169) -> Result<String> {
1170 let store = get_key_store()
1171 .ok_or_else(|| EncryptionError::InvalidKey("Key store not initialized".to_string()))?;
1172
1173 let key = store
1174 .get_key(key_id)
1175 .ok_or_else(|| EncryptionError::InvalidKey(format!("Key '{}' not found", key_id)))?;
1176
1177 key.decrypt(ciphertext, associated_data)
1178}
1179
1180mod algorithms;
1181mod auto_encryption;
1182mod derivation;
1183mod errors;
1184mod key_management;
1185mod key_rotation;
1186
1187#[cfg(test)]
1188mod tests {
1189 use super::*;
1190 use once_cell::sync::Lazy;
1191 use std::sync::Mutex;
1192
1193 static MASTER_KEY_TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
1194
1195 #[test]
1196 fn test_aes_gcm_encrypt_decrypt() {
1197 let key = EncryptionKey::from_password_pbkdf2(
1198 "test_password",
1199 None,
1200 EncryptionAlgorithm::Aes256Gcm,
1201 )
1202 .unwrap();
1203
1204 let plaintext = "Hello, World!";
1205 let ciphertext = key.encrypt(plaintext, None).unwrap();
1206 let decrypted = key.decrypt(&ciphertext, None).unwrap();
1207
1208 assert_eq!(plaintext, decrypted);
1209 }
1210
1211 #[test]
1212 fn test_chacha20_encrypt_decrypt() {
1213 let key = EncryptionKey::from_password_pbkdf2(
1214 "test_password",
1215 None,
1216 EncryptionAlgorithm::ChaCha20Poly1305,
1217 )
1218 .unwrap();
1219
1220 let plaintext = "Hello, World!";
1221 let ciphertext = key.encrypt(plaintext, None).unwrap();
1222 let decrypted = key.decrypt(&ciphertext, None).unwrap();
1223
1224 assert_eq!(plaintext, decrypted);
1225 }
1226
1227 #[test]
1228 fn test_key_store() {
1229 let mut store = KeyStore::new();
1230
1231 store
1232 .derive_and_store_key(
1233 "test_key".to_string(),
1234 "test_password",
1235 EncryptionAlgorithm::Aes256Gcm,
1236 KeyDerivationMethod::Pbkdf2,
1237 )
1238 .unwrap();
1239
1240 assert!(store.get_key("test_key").is_some());
1241 assert!(store.list_keys().contains(&"test_key".to_string()));
1242
1243 store.remove_key("test_key");
1244 assert!(store.get_key("test_key").is_none());
1245 }
1246
1247 #[test]
1248 fn test_invalid_key_length() {
1249 let result = EncryptionKey::new(EncryptionAlgorithm::Aes256Gcm, vec![1, 2, 3]);
1250 assert!(matches!(result, Err(EncryptionError::InvalidKey(_))));
1251 }
1252
1253 #[test]
1254 fn test_invalid_ciphertext() {
1255 let key = EncryptionKey::from_password_pbkdf2("test", None, EncryptionAlgorithm::Aes256Gcm)
1256 .unwrap();
1257 let result = key.decrypt("invalid_base64!", None);
1258 assert!(matches!(result, Err(EncryptionError::InvalidCiphertext(_))));
1259 }
1260
1261 #[test]
1262 fn test_chacha20_encrypt_decrypt_12byte_nonce() {
1263 let key = EncryptionKey::from_password_pbkdf2(
1264 "test_password",
1265 None,
1266 EncryptionAlgorithm::ChaCha20Poly1305,
1267 )
1268 .unwrap();
1269
1270 let plaintext = "Hello, World! This is a test of ChaCha20-Poly1305 with 12-byte nonce.";
1271 let ciphertext = key.encrypt_chacha20(plaintext, None).unwrap();
1272 let decrypted = key.decrypt_chacha20(&ciphertext, None).unwrap();
1273
1274 assert_eq!(plaintext, decrypted);
1275 }
1276
1277 #[test]
1278 fn test_secure_function_template() {
1279 use crate::templating::expand_str;
1280
1281 let template = r#"{{secure "test message"}}"#;
1283 let result = expand_str(template);
1284
1285 assert_ne!(result, "test message");
1287 assert!(!result.is_empty());
1288
1289 assert!(general_purpose::STANDARD.decode(&result).is_ok());
1291 }
1292
1293 #[test]
1294 fn test_master_key_manager() {
1295 let _guard = MASTER_KEY_TEST_LOCK.lock().unwrap();
1296
1297 let manager = MasterKeyManager::new();
1298
1299 #[cfg(any(target_os = "macos", target_os = "linux"))]
1301 {
1302 if let Ok(home) = std::env::var("HOME") {
1303 let key_path = std::path::Path::new(&home).join(".mockforge").join("master_key");
1304 let _ = std::fs::remove_file(&key_path);
1305 }
1306 }
1307
1308 assert!(!manager.has_master_key());
1310
1311 manager.generate_master_key().unwrap();
1313 assert!(manager.has_master_key());
1314
1315 let master_key = manager.get_master_key().unwrap();
1317 assert_eq!(master_key.algorithm, EncryptionAlgorithm::ChaCha20Poly1305);
1318 }
1319
1320 #[test]
1321 fn test_workspace_key_manager() {
1322 let _guard = MASTER_KEY_TEST_LOCK.lock().unwrap();
1323
1324 let master_manager = MasterKeyManager::new();
1326 let needs_generation =
1327 !master_manager.has_master_key() || master_manager.get_master_key().is_err();
1328 if needs_generation && master_manager.generate_master_key().is_err() {
1329 return;
1331 }
1332
1333 let workspace_manager = WorkspaceKeyManager::new();
1334 let workspace_id = &format!("test_workspace_{}", uuid::Uuid::new_v4());
1335
1336 assert!(!workspace_manager.has_workspace_key(workspace_id));
1338
1339 let encrypted_key = workspace_manager.generate_workspace_key(workspace_id).unwrap();
1341 assert!(workspace_manager.has_workspace_key(workspace_id));
1342 assert!(!encrypted_key.is_empty());
1343
1344 let workspace_key = workspace_manager.get_workspace_key(workspace_id).unwrap();
1346 assert_eq!(workspace_key.algorithm, EncryptionAlgorithm::ChaCha20Poly1305);
1347
1348 let test_data = "sensitive workspace data";
1350 let ciphertext = workspace_key.encrypt_chacha20(test_data, None).unwrap();
1351 let decrypted = workspace_key.decrypt_chacha20(&ciphertext, None).unwrap();
1352 assert_eq!(test_data, decrypted);
1353 }
1354
1355 #[test]
1356 fn test_backup_string_formatting() {
1357 let manager = WorkspaceKeyManager::new();
1358
1359 let test_key = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrst";
1361 let backup = manager.format_backup_string(test_key);
1362
1363 assert!(backup.contains('-'));
1365
1366 let parsed = manager.parse_backup_string(&backup).unwrap();
1368 assert_eq!(parsed, test_key);
1369 }
1370
1371 #[test]
1372 fn test_auto_encryption_processor() {
1373 let _guard = MASTER_KEY_TEST_LOCK.lock().unwrap();
1374
1375 let master_manager = MasterKeyManager::new();
1377 let needs_key =
1378 !master_manager.has_master_key() || master_manager.get_master_key().is_err();
1379 if needs_key && master_manager.generate_master_key().is_err() {
1380 eprintln!("Skipping test_auto_encryption_processor: Failed to generate master key");
1381 return;
1382 }
1383
1384 let workspace_manager = WorkspaceKeyManager::new();
1385 let workspace_id = &format!("test_auto_encrypt_workspace_{}", uuid::Uuid::new_v4());
1386
1387 workspace_manager.generate_workspace_key(workspace_id).unwrap();
1388
1389 let config = AutoEncryptionConfig {
1390 enabled: true,
1391 ..AutoEncryptionConfig::default()
1392 };
1393
1394 let processor = AutoEncryptionProcessor::new(workspace_id, config);
1395
1396 let mut headers = std::collections::HashMap::new();
1398 headers.insert("Authorization".to_string(), "Bearer my-secret-token".to_string());
1399 headers.insert("Content-Type".to_string(), "application/json".to_string());
1400
1401 processor.process_headers(&mut headers).unwrap();
1402
1403 assert_ne!(headers["Authorization"], "Bearer my-secret-token");
1405 assert!(general_purpose::STANDARD.decode(&headers["Authorization"]).is_ok());
1406
1407 assert_eq!(headers["Content-Type"], "application/json");
1409 }
1410
1411 #[test]
1412 #[ignore] fn test_json_field_encryption() {
1414 let _guard = MASTER_KEY_TEST_LOCK.lock().unwrap();
1415
1416 let master_manager = MasterKeyManager::new();
1418 if !master_manager.has_master_key() || master_manager.get_master_key().is_err() {
1419 master_manager.generate_master_key().unwrap();
1420 }
1421
1422 let workspace_manager = WorkspaceKeyManager::new();
1423 let workspace_id = &format!("test_json_workspace_{}", uuid::Uuid::new_v4());
1424
1425 workspace_manager.generate_workspace_key(workspace_id).unwrap();
1426
1427 let another_manager = WorkspaceKeyManager::new();
1429 assert!(another_manager.has_workspace_key(workspace_id));
1430
1431 let config = AutoEncryptionConfig {
1432 enabled: true,
1433 ..AutoEncryptionConfig::default()
1434 };
1435
1436 let processor = AutoEncryptionProcessor::new(workspace_id, config);
1437
1438 let mut json = serde_json::json!({
1440 "username": "testuser",
1441 "password": "secret123",
1442 "email": "test@example.com",
1443 "nested": {
1444 "token": "my-api-token",
1445 "normal_field": "normal_value"
1446 }
1447 });
1448
1449 processor.process_json(&mut json).unwrap();
1450
1451 assert_ne!(json["password"], "secret123");
1453 assert_ne!(json["nested"]["token"], "my-api-token");
1454
1455 assert_eq!(json["username"], "testuser");
1457 assert_eq!(json["email"], "test@example.com");
1458 assert_eq!(json["nested"]["normal_field"], "normal_value");
1459 }
1460
1461 #[test]
1462 fn test_env_var_encryption() {
1463 let _guard = MASTER_KEY_TEST_LOCK.lock().unwrap();
1464
1465 let master_manager = MasterKeyManager::new();
1467 if !master_manager.has_master_key() || master_manager.get_master_key().is_err() {
1468 master_manager.generate_master_key().unwrap();
1469 }
1470
1471 let workspace_manager = WorkspaceKeyManager::new();
1472 let workspace_id = &format!("test_env_workspace_{}", uuid::Uuid::new_v4());
1473
1474 workspace_manager.generate_workspace_key(workspace_id).unwrap();
1475
1476 let config = AutoEncryptionConfig {
1477 enabled: true,
1478 ..AutoEncryptionConfig::default()
1479 };
1480
1481 let processor = AutoEncryptionProcessor::new(workspace_id, config);
1482
1483 let mut env_vars = std::collections::HashMap::new();
1485 env_vars.insert("API_KEY".to_string(), "sk-1234567890abcdef".to_string());
1486 env_vars
1487 .insert("DATABASE_URL".to_string(), "postgres://user:pass@host:5432/db".to_string());
1488 env_vars.insert("NORMAL_VAR".to_string(), "normal_value".to_string());
1489
1490 processor.process_env_vars(&mut env_vars).unwrap();
1491
1492 assert_ne!(env_vars["API_KEY"], "sk-1234567890abcdef");
1494 assert_ne!(env_vars["DATABASE_URL"], "postgres://user:pass@host:5432/db");
1495
1496 assert_eq!(env_vars["NORMAL_VAR"], "normal_value");
1498 }
1499
1500 #[test]
1501 fn test_encryption_utils() {
1502 let _guard = MASTER_KEY_TEST_LOCK.lock().unwrap();
1503
1504 let master_manager = MasterKeyManager::new();
1506 let needs_key =
1507 !master_manager.has_master_key() || master_manager.get_master_key().is_err();
1508 if needs_key && master_manager.generate_master_key().is_err() {
1509 eprintln!("Skipping test_encryption_utils: Failed to generate master key");
1510 return;
1511 }
1512
1513 let workspace_manager = WorkspaceKeyManager::new();
1514 let workspace_id = &format!("test_utils_workspace_{}", uuid::Uuid::new_v4());
1515 workspace_manager.generate_workspace_key(workspace_id).unwrap();
1516
1517 assert!(workspace_manager.has_workspace_key(workspace_id));
1519
1520 let another_manager = WorkspaceKeyManager::new();
1522 assert!(another_manager.has_workspace_key(workspace_id));
1523
1524 let test_data = "test data for utils";
1525 let encrypted = utils::encrypt_for_workspace(workspace_id, test_data).unwrap();
1526 let decrypted = utils::decrypt_for_workspace(workspace_id, &encrypted).unwrap();
1527
1528 assert_eq!(test_data, decrypted);
1529 assert_ne!(encrypted, test_data);
1530 }
1531}