1#![allow(unused_assignments)]
2use aes_gcm::{
12 aead::{Aead, KeyInit, OsRng},
13 Aes256Gcm, Nonce,
14};
15use lazy_static::lazy_static;
16use pbkdf2::pbkdf2_hmac;
17use serde::{Deserialize, Serialize};
18use sha2::Sha512;
19use std::collections::HashMap;
20use std::fs;
21use std::io::IsTerminal;
22use std::path::Path;
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::sync::Mutex;
25use std::time::{Duration, SystemTime};
26use zeroize::{ZeroizeOnDrop, Zeroizing};
27
28const KEY_SIZE: usize = 32;
30
31const DEFAULT_CACHE_TIMEOUT: Duration = Duration::from_secs(300);
33
34struct CachedPassphrase {
37 passphrase: Zeroizing<String>,
38 expires_at: SystemTime,
39}
40
41lazy_static! {
42 static ref PASSPHRASE_CACHE: Mutex<HashMap<String, CachedPassphrase>> = Mutex::new(HashMap::new());
44
45 static ref FAILED_ATTEMPTS: Mutex<HashMap<String, FailedAttemptTracker>> = Mutex::new(HashMap::new());
47}
48
49struct FailedAttemptTracker {
51 count: u32,
52 last_attempt: SystemTime,
53 lockout_until: Option<SystemTime>,
54}
55
56const NONCE_SIZE: usize = 12;
58
59const PBKDF2_ITERATIONS: u32 = 600_000;
65
66const SALT_SIZE: usize = 16;
69
70const ENCRYPTION_VERSION: u8 = 1;
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct EncryptionConfig {
76 pub enabled: bool,
78 pub key_file: String,
80 pub fips_mode: bool,
82 #[serde(default = "default_cache_timeout")]
84 pub cache_timeout_secs: u64,
85}
86
87fn default_cache_timeout() -> u64 {
88 300 }
90
91impl Default for EncryptionConfig {
92 fn default() -> Self {
93 EncryptionConfig {
94 enabled: false,
95 key_file: "~/.lit/encryption.key".to_string(),
96 fips_mode: true,
97 cache_timeout_secs: default_cache_timeout(),
98 }
99 }
100}
101
102impl EncryptionConfig {
103 pub fn load(repo_path: &Path) -> Result<Self, String> {
105 let config_path = repo_path.join(".lit").join("encryption.toml");
106
107 if !config_path.exists() {
108 return Ok(Self::default());
109 }
110
111 let content = fs::read_to_string(&config_path)
112 .map_err(|e| format!("Failed to read encryption config: {}", e))?;
113
114 toml::from_str(&content).map_err(|e| format!("Failed to parse encryption config: {}", e))
115 }
116
117 pub fn save(&self, repo_path: &Path) -> Result<(), String> {
119 let config_path = repo_path.join(".lit").join("encryption.toml");
120
121 let content = toml::to_string_pretty(self)
122 .map_err(|e| format!("Failed to serialize encryption config: {}", e))?;
123
124 fs::write(&config_path, content)
125 .map_err(|e| format!("Failed to write encryption config: {}", e))
126 }
127}
128
129fn check_rate_limit(repo_path: &str) -> Result<(), String> {
132 let mut attempts = FAILED_ATTEMPTS
133 .lock()
134 .map_err(|_| "Internal error: rate-limit lock poisoned".to_string())?;
135 let tracker = attempts
136 .entry(repo_path.to_string())
137 .or_insert_with(|| FailedAttemptTracker {
138 count: 0,
139 last_attempt: SystemTime::now(),
140 lockout_until: None,
141 });
142
143 if let Some(lockout) = tracker.lockout_until {
145 if SystemTime::now() < lockout {
146 let remaining = lockout
147 .duration_since(SystemTime::now())
148 .unwrap_or(Duration::from_secs(0));
149 return Err(format!(
150 "Too many failed attempts. Please wait {} seconds before trying again.",
151 remaining.as_secs()
152 ));
153 }
154 tracker.lockout_until = None;
156 tracker.count = 0;
157 }
158
159 if tracker.count > 0 {
161 let delay = Duration::from_secs(2u64.pow(tracker.count.min(5)));
162 if let Ok(elapsed) = tracker.last_attempt.elapsed() {
163 if elapsed < delay {
164 let remaining = delay.as_secs().saturating_sub(elapsed.as_secs());
165 return Err(format!(
166 "Please wait {} seconds between passphrase attempts.",
167 remaining
168 ));
169 }
170 }
171 }
172
173 Ok(())
174}
175
176fn record_failed_attempt(repo_path: &str) {
178 let Ok(mut attempts) = FAILED_ATTEMPTS.lock() else {
179 return;
180 };
181 let tracker = attempts
182 .entry(repo_path.to_string())
183 .or_insert_with(|| FailedAttemptTracker {
184 count: 0,
185 last_attempt: SystemTime::now(),
186 lockout_until: None,
187 });
188
189 tracker.count += 1;
190 tracker.last_attempt = SystemTime::now();
191
192 if tracker.count >= 5 {
194 tracker.lockout_until = Some(SystemTime::now() + Duration::from_secs(300));
195 eprintln!("Warning: Account locked due to multiple failed attempts. Locked for 5 minutes.");
196 }
197}
198
199fn clear_failed_attempts(repo_path: &str) {
201 if let Ok(mut attempts) = FAILED_ATTEMPTS.lock() {
202 attempts.remove(repo_path);
203 }
204}
205
206#[derive(ZeroizeOnDrop)]
208#[allow(unused_assignments)]
209pub struct EncryptionKey {
210 key_bytes: [u8; KEY_SIZE],
211 #[zeroize(skip)]
213 salt: [u8; SALT_SIZE],
214}
215
216impl EncryptionKey {
217 pub fn from_passphrase(passphrase: &str, salt: &[u8]) -> Result<Self, String> {
219 #[cfg(not(test))]
221 validate_passphrase_strength(passphrase)?;
222 #[cfg(test)]
223 if !passphrase.starts_with("test-") {
224 validate_passphrase_strength(passphrase)?;
225 }
226
227 if salt.len() != SALT_SIZE {
228 return Err(format!(
229 "Invalid salt size: expected {}, got {}",
230 SALT_SIZE,
231 salt.len()
232 ));
233 }
234
235 let mut key_bytes = [0u8; KEY_SIZE];
236 pbkdf2_hmac::<Sha512>(
237 passphrase.as_bytes(),
238 salt,
239 PBKDF2_ITERATIONS,
240 &mut key_bytes,
241 );
242
243 let mut salt_array = [0u8; SALT_SIZE];
244 salt_array.copy_from_slice(salt);
245
246 Ok(EncryptionKey {
247 key_bytes,
248 salt: salt_array,
249 })
250 }
251
252 pub fn generate_salt() -> [u8; SALT_SIZE] {
254 use aes_gcm::aead::rand_core::RngCore;
255 let mut salt = [0u8; SALT_SIZE];
256 OsRng.fill_bytes(&mut salt);
257 salt
258 }
259
260 pub fn load(key_file: &Path, passphrase: &str) -> Result<Self, String> {
264 let key_file_str = key_file.to_string_lossy().to_string();
266 #[cfg(not(test))]
267 check_rate_limit(&key_file_str)?;
268 #[cfg(test)]
269 if !passphrase.starts_with("test-") {
270 check_rate_limit(&key_file_str)?;
271 }
272
273 if !key_file.exists() {
274 return Err(
275 "Encryption key file not found. Initialize repository with encryption first."
276 .to_string(),
277 );
278 }
279
280 let encrypted_data =
281 fs::read(key_file).map_err(|e| format!("Failed to read key file: {}", e))?;
282
283 if encrypted_data.len() < SALT_SIZE + 1 {
284 return Err("Invalid key file format (too short)".to_string());
285 }
286
287 let salt = &encrypted_data[0..SALT_SIZE];
289 let version = encrypted_data[SALT_SIZE];
290
291 if version != ENCRYPTION_VERSION {
292 return Err(format!("Unsupported key file version: {}", version));
293 }
294
295 if encrypted_data.len() == SALT_SIZE + 1 {
297 let key = Self::from_passphrase(passphrase, salt)?;
299 clear_failed_attempts(&key_file_str);
301 return Ok(key);
302 }
303
304 if encrypted_data.len() < SALT_SIZE + 1 + 32 {
305 return Err("Invalid key file format (unexpected size)".to_string());
306 }
307
308 let stored_verification = &encrypted_data[SALT_SIZE + 1..SALT_SIZE + 1 + 32];
309
310 let key = Self::from_passphrase(passphrase, salt)?;
312
313 use sha2::{Digest, Sha256};
315 let mut hasher = Sha256::new();
316 hasher.update(b"lit-passphrase-verification-v1");
317 hasher.update(&key.key_bytes);
318 let verification_hash = hasher.finalize();
319
320 use subtle::ConstantTimeEq;
322 if verification_hash.ct_eq(stored_verification).unwrap_u8() != 1 {
323 #[cfg(not(test))]
325 record_failed_attempt(&key_file_str);
326 #[cfg(test)]
327 if !passphrase.starts_with("test-") {
328 record_failed_attempt(&key_file_str);
329 }
330 std::thread::sleep(std::time::Duration::from_millis(100));
332 return Err("Invalid passphrase".to_string());
333 }
334
335 clear_failed_attempts(&key_file_str);
337 Ok(key)
338 }
339
340 pub fn save(&self, key_file_str: &str, _passphrase: &str) -> Result<(), String> {
344 let expanded = shellexpand::tilde(key_file_str);
345 let key_file = Path::new(expanded.as_ref());
346
347 use sha2::{Digest, Sha256};
349 let mut hasher = Sha256::new();
350 hasher.update(b"lit-passphrase-verification-v1");
351 hasher.update(self.key_bytes);
352 let verification_hash = hasher.finalize();
353
354 if let Some(parent) = key_file.parent() {
356 fs::create_dir_all(parent)
357 .map_err(|e| format!("Failed to create key directory: {}", e))?;
358 }
359
360 let mut data = Vec::new();
362 data.extend_from_slice(&self.salt);
363 data.push(ENCRYPTION_VERSION);
364 data.extend_from_slice(&verification_hash);
365
366 let temp_file = key_file.with_extension("tmp");
368 fs::write(&temp_file, &data)
369 .map_err(|e| format!("Failed to write temp key file: {}", e))?;
370 fs::rename(&temp_file, key_file)
371 .map_err(|e| format!("Failed to rename key file: {}", e))?;
372
373 Ok(())
374 }
375
376 fn as_bytes(&self) -> &[u8; KEY_SIZE] {
378 &self.key_bytes
379 }
380}
381
382const MAX_ENCRYPTIONS_PER_KEY: u64 = 1u64 << 32;
385
386pub struct EncryptionEngine {
389 cipher: Aes256Gcm,
390 nonce_counter: AtomicU64,
392}
393
394impl EncryptionEngine {
395 pub fn new(key: &EncryptionKey) -> Result<Self, String> {
397 let cipher = Aes256Gcm::new_from_slice(key.as_bytes())
398 .map_err(|e| format!("Failed to create cipher: {}", e))?;
399
400 Ok(EncryptionEngine {
401 cipher,
402 nonce_counter: AtomicU64::new(0),
403 })
404 }
405
406 #[allow(deprecated)]
411 pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, String> {
412 let count = self.nonce_counter.fetch_add(1, Ordering::SeqCst);
414 if count >= MAX_ENCRYPTIONS_PER_KEY {
415 return Err(format!(
416 "Encryption limit exceeded ({} operations). Key rotation required for security.",
417 MAX_ENCRYPTIONS_PER_KEY
418 ));
419 }
420
421 use aes_gcm::aead::rand_core::RngCore;
424 let mut nonce_bytes = [0u8; NONCE_SIZE];
425 nonce_bytes[..8].copy_from_slice(&count.to_be_bytes());
426 OsRng.fill_bytes(&mut nonce_bytes[8..]);
427 let nonce = Nonce::from_slice(&nonce_bytes);
428
429 let ciphertext = self
431 .cipher
432 .encrypt(nonce, plaintext)
433 .map_err(|e| format!("Encryption failed: {}", e))?;
434
435 let mut output = Vec::with_capacity(1 + NONCE_SIZE + ciphertext.len());
437 output.push(ENCRYPTION_VERSION);
438 output.extend_from_slice(&nonce_bytes);
439 output.extend_from_slice(&ciphertext);
440
441 Ok(output)
442 }
443
444 #[allow(deprecated)]
446 pub fn decrypt(&self, encrypted: &[u8]) -> Result<Vec<u8>, String> {
447 if encrypted.len() < 1 + NONCE_SIZE {
448 return Err("Invalid encrypted data: too short".to_string());
449 }
450
451 let version = encrypted[0];
453 if version != ENCRYPTION_VERSION {
454 return Err(format!("Unsupported encryption version: {}", version));
455 }
456
457 let nonce_bytes = &encrypted[1..1 + NONCE_SIZE];
459 let nonce = Nonce::from_slice(nonce_bytes);
460
461 let ciphertext = &encrypted[1 + NONCE_SIZE..];
463
464 let plaintext = self
466 .cipher
467 .decrypt(nonce, ciphertext)
468 .map_err(|e| format!("Decryption failed (possible tampering): {}", e))?;
469
470 Ok(plaintext)
471 }
472}
473
474impl CachedPassphrase {
476 fn is_valid(&self) -> bool {
478 SystemTime::now() < self.expires_at
479 }
480}
481
482pub fn cache_passphrase(repo_path: &str, passphrase: String, timeout: Option<Duration>) {
485 let timeout = timeout.unwrap_or(DEFAULT_CACHE_TIMEOUT);
486 let expires_at = SystemTime::now() + timeout;
487
488 if let Ok(mut cache) = PASSPHRASE_CACHE.lock() {
489 cache.insert(
490 repo_path.to_string(),
491 CachedPassphrase {
492 passphrase: Zeroizing::new(passphrase),
493 expires_at,
494 },
495 );
496 }
497}
498
499pub fn get_cached_passphrase(repo_path: &str) -> Option<Zeroizing<String>> {
502 if let Ok(mut cache) = PASSPHRASE_CACHE.lock() {
503 if let Some(entry) = cache.get(repo_path) {
504 if entry.is_valid() {
505 return Some(entry.passphrase.clone());
506 } else {
507 cache.remove(repo_path);
509 }
510 }
511 }
512 None
513}
514
515pub fn clear_passphrase_cache() {
517 if let Ok(mut cache) = PASSPHRASE_CACHE.lock() {
518 cache.clear();
519 }
520}
521
522pub fn clear_cached_passphrase(repo_path: &str) {
524 if let Ok(mut cache) = PASSPHRASE_CACHE.lock() {
525 cache.remove(repo_path);
526 }
527}
528
529fn get_passphrase_non_interactive(
535 repo_path: &str,
536 config: &EncryptionConfig,
537) -> Option<Zeroizing<String>> {
538 if let Ok(pass) = std::env::var("LIT_PASSPHRASE") {
540 if !pass.is_empty() {
541 return Some(Zeroizing::new(pass));
542 }
543 }
544
545 if let Ok(path) = std::env::var("LIT_PASSPHRASE_FILE") {
547 if let Ok(pass) = std::fs::read_to_string(&path) {
548 let pass = pass
549 .trim_end_matches('\n')
550 .trim_end_matches('\r')
551 .to_string();
552 if !pass.is_empty() {
553 return Some(Zeroizing::new(pass));
554 }
555 }
556 }
557
558 if config.cache_timeout_secs > 0 {
560 if let Some(cached) = get_cached_passphrase(repo_path) {
561 return Some(cached);
562 }
563 }
564
565 None
566}
567
568pub fn prompt_for_passphrase(
574 repo_path: &str,
575 config: &EncryptionConfig,
576 prompt_text: &str,
577) -> Result<Zeroizing<String>, String> {
578 if let Some(pass) = get_passphrase_non_interactive(repo_path, config) {
580 return Ok(pass);
581 }
582
583 if !std::io::stdin().is_terminal() {
586 return Err(
587 "no passphrase available and no interactive terminal; set LIT_PASSPHRASE or \
588 LIT_PASSPHRASE_FILE"
589 .to_string(),
590 );
591 }
592
593 rpassword::prompt_password(prompt_text)
595 .map(Zeroizing::new)
596 .map_err(|e| format!("Failed to read passphrase: {}", e))
597}
598
599const MIN_PASSPHRASE_LENGTH: usize = 16;
601
602fn validate_passphrase_strength(passphrase: &str) -> Result<(), String> {
608 #[cfg(test)]
610 if passphrase.starts_with("test-") {
611 return Ok(());
612 }
613
614 if passphrase.len() < MIN_PASSPHRASE_LENGTH {
615 return Err(format!(
616 "Passphrase must be at least {} characters (recommended: 20+)",
617 MIN_PASSPHRASE_LENGTH
618 ));
619 }
620
621 let has_upper = passphrase.chars().any(|c| c.is_uppercase());
623 let has_lower = passphrase.chars().any(|c| c.is_lowercase());
624 let has_digit = passphrase.chars().any(|c| c.is_numeric());
625 let has_special = passphrase.chars().any(|c| !c.is_alphanumeric());
626
627 let complexity_count = [has_upper, has_lower, has_digit, has_special]
628 .iter()
629 .filter(|&&x| x)
630 .count();
631
632 if complexity_count < 3 {
633 return Err(
634 "Passphrase must include at least 3 of: uppercase, lowercase, digits, special characters"
635 .to_string(),
636 );
637 }
638
639 Ok(())
640}
641
642pub fn prompt_for_passphrase_confirmation(prompt_text: &str) -> Result<Zeroizing<String>, String> {
646 if let Ok(pass) = std::env::var("LIT_PASSPHRASE") {
648 if !pass.is_empty() {
649 validate_passphrase_strength(&pass)?;
650 return Ok(Zeroizing::new(pass));
651 }
652 }
653
654 if let Ok(path) = std::env::var("LIT_PASSPHRASE_FILE") {
656 if let Ok(pass) = std::fs::read_to_string(&path) {
657 let pass = pass
658 .trim_end_matches('\n')
659 .trim_end_matches('\r')
660 .to_string();
661 if !pass.is_empty() {
662 validate_passphrase_strength(&pass)?;
663 return Ok(Zeroizing::new(pass));
664 }
665 }
666 }
667
668 if !std::io::stdin().is_terminal() {
670 return Err(
671 "no passphrase available and no interactive terminal; set LIT_PASSPHRASE or \
672 LIT_PASSPHRASE_FILE"
673 .to_string(),
674 );
675 }
676
677 let pass1 = rpassword::prompt_password(prompt_text)
679 .map_err(|e| format!("Failed to read passphrase: {}", e))?;
680
681 let pass2 = rpassword::prompt_password("Confirm passphrase: ")
682 .map_err(|e| format!("Failed to read passphrase confirmation: {}", e))?;
683
684 if pass1 != pass2 {
685 return Err("Passphrases do not match".to_string());
686 }
687
688 validate_passphrase_strength(&pass1)?;
689
690 Ok(Zeroizing::new(pass1))
691}
692
693pub struct EncryptionManager {
695 config: EncryptionConfig,
696 engine: Option<EncryptionEngine>,
697 repo_path: Option<String>,
698}
699
700impl EncryptionManager {
701 pub fn new(config: EncryptionConfig) -> Self {
703 EncryptionManager {
704 config,
705 engine: None,
706 repo_path: None,
707 }
708 }
709
710 pub fn initialize(&mut self, passphrase: &str) -> Result<(), String> {
712 if !self.config.enabled {
713 return Ok(());
714 }
715
716 let expanded = shellexpand::tilde(&self.config.key_file);
717 let key_file = Path::new(expanded.as_ref());
718
719 let key = if key_file.exists() {
721 EncryptionKey::load(key_file, passphrase)?
722 } else {
723 let key = EncryptionKey::from_passphrase(passphrase, &EncryptionKey::generate_salt())?;
724 key.save(&self.config.key_file, passphrase)?;
725 key
726 };
727
728 self.engine = Some(EncryptionEngine::new(&key)?);
730
731 Ok(())
732 }
733
734 pub fn initialize_with_cache(
736 &mut self,
737 repo_path: &str,
738 passphrase: Option<&str>,
739 ) -> Result<(), String> {
740 if !self.config.enabled {
741 return Ok(());
742 }
743
744 self.repo_path = Some(repo_path.to_string());
745
746 let actual_passphrase: Zeroizing<String> = if let Some(pass) = passphrase {
748 Zeroizing::new(pass.to_string())
749 } else if let Some(cached) = get_cached_passphrase(repo_path) {
750 cached
751 } else {
752 return Err("No passphrase provided and no valid cached passphrase found".to_string());
753 };
754
755 self.initialize(&actual_passphrase)?;
757
758 if self.config.cache_timeout_secs > 0 {
760 let timeout = Duration::from_secs(self.config.cache_timeout_secs);
761 cache_passphrase(repo_path, (*actual_passphrase).clone(), Some(timeout));
762 }
763
764 Ok(())
765 }
766
767 pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, String> {
769 if !self.config.enabled {
770 return Ok(plaintext.to_vec());
771 }
772
773 match &self.engine {
774 Some(engine) => engine.encrypt(plaintext),
775 None => Err(
776 "Encryption not initialized. Call initialize() with passphrase first.".to_string(),
777 ),
778 }
779 }
780
781 pub fn decrypt(&self, encrypted: &[u8]) -> Result<Vec<u8>, String> {
783 if !self.config.enabled {
784 return Ok(encrypted.to_vec());
785 }
786
787 match &self.engine {
788 Some(engine) => engine.decrypt(encrypted),
789 None => Err(
790 "Encryption not initialized. Call initialize() with passphrase first.".to_string(),
791 ),
792 }
793 }
794
795 pub fn is_enabled(&self) -> bool {
797 self.config.enabled
798 }
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804
805 static CACHE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
813
814 fn cache_test_guard() -> std::sync::MutexGuard<'static, ()> {
815 CACHE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
816 }
817
818 #[test]
819 fn test_key_derivation() {
820 let passphrase = "test-passphrase-12345";
821 let salt = EncryptionKey::generate_salt();
822
823 let key1 = EncryptionKey::from_passphrase(passphrase, &salt).unwrap();
824 let key2 = EncryptionKey::from_passphrase(passphrase, &salt).unwrap();
825
826 assert_eq!(key1.as_bytes(), key2.as_bytes());
828 }
829
830 #[test]
831 fn test_encryption_decryption() {
832 let passphrase = "test-secure-passphrase";
833 let salt = EncryptionKey::generate_salt();
834 let key = EncryptionKey::from_passphrase(passphrase, &salt).unwrap();
835
836 let engine = EncryptionEngine::new(&key).unwrap();
837
838 let plaintext = b"Hello, this is secret data!";
839
840 let encrypted = engine.encrypt(plaintext).unwrap();
842
843 assert_ne!(encrypted.as_slice(), plaintext);
845
846 let decrypted = engine.decrypt(&encrypted).unwrap();
848
849 assert_eq!(decrypted.as_slice(), plaintext);
851 }
852
853 #[test]
854 fn test_encryption_nonce_randomness() {
855 let passphrase = "test-passphrase";
856 let salt = EncryptionKey::generate_salt();
857 let key = EncryptionKey::from_passphrase(passphrase, &salt).unwrap();
858
859 let engine = EncryptionEngine::new(&key).unwrap();
860
861 let plaintext = b"Same data";
862
863 let encrypted1 = engine.encrypt(plaintext).unwrap();
865 let encrypted2 = engine.encrypt(plaintext).unwrap();
866
867 assert_ne!(encrypted1, encrypted2);
869
870 assert_eq!(engine.decrypt(&encrypted1).unwrap(), plaintext);
872 assert_eq!(engine.decrypt(&encrypted2).unwrap(), plaintext);
873 }
874
875 #[test]
876 fn test_tampering_detection() {
877 let passphrase = "test-passphrase";
878 let salt = EncryptionKey::generate_salt();
879 let key = EncryptionKey::from_passphrase(passphrase, &salt).unwrap();
880
881 let engine = EncryptionEngine::new(&key).unwrap();
882
883 let plaintext = b"Secret data";
884 let mut encrypted = engine.encrypt(plaintext).unwrap();
885
886 let len = encrypted.len();
888 encrypted[len - 1] ^= 0x01;
889
890 assert!(engine.decrypt(&encrypted).is_err());
892 }
893
894 #[test]
895 fn test_encryption_manager_disabled() {
896 let config = EncryptionConfig {
897 enabled: false,
898 ..Default::default()
899 };
900
901 let manager = EncryptionManager::new(config);
902
903 let data = b"Some data";
904
905 assert_eq!(manager.encrypt(data).unwrap(), data);
907 assert_eq!(manager.decrypt(data).unwrap(), data);
908 }
909
910 #[test]
911 fn test_passphrase_caching() {
912 let _guard = cache_test_guard();
913 let repo_path = "/tmp/test-repo";
914 let passphrase = "cache-test-passphrase".to_string();
915
916 clear_passphrase_cache();
918
919 assert!(get_cached_passphrase(repo_path).is_none());
921
922 cache_passphrase(repo_path, passphrase.clone(), Some(Duration::from_secs(5)));
924
925 assert_eq!(&*get_cached_passphrase(repo_path).unwrap(), &passphrase);
927
928 clear_cached_passphrase(repo_path);
930 assert!(get_cached_passphrase(repo_path).is_none());
931 }
932
933 #[test]
934 fn test_passphrase_cache_expiration() {
935 let _guard = cache_test_guard();
936 let repo_path = "/tmp/test-repo-expire";
937 let passphrase = "expire-test".to_string();
938
939 clear_passphrase_cache();
940
941 cache_passphrase(
948 repo_path,
949 passphrase.clone(),
950 Some(Duration::from_millis(200)),
951 );
952
953 std::thread::sleep(Duration::from_millis(600));
954
955 assert!(get_cached_passphrase(repo_path).is_none());
957 }
958
959 #[test]
960 fn test_passphrase_cache_multiple_repos() {
961 let _guard = cache_test_guard();
962 let repo1 = "/tmp/multi-cache-repo1";
963 let repo2 = "/tmp/multi-cache-repo2";
964 let pass1 = "password1".to_string();
965 let pass2 = "password2".to_string();
966
967 clear_passphrase_cache();
968
969 cache_passphrase(repo1, pass1.clone(), Some(Duration::from_secs(60)));
971 cache_passphrase(repo2, pass2.clone(), Some(Duration::from_secs(60)));
972
973 assert_eq!(&*get_cached_passphrase(repo1).unwrap(), &pass1);
975 assert_eq!(&*get_cached_passphrase(repo2).unwrap(), &pass2);
976 }
977
978 #[test]
979 #[ignore] fn test_encryption_manager_with_cache() {
981 use std::env;
982
983 let _guard = cache_test_guard();
984
985 let key_path = shellexpand::tilde("~/.lit/encryption.key");
987 fs::remove_file(key_path.as_ref()).ok();
988
989 let temp_dir = env::temp_dir();
990 let repo_path = temp_dir.join("test-cache-manager");
991 let repo_str = repo_path.to_str().unwrap();
992
993 clear_passphrase_cache();
994
995 let config = EncryptionConfig {
996 enabled: true,
997 cache_timeout_secs: 300, ..Default::default()
999 };
1000
1001 let mut manager = EncryptionManager::new(config);
1002 let passphrase = "test-cache-manager-pass";
1003
1004 manager
1006 .initialize_with_cache(repo_str, Some(passphrase))
1007 .unwrap();
1008
1009 assert_eq!(&*get_cached_passphrase(repo_str).unwrap(), passphrase);
1011
1012 let mut manager2 = EncryptionManager::new(manager.config.clone());
1014 manager2.initialize_with_cache(repo_str, None).unwrap();
1015
1016 clear_passphrase_cache();
1018 }
1019
1020 #[test]
1021 #[ignore] fn test_rate_limiting() {
1023 let key_path = shellexpand::tilde("~/.lit/encryption.key");
1025 fs::remove_file(key_path.as_ref()).ok();
1026
1027 let passphrase = "correct-passphrase-1234567890";
1029 let salt = EncryptionKey::generate_salt();
1030 let key = EncryptionKey::from_passphrase(passphrase, &salt).unwrap();
1031 key.save("~/.lit/encryption.key", passphrase).unwrap();
1032
1033 let result1 = EncryptionKey::load(
1035 Path::new(shellexpand::tilde("~/.lit/encryption.key").as_ref()),
1036 "wrong-password-111111111111",
1037 );
1038 assert!(result1.is_err());
1039
1040 let start = std::time::Instant::now();
1042 let result2 = EncryptionKey::load(
1043 Path::new(shellexpand::tilde("~/.lit/encryption.key").as_ref()),
1044 "wrong-password-222222222222",
1045 );
1046 assert!(result2.is_err());
1047 let elapsed2 = start.elapsed().as_secs();
1048 assert!(
1049 elapsed2 >= 2,
1050 "Expected at least 2 second rate limit delay, got {} seconds",
1051 elapsed2
1052 );
1053
1054 std::thread::sleep(std::time::Duration::from_secs(3));
1056
1057 let start = std::time::Instant::now();
1059 let result3 = EncryptionKey::load(
1060 Path::new(shellexpand::tilde("~/.lit/encryption.key").as_ref()),
1061 "wrong-password-333333333333",
1062 );
1063 assert!(result3.is_err());
1064 let elapsed3 = start.elapsed().as_secs();
1065 assert!(
1066 elapsed3 >= 4,
1067 "Expected at least 4 second rate limit delay, got {} seconds",
1068 elapsed3
1069 );
1070
1071 let result_correct = EncryptionKey::load(
1073 Path::new(shellexpand::tilde("~/.lit/encryption.key").as_ref()),
1074 passphrase,
1075 );
1076 assert!(result_correct.is_ok());
1077
1078 let start = std::time::Instant::now();
1080 let result_after_reset = EncryptionKey::load(
1081 Path::new(shellexpand::tilde("~/.lit/encryption.key").as_ref()),
1082 "wrong-again-444444444444",
1083 );
1084 assert!(result_after_reset.is_err());
1085 let elapsed_after_reset = start.elapsed().as_secs();
1086 assert!(
1088 elapsed_after_reset < 2,
1089 "Expected <2 seconds after reset, got {} seconds",
1090 elapsed_after_reset
1091 );
1092
1093 fs::remove_file(shellexpand::tilde("~/.lit/encryption.key").as_ref()).ok();
1095 }
1096}