1use std::fmt;
29use std::path::Path;
30
31use aes_gcm::{
32 Aes256Gcm, Nonce,
33 aead::{Aead, KeyInit, OsRng, rand_core::RngCore},
34};
35use anyhow::{Context, Result, bail};
36use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
37use pbkdf2::pbkdf2_hmac_array;
38use serde::{Deserialize, Serialize};
39use sha2::Sha256;
40
41const SALT_SIZE: usize = 16;
43const NONCE_SIZE: usize = 12;
45const PBKDF2_ITERATIONS: u32 = 100_000;
47const KEY_SIZE: usize = 32;
49
50#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52pub struct EncryptionConfig {
53 #[serde(default)]
55 pub enabled: bool,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub passphrase: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub env_var: Option<String>,
62}
63
64impl EncryptionConfig {
65 pub fn new(passphrase: String) -> Self {
67 Self {
68 enabled: true,
69 passphrase: Some(passphrase),
70 env_var: None,
71 }
72 }
73
74 pub fn from_env(env_var: String) -> Self {
76 Self {
77 enabled: true,
78 passphrase: None,
79 env_var: Some(env_var),
80 }
81 }
82
83 pub fn get_passphrase(&self) -> Result<Option<String>> {
85 if let Some(passphrase) = &self.passphrase {
86 return Ok(Some(passphrase.clone()));
87 }
88
89 if let Some(ref env_var) = self.env_var {
90 return Ok(std::env::var(env_var).ok());
91 }
92
93 Ok(None)
94 }
95}
96
97impl fmt::Display for EncryptionConfig {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 if !self.enabled {
100 return write!(f, "encryption: disabled");
101 }
102 match (&self.passphrase, &self.env_var) {
103 (Some(p), _) => write!(
104 f,
105 "encryption: enabled (passphrase: {})",
106 mask_passphrase(p)
107 ),
108 (None, Some(var)) => write!(f, "encryption: enabled (env: {var})"),
109 (None, None) => write!(f, "encryption: enabled (no passphrase configured)"),
110 }
111 }
112}
113
114pub fn mask_passphrase(passphrase: &str) -> String {
118 let chars: Vec<char> = passphrase.chars().collect();
119 if chars.len() < 3 {
120 return "*".repeat(chars.len().max(1));
121 }
122 let first = chars[0];
123 let last = chars[chars.len() - 1];
124 format!("{first}{}{last}", "*".repeat(chars.len() - 2))
125}
126
127impl fmt::Display for StateEncryption {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 write!(f, "{}", self.config)
130 }
131}
132
133pub fn encrypt(data: &[u8], passphrase: &str) -> Result<Vec<u8>> {
154 let mut salt = [0u8; SALT_SIZE];
156 let mut nonce_bytes = [0u8; NONCE_SIZE];
157 OsRng.fill_bytes(&mut salt);
158 OsRng.fill_bytes(&mut nonce_bytes);
159
160 let key = derive_key(passphrase, &salt);
162
163 let cipher = Aes256Gcm::new_from_slice(&key).context("failed to create AES-256-GCM cipher")?;
165 let nonce = Nonce::from_slice(&nonce_bytes);
166 let ciphertext = cipher
167 .encrypt(nonce, data)
168 .map_err(|e| anyhow::anyhow!("encryption failed: {:?}", e))?;
169
170 let mut result = Vec::with_capacity(SALT_SIZE + NONCE_SIZE + ciphertext.len());
172 result.extend_from_slice(&salt);
173 result.extend_from_slice(&nonce_bytes);
174 result.extend_from_slice(&ciphertext);
175
176 Ok(BASE64.encode(&result).into_bytes())
178}
179
180pub fn decrypt(encrypted_data: impl AsRef<str>, passphrase: &str) -> Result<Vec<u8>> {
204 let encrypted_str = encrypted_data.as_ref();
205 let data = BASE64
207 .decode(encrypted_str)
208 .context("invalid base64 encoding")?;
209
210 if data.len() < SALT_SIZE + NONCE_SIZE + 16 {
212 bail!("encrypted data too short");
213 }
214
215 let salt = &data[..SALT_SIZE];
217 let nonce_bytes = &data[SALT_SIZE..SALT_SIZE + NONCE_SIZE];
218 let ciphertext = &data[SALT_SIZE + NONCE_SIZE..];
219
220 let key = derive_key(passphrase, salt);
222
223 let cipher = Aes256Gcm::new_from_slice(&key).context("failed to create AES-256-GCM cipher")?;
225 let nonce = Nonce::from_slice(nonce_bytes);
226 let plaintext = cipher.decrypt(nonce, ciphertext).map_err(|e| {
227 anyhow::anyhow!(
228 "decryption failed - wrong passphrase or corrupted data: {:?}",
229 e
230 )
231 })?;
232
233 Ok(plaintext)
234}
235
236fn derive_key(passphrase: &str, salt: &[u8]) -> [u8; KEY_SIZE] {
238 pbkdf2_hmac_array::<Sha256, KEY_SIZE>(passphrase.as_bytes(), salt, PBKDF2_ITERATIONS)
239}
240
241pub fn is_encrypted(content: &str) -> bool {
245 let Ok(data) = BASE64.decode(content) else {
247 return false;
248 };
249
250 if data.len() < SALT_SIZE + NONCE_SIZE + 16 {
252 return false;
253 }
254
255 true
260}
261
262pub fn read_decrypted(path: &Path, passphrase: &str) -> Result<String> {
271 let encrypted = std::fs::read_to_string(path)
272 .with_context(|| format!("failed to read encrypted file: {}", path.display()))?;
273
274 let decrypted = decrypt(&encrypted, passphrase)?;
276 String::from_utf8(decrypted).context("decrypted data is not valid UTF-8")
277}
278
279pub fn write_encrypted(path: &Path, data: &[u8], passphrase: &str) -> Result<()> {
286 let encrypted = encrypt(data, passphrase)?;
287
288 let encrypted_str =
290 String::from_utf8(encrypted).context("encrypted data is not valid UTF-8")?;
291
292 std::fs::write(path, encrypted_str)
293 .with_context(|| format!("failed to write encrypted file: {}", path.display()))?;
294
295 Ok(())
296}
297
298pub struct StateEncryption {
303 config: EncryptionConfig,
304}
305
306impl StateEncryption {
307 pub fn new(config: EncryptionConfig) -> Result<Self> {
309 Ok(Self { config })
310 }
311
312 fn get_passphrase(&self) -> Result<Option<String>> {
314 if !self.config.enabled {
315 return Ok(None);
316 }
317
318 if let Some(ref env_var) = self.config.env_var
320 && let Ok(passphrase) = std::env::var(env_var)
321 {
322 return Ok(Some(passphrase));
323 }
324
325 self.config.get_passphrase()
327 }
328
329 pub fn is_enabled(&self) -> bool {
331 self.config.enabled && self.get_passphrase().ok().flatten().is_some()
332 }
333
334 pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
336 let passphrase = self.get_passphrase()?.context(
337 "encryption is enabled but no passphrase available. Set SHIPPER_ENCRYPT_KEY environment variable or provide passphrase in config.",
338 )?;
339
340 encrypt(data, &passphrase)
341 }
342
343 pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
345 if let Some(passphrase) = self.get_passphrase()? {
347 if let Ok(decrypted) = decrypt(String::from_utf8_lossy(data), &passphrase) {
349 return Ok(decrypted);
350 }
351 }
352
353 Ok(data.to_vec())
356 }
357
358 pub fn read_file(&self, path: &Path) -> Result<String> {
360 if !self.is_enabled() {
361 return std::fs::read_to_string(path)
363 .with_context(|| format!("failed to read file: {}", path.display()));
364 }
365
366 let passphrase = self
367 .get_passphrase()?
368 .context("encryption is enabled but no passphrase available")?;
369
370 let content = std::fs::read_to_string(path)
371 .with_context(|| format!("failed to read file: {}", path.display()))?;
372
373 match decrypt(&content, &passphrase) {
375 Ok(decrypted) => {
376 String::from_utf8(decrypted).context("decrypted data is not valid UTF-8")
377 }
378 Err(_) => {
379 Ok(content)
381 }
382 }
383 }
384
385 pub fn write_file(&self, path: &Path, data: &[u8]) -> Result<()> {
387 if !self.is_enabled() {
388 return std::fs::write(path, data)
390 .with_context(|| format!("failed to write file: {}", path.display()));
391 }
392
393 let passphrase = self
394 .get_passphrase()?
395 .context("encryption is enabled but no passphrase available")?;
396
397 let encrypted = encrypt(data, &passphrase)?;
398 let encrypted_str =
399 String::from_utf8(encrypted).context("encrypted data is not valid UTF-8")?;
400
401 std::fs::write(path, encrypted_str)
402 .with_context(|| format!("failed to write encrypted file: {}", path.display()))
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409 use serial_test::serial;
410 use tempfile::tempdir;
411
412 #[test]
415 fn encrypt_decrypt_roundtrip() {
416 let plaintext = b"Hello, World! This is a test message.";
417 let passphrase = "test-passphrase-123";
418
419 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
420 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
421 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
422
423 assert_eq!(plaintext.to_vec(), decrypted);
424 }
425
426 #[test]
427 fn encrypt_produces_different_output_for_same_plaintext() {
428 let plaintext = b"Hello, World!";
429 let passphrase = "test-passphrase";
430
431 let encrypted1 = encrypt(plaintext, passphrase).expect("encryption should succeed");
432 let encrypted2 = encrypt(plaintext, passphrase).expect("encryption should succeed");
433
434 assert_ne!(encrypted1, encrypted2);
436
437 let decrypted1 = decrypt(
439 String::from_utf8(encrypted1).expect("valid UTF-8"),
440 passphrase,
441 )
442 .expect("decryption should succeed");
443 let decrypted2 = decrypt(
444 String::from_utf8(encrypted2).expect("valid UTF-8"),
445 passphrase,
446 )
447 .expect("decryption should succeed");
448
449 assert_eq!(decrypted1, decrypted2);
450 }
451
452 #[test]
453 fn decrypt_wrong_passphrase_fails() {
454 let plaintext = b"Secret data";
455 let passphrase = "correct-passphrase";
456 let wrong_passphrase = "wrong-passphrase";
457
458 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
459 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
460
461 let result = decrypt(&encrypted_str, wrong_passphrase);
462 assert!(result.is_err());
463 }
464
465 #[test]
468 fn encrypt_decrypt_empty_input() {
469 let plaintext = b"";
470 let passphrase = "test-passphrase";
471
472 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
473 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
474 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
475
476 assert_eq!(plaintext.to_vec(), decrypted);
477 }
478
479 #[test]
480 fn encrypt_empty_with_empty_passphrase() {
481 let plaintext = b"";
482 let passphrase = "";
483
484 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
485 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
486 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
487
488 assert_eq!(plaintext.to_vec(), decrypted);
489 }
490
491 #[test]
494 fn encrypt_decrypt_large_input() {
495 let plaintext: Vec<u8> = (0..1_048_576).map(|i| (i % 256) as u8).collect();
497 let passphrase = "large-data-passphrase";
498
499 let encrypted = encrypt(&plaintext, passphrase).expect("encryption should succeed");
500 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
501 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
502
503 assert_eq!(plaintext, decrypted);
504 }
505
506 #[test]
507 fn encrypt_decrypt_single_byte() {
508 let plaintext = b"\x42";
509 let passphrase = "single-byte";
510
511 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
512 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
513 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
514
515 assert_eq!(plaintext.to_vec(), decrypted);
516 }
517
518 #[test]
521 fn decrypt_invalid_base64_fails() {
522 let result = decrypt("not-valid-base64!!!", "passphrase");
523 assert!(result.is_err());
524 let err = result.unwrap_err().to_string();
525 assert!(
526 err.contains("base64"),
527 "error should mention base64, got: {err}"
528 );
529 }
530
531 #[test]
532 fn decrypt_too_short_data_fails() {
533 let short_data = vec![0u8; SALT_SIZE + NONCE_SIZE + 15];
535 let encoded = BASE64.encode(&short_data);
536
537 let result = decrypt(&encoded, "passphrase");
538 assert!(result.is_err());
539 let err = result.unwrap_err().to_string();
540 assert!(
541 err.contains("too short"),
542 "error should mention 'too short', got: {err}"
543 );
544 }
545
546 #[test]
547 fn decrypt_corrupted_ciphertext_fails() {
548 let plaintext = b"Some data to encrypt";
549 let passphrase = "test-pass";
550
551 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
552 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
553
554 let mut raw = BASE64.decode(&encrypted_str).expect("valid base64");
556 let idx = SALT_SIZE + NONCE_SIZE + 1;
557 raw[idx] ^= 0xFF;
558 let corrupted = BASE64.encode(&raw);
559
560 let result = decrypt(&corrupted, passphrase);
561 assert!(result.is_err());
562 }
563
564 #[test]
565 fn decrypt_corrupted_salt_fails() {
566 let plaintext = b"Some data";
567 let passphrase = "test-pass";
568
569 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
570 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
571
572 let mut raw = BASE64.decode(&encrypted_str).expect("valid base64");
574 raw[0] ^= 0xFF;
575 let corrupted = BASE64.encode(&raw);
576
577 let result = decrypt(&corrupted, passphrase);
578 assert!(result.is_err());
579 }
580
581 #[test]
582 fn decrypt_corrupted_nonce_fails() {
583 let plaintext = b"Some data";
584 let passphrase = "test-pass";
585
586 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
587 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
588
589 let mut raw = BASE64.decode(&encrypted_str).expect("valid base64");
591 raw[SALT_SIZE] ^= 0xFF;
592 let corrupted = BASE64.encode(&raw);
593
594 let result = decrypt(&corrupted, passphrase);
595 assert!(result.is_err());
596 }
597
598 #[test]
599 fn decrypt_empty_string_fails() {
600 let result = decrypt("", "passphrase");
601 assert!(result.is_err());
602 }
603
604 #[test]
605 fn decrypt_exactly_minimum_length_minus_one_fails() {
606 let data = vec![0u8; SALT_SIZE + NONCE_SIZE + 15];
608 let encoded = BASE64.encode(&data);
609 assert!(decrypt(&encoded, "pass").is_err());
610 }
611
612 #[test]
613 fn decrypt_exactly_minimum_length_fails_with_wrong_key() {
614 let data = vec![0u8; SALT_SIZE + NONCE_SIZE + 16];
616 let encoded = BASE64.encode(&data);
617 assert!(decrypt(&encoded, "pass").is_err());
619 }
620
621 #[test]
624 fn is_encrypted_detects_encrypted_data() {
625 let plaintext = b"Hello, World!";
626 let passphrase = "test-passphrase";
627
628 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
629 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
630
631 assert!(is_encrypted(&encrypted_str));
632 }
633
634 #[test]
635 fn is_encrypted_rejects_plaintext() {
636 let plaintext = r#"{"key": "value"}"#;
637 assert!(!is_encrypted(plaintext));
638 }
639
640 #[test]
641 fn is_encrypted_rejects_empty_string() {
642 assert!(!is_encrypted(""));
643 }
644
645 #[test]
646 fn is_encrypted_rejects_short_base64() {
647 let short = BASE64.encode(vec![0u8; SALT_SIZE + NONCE_SIZE + 10]);
649 assert!(!is_encrypted(&short));
650 }
651
652 #[test]
653 fn is_encrypted_rejects_non_base64() {
654 assert!(!is_encrypted("definitely not base64 $$$ !!!"));
655 }
656
657 #[test]
660 fn roundtrip_with_unicode_passphrase() {
661 let plaintext = b"Unicode passphrase test";
662 let passphrase = "pässwörd-密码-🔑";
663
664 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
665 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
666 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
667
668 assert_eq!(plaintext.to_vec(), decrypted);
669 }
670
671 #[test]
672 fn roundtrip_with_very_long_passphrase() {
673 let plaintext = b"Long passphrase test";
674 let passphrase: String = "a".repeat(10_000);
675
676 let encrypted = encrypt(plaintext, &passphrase).expect("encryption should succeed");
677 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
678 let decrypted = decrypt(&encrypted_str, &passphrase).expect("decryption should succeed");
679
680 assert_eq!(plaintext.to_vec(), decrypted);
681 }
682
683 #[test]
684 fn different_passphrases_produce_different_ciphertexts_when_decoded() {
685 let plaintext = b"Same plaintext";
686 let pass1 = "passphrase-one";
687 let pass2 = "passphrase-two";
688
689 let enc1 = encrypt(plaintext, pass1).expect("encrypt");
690 let enc2 = encrypt(plaintext, pass2).expect("encrypt");
691
692 let raw1 = BASE64
694 .decode(String::from_utf8(enc1).expect("utf8"))
695 .expect("base64");
696 let raw2 = BASE64
697 .decode(String::from_utf8(enc2).expect("utf8"))
698 .expect("base64");
699
700 let ct1 = &raw1[SALT_SIZE + NONCE_SIZE..];
702 let ct2 = &raw2[SALT_SIZE + NONCE_SIZE..];
703 assert_ne!(ct1, ct2);
704 }
705
706 #[test]
709 fn roundtrip_binary_data() {
710 let plaintext: Vec<u8> = (0..=255).collect();
711 let passphrase = "binary-data-test";
712
713 let encrypted = encrypt(&plaintext, passphrase).expect("encrypt");
714 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
715 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
716
717 assert_eq!(plaintext, decrypted);
718 }
719
720 #[test]
721 fn roundtrip_all_zero_bytes() {
722 let plaintext = vec![0u8; 1024];
723 let passphrase = "zeroes";
724
725 let encrypted = encrypt(&plaintext, passphrase).expect("encrypt");
726 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
727 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
728
729 assert_eq!(plaintext, decrypted);
730 }
731
732 #[test]
735 fn derive_key_produces_consistent_output() {
736 let passphrase = "test-passphrase";
737 let salt = [0u8; SALT_SIZE];
738
739 let key1 = derive_key(passphrase, &salt);
740 let key2 = derive_key(passphrase, &salt);
741
742 assert_eq!(key1, key2);
743 }
744
745 #[test]
746 fn derive_key_different_salts_produce_different_keys() {
747 let passphrase = "test-passphrase";
748 let salt1 = [0u8; SALT_SIZE];
749 let mut salt2 = [0u8; SALT_SIZE];
750 salt2[0] = 1;
751
752 let key1 = derive_key(passphrase, &salt1);
753 let key2 = derive_key(passphrase, &salt2);
754
755 assert_ne!(key1, key2);
756 }
757
758 #[test]
759 fn derive_key_different_passphrases_produce_different_keys() {
760 let salt = [42u8; SALT_SIZE];
761
762 let key1 = derive_key("passphrase-a", &salt);
763 let key2 = derive_key("passphrase-b", &salt);
764
765 assert_ne!(key1, key2);
766 }
767
768 #[test]
769 fn derive_key_empty_passphrase() {
770 let salt = [0u8; SALT_SIZE];
771 let key1 = derive_key("", &salt);
773 let key2 = derive_key("", &salt);
774 assert_eq!(key1, key2);
775 }
776
777 #[test]
780 fn encryption_config_default_is_disabled() {
781 let cfg = EncryptionConfig::default();
782 assert!(!cfg.enabled);
783 assert!(cfg.passphrase.is_none());
784 assert!(cfg.env_var.is_none());
785 }
786
787 #[test]
788 fn encryption_config_new_is_enabled() {
789 let cfg = EncryptionConfig::new("secret".to_string());
790 assert!(cfg.enabled);
791 assert_eq!(cfg.passphrase.as_deref(), Some("secret"));
792 assert!(cfg.env_var.is_none());
793 }
794
795 #[test]
796 fn encryption_config_from_env_is_enabled() {
797 let cfg = EncryptionConfig::from_env("MY_VAR".to_string());
798 assert!(cfg.enabled);
799 assert!(cfg.passphrase.is_none());
800 assert_eq!(cfg.env_var.as_deref(), Some("MY_VAR"));
801 }
802
803 #[test]
804 fn encryption_config_get_passphrase_direct() {
805 let cfg = EncryptionConfig::new("hello".to_string());
806 assert_eq!(cfg.get_passphrase().unwrap(), Some("hello".to_string()));
807 }
808
809 #[test]
810 fn encryption_config_get_passphrase_none_when_disabled() {
811 let cfg = EncryptionConfig::default();
812 assert_eq!(cfg.get_passphrase().unwrap(), None);
813 }
814
815 #[test]
816 fn encryption_config_serde_roundtrip() {
817 let cfg = EncryptionConfig::new("test".to_string());
818 let json = serde_json::to_string(&cfg).expect("serialize");
819 let deserialized: EncryptionConfig = serde_json::from_str(&json).expect("deserialize");
820 assert_eq!(deserialized.enabled, cfg.enabled);
821 assert_eq!(deserialized.passphrase, cfg.passphrase);
822 }
823
824 #[test]
825 fn encryption_config_serde_skips_none_fields() {
826 let cfg = EncryptionConfig::default();
827 let json = serde_json::to_string(&cfg).expect("serialize");
828 assert!(!json.contains("passphrase"));
829 assert!(!json.contains("env_var"));
830 }
831
832 #[test]
835 fn state_encryption_enabled_disabled() {
836 let config = EncryptionConfig::default();
837 let encryption = StateEncryption::new(config.clone()).expect("should create");
838 assert!(!encryption.is_enabled());
839
840 let config = EncryptionConfig::new("test-passphrase".to_string());
841 let encryption = StateEncryption::new(config).expect("should create");
842 assert!(encryption.is_enabled());
843 }
844
845 #[test]
846 fn state_encryption_roundtrip() {
847 let config = EncryptionConfig::new("my-secret-passphrase".to_string());
848 let encryption = StateEncryption::new(config).expect("should create");
849
850 let data = b"Test state data";
851
852 let encrypted = encryption.encrypt(data).expect("encryption should succeed");
853 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
854 let decrypted =
855 decrypt(&encrypted_str, "my-secret-passphrase").expect("decryption should succeed");
856
857 assert_eq!(data.to_vec(), decrypted);
858 }
859
860 #[test]
861 fn state_encryption_decrypt_roundtrip() {
862 let config = EncryptionConfig::new("my-pass".to_string());
863 let encryption = StateEncryption::new(config).expect("should create");
864
865 let data = b"state data to encrypt";
866 let encrypted = encryption.encrypt(data).expect("encrypt");
867 let decrypted = encryption.decrypt(&encrypted).expect("decrypt");
868
869 assert_eq!(data.to_vec(), decrypted);
870 }
871
872 #[test]
873 fn state_encryption_disabled_passthrough() {
874 let config = EncryptionConfig::default();
875 let encryption = StateEncryption::new(config).expect("should create");
876
877 let data = b"Plain text data";
878
879 let result = encryption.decrypt(data).expect("should succeed");
880 assert_eq!(data.to_vec(), result);
881 }
882
883 #[test]
884 fn state_encryption_disabled_encrypt_passthrough_on_decrypt() {
885 let config = EncryptionConfig::default();
887 let encryption = StateEncryption::new(config).expect("should create");
888
889 let garbage = b"\x00\x01\x02\x03";
890 let result = encryption.decrypt(garbage).expect("should succeed");
891 assert_eq!(garbage.to_vec(), result);
892 }
893
894 #[test]
897 fn encrypt_produces_valid_base64() {
898 let plaintext = b"Test data";
899 let passphrase = "test";
900
901 let encrypted = encrypt(plaintext, passphrase).expect("should encrypt");
902 let encrypted_str = String::from_utf8(encrypted.clone()).expect("valid UTF-8");
903
904 let decoded = BASE64.decode(&encrypted_str).expect("valid base64");
905 assert!(decoded.len() > plaintext.len());
906 }
907
908 #[test]
909 fn encrypted_output_has_expected_structure() {
910 let plaintext = b"Hello";
911 let passphrase = "test";
912
913 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
914 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
915 let raw = BASE64.decode(&encrypted_str).expect("base64");
916
917 let expected_len = SALT_SIZE + NONCE_SIZE + plaintext.len() + 16;
919 assert_eq!(raw.len(), expected_len);
920 }
921
922 #[test]
925 fn read_write_encrypted_file() {
926 let td = tempdir().expect("tempdir");
927 let path = td.path().join("test.enc");
928
929 let plaintext = b"Secret file content";
930 let passphrase = "file-passphrase";
931
932 write_encrypted(&path, plaintext, passphrase).expect("write encrypted");
933 let decrypted = read_decrypted(&path, passphrase).expect("read decrypted");
934
935 assert_eq!(plaintext.to_vec(), decrypted.into_bytes());
936 }
937
938 #[test]
939 fn read_decrypted_wrong_passphrase_fails() {
940 let td = tempdir().expect("tempdir");
941 let path = td.path().join("test.enc");
942
943 write_encrypted(&path, b"data", "correct").expect("write");
944 let result = read_decrypted(&path, "wrong");
945 assert!(result.is_err());
946 }
947
948 #[test]
949 fn read_decrypted_nonexistent_file_fails() {
950 let td = tempdir().expect("tempdir");
951 let path = td.path().join("does-not-exist.enc");
952
953 let result = read_decrypted(&path, "pass");
954 assert!(result.is_err());
955 }
956
957 #[test]
958 fn write_encrypted_file_is_base64_on_disk() {
959 let td = tempdir().expect("tempdir");
960 let path = td.path().join("test.enc");
961
962 write_encrypted(&path, b"data", "pass").expect("write");
963 let on_disk = std::fs::read_to_string(&path).expect("read");
964
965 assert!(BASE64.decode(&on_disk).is_ok());
967 assert_ne!(on_disk, "data");
969 }
970
971 #[test]
972 fn state_encryption_file_roundtrip() {
973 let td = tempdir().expect("tempdir");
974 let path = td.path().join("state.json");
975
976 let config = EncryptionConfig::new("test-pass".to_string());
977 let encryption = StateEncryption::new(config).expect("should create");
978
979 let data = br#"{"key": "value"}"#;
980
981 encryption.write_file(&path, data).expect("write file");
982 let content = encryption.read_file(&path).expect("read file");
983
984 assert_eq!(String::from_utf8_lossy(data), content);
985 }
986
987 #[test]
988 fn state_encryption_unencrypted_fallback() {
989 let td = tempdir().expect("tempdir");
990 let path = td.path().join("plain.json");
991
992 let config = EncryptionConfig::new("test-pass".to_string());
993 let encryption = StateEncryption::new(config).expect("should create");
994
995 let data = r#"{"plain": "data"}"#;
997 std::fs::write(&path, data).expect("write plain");
998
999 let content = encryption.read_file(&path).expect("read file");
1001 assert_eq!(data, content);
1002 }
1003
1004 #[test]
1005 fn state_encryption_disabled_writes_plaintext() {
1006 let td = tempdir().expect("tempdir");
1007 let path = td.path().join("plain.json");
1008
1009 let config = EncryptionConfig::default();
1010 let encryption = StateEncryption::new(config).expect("create");
1011
1012 let data = b"plain text content";
1013 encryption.write_file(&path, data).expect("write");
1014
1015 let on_disk = std::fs::read(&path).expect("read");
1016 assert_eq!(data.to_vec(), on_disk);
1017 }
1018
1019 #[test]
1020 fn state_encryption_disabled_reads_plaintext() {
1021 let td = tempdir().expect("tempdir");
1022 let path = td.path().join("plain.txt");
1023 std::fs::write(&path, "hello").expect("write");
1024
1025 let config = EncryptionConfig::default();
1026 let encryption = StateEncryption::new(config).expect("create");
1027
1028 let content = encryption.read_file(&path).expect("read");
1029 assert_eq!(content, "hello");
1030 }
1031
1032 #[test]
1033 fn state_encryption_read_nonexistent_file_fails() {
1034 let td = tempdir().expect("tempdir");
1035 let path = td.path().join("nope.json");
1036
1037 let config = EncryptionConfig::new("pass".to_string());
1038 let encryption = StateEncryption::new(config).expect("create");
1039
1040 assert!(encryption.read_file(&path).is_err());
1041 }
1042
1043 #[test]
1046 fn encrypt_decrypt_data_over_1mb() {
1047 let plaintext: Vec<u8> = (0u64..2_097_152)
1049 .map(|i| (i.wrapping_mul(7) % 256) as u8)
1050 .collect();
1051 let passphrase = "large-2mb-passphrase";
1052
1053 let encrypted = encrypt(&plaintext, passphrase).expect("encryption should succeed");
1054 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1055 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
1056
1057 assert_eq!(plaintext, decrypted);
1058 }
1059
1060 #[test]
1063 fn roundtrip_single_char_passphrase() {
1064 let plaintext = b"single char key";
1065 let passphrase = "x";
1066
1067 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1068 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1069 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1070
1071 assert_eq!(plaintext.to_vec(), decrypted);
1072 }
1073
1074 #[test]
1075 fn roundtrip_whitespace_only_passphrase() {
1076 let plaintext = b"whitespace key test";
1077 let passphrase = " \t\n ";
1078
1079 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1080 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1081 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1082
1083 assert_eq!(plaintext.to_vec(), decrypted);
1084 }
1085
1086 #[test]
1087 fn roundtrip_max_reasonable_passphrase() {
1088 let plaintext = b"max key test";
1089 let passphrase: String = "Z".repeat(100_000);
1091
1092 let encrypted = encrypt(plaintext, &passphrase).expect("encrypt");
1093 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1094 let decrypted = decrypt(&encrypted_str, &passphrase).expect("decrypt");
1095
1096 assert_eq!(plaintext.to_vec(), decrypted);
1097 }
1098
1099 #[test]
1102 fn nonce_uniqueness_raw_salt_and_nonce_differ() {
1103 let plaintext = b"nonce uniqueness check";
1104 let passphrase = "same-passphrase";
1105
1106 let enc1 = encrypt(plaintext, passphrase).expect("encrypt");
1107 let enc2 = encrypt(plaintext, passphrase).expect("encrypt");
1108
1109 let raw1 = BASE64
1110 .decode(String::from_utf8(enc1).expect("utf8"))
1111 .expect("base64");
1112 let raw2 = BASE64
1113 .decode(String::from_utf8(enc2).expect("utf8"))
1114 .expect("base64");
1115
1116 let salt_nonce_1 = &raw1[..SALT_SIZE + NONCE_SIZE];
1117 let salt_nonce_2 = &raw2[..SALT_SIZE + NONCE_SIZE];
1118
1119 assert_ne!(salt_nonce_1, salt_nonce_2);
1121 }
1122
1123 #[test]
1126 fn tampered_auth_tag_detected() {
1127 let plaintext = b"auth tag tamper test";
1128 let passphrase = "test-pass";
1129
1130 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1131 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1132
1133 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
1134 let last = raw.len() - 1;
1136 raw[last] ^= 0xFF;
1137 let corrupted = BASE64.encode(&raw);
1138
1139 assert!(decrypt(&corrupted, passphrase).is_err());
1140 }
1141
1142 #[test]
1143 fn tampered_single_bit_flip_detected() {
1144 let plaintext = b"bit flip detection";
1145 let passphrase = "bit-flip-pass";
1146
1147 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1148 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1149
1150 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
1151 let mid = raw.len() / 2;
1153 raw[mid] ^= 0x01;
1154 let corrupted = BASE64.encode(&raw);
1155
1156 assert!(decrypt(&corrupted, passphrase).is_err());
1157 }
1158
1159 #[test]
1162 fn wrong_key_returns_error_not_garbage_data() {
1163 let plaintext = b"This must not leak through wrong key";
1164 let correct = "correct-key";
1165 let wrong = "wrong-key";
1166
1167 let encrypted = encrypt(plaintext, correct).expect("encrypt");
1168 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1169
1170 let result = decrypt(&encrypted_str, wrong);
1171 assert!(
1173 result.is_err(),
1174 "wrong key must return Err, not Ok with garbage"
1175 );
1176
1177 let err_msg = result.unwrap_err().to_string();
1178 assert!(
1179 err_msg.contains("wrong passphrase or corrupted data"),
1180 "error message should indicate wrong passphrase, got: {err_msg}"
1181 );
1182 }
1183
1184 #[test]
1185 fn wrong_key_similar_passphrase_returns_error() {
1186 let plaintext = b"subtle key difference";
1187 let correct = "my-passphrase-abc";
1188 let wrong = "my-passphrase-abd"; let encrypted = encrypt(plaintext, correct).expect("encrypt");
1191 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1192
1193 assert!(
1194 decrypt(&encrypted_str, wrong).is_err(),
1195 "even a single-char difference must cause decryption failure"
1196 );
1197 }
1198
1199 #[test]
1202 fn roundtrip_realistic_json_state() {
1203 let state_json = br#"{
1204 "plan_id": "abc123",
1205 "workspace": "/home/user/project",
1206 "crates": [
1207 {"name": "core", "version": "0.1.0", "status": "published"},
1208 {"name": "cli", "version": "0.2.0", "status": "pending"}
1209 ],
1210 "started_at": "2024-01-15T10:30:00Z",
1211 "token": "cio_supersecrettoken1234567890"
1212 }"#;
1213 let passphrase = "ci-pipeline-key-2024";
1214
1215 let encrypted = encrypt(state_json, passphrase).expect("encrypt");
1216 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1217 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1218
1219 assert_eq!(state_json.to_vec(), decrypted);
1220 }
1221
1222 #[test]
1223 fn roundtrip_event_log_jsonl() {
1224 let events = b"{\"event\":\"publish_start\",\"crate\":\"core\",\"ts\":1700000000}\n\
1225 {\"event\":\"publish_ok\",\"crate\":\"core\",\"ts\":1700000005}\n\
1226 {\"event\":\"publish_start\",\"crate\":\"cli\",\"ts\":1700000010}\n";
1227 let passphrase = "event-log-key";
1228
1229 let encrypted = encrypt(events, passphrase).expect("encrypt");
1230 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1231 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1232
1233 assert_eq!(events.to_vec(), decrypted);
1234 }
1235
1236 #[test]
1239 fn derive_key_always_produces_32_bytes() {
1240 for passphrase in ["", "a", "short", &"x".repeat(10_000)] {
1241 for salt in [&[0u8; 0][..], &[0u8; 1], &[0u8; SALT_SIZE], &[0xFF; 64]] {
1242 let key = derive_key(passphrase, salt);
1243 assert_eq!(
1244 key.len(),
1245 KEY_SIZE,
1246 "key must be {KEY_SIZE} bytes for passphrase len={}, salt len={}",
1247 passphrase.len(),
1248 salt.len()
1249 );
1250 }
1251 }
1252 }
1253
1254 #[test]
1255 fn derive_key_with_empty_salt() {
1256 let key1 = derive_key("passphrase", &[]);
1257 let key2 = derive_key("passphrase", &[]);
1258 assert_eq!(key1, key2, "empty salt should still be deterministic");
1259 assert_eq!(key1.len(), KEY_SIZE);
1260 }
1261
1262 #[test]
1265 fn encrypt_decrypt_exactly_aes_block_size() {
1266 let plaintext = [0xABu8; 16];
1268 let passphrase = "block-boundary";
1269
1270 let encrypted = encrypt(&plaintext, passphrase).expect("encrypt");
1271 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1272 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1273
1274 assert_eq!(plaintext.to_vec(), decrypted);
1275 }
1276
1277 #[test]
1278 fn encrypt_decrypt_multi_block_boundaries() {
1279 let passphrase = "multi-block";
1280 for size in [15, 16, 17, 31, 32, 33, 48, 64, 128, 255, 256, 257] {
1281 let plaintext: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
1282 let encrypted = encrypt(&plaintext, passphrase).expect("encrypt");
1283 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1284 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1285 assert_eq!(plaintext, decrypted, "roundtrip failed for size {size}");
1286 }
1287 }
1288
1289 #[test]
1292 fn state_encryption_encrypt_enabled_no_passphrase_errors() {
1293 let config = EncryptionConfig {
1294 enabled: true,
1295 passphrase: None,
1296 env_var: None,
1297 };
1298 let encryption = StateEncryption::new(config).expect("create");
1299 assert!(!encryption.is_enabled());
1300
1301 let result = encryption.encrypt(b"data");
1302 assert!(result.is_err(), "encrypt with no passphrase should fail");
1303 let err = result.unwrap_err().to_string();
1304 assert!(
1305 err.contains("no passphrase"),
1306 "error should mention missing passphrase, got: {err}"
1307 );
1308 }
1309
1310 #[test]
1311 fn state_encryption_cross_config_decrypt_fails() {
1312 let config_a = EncryptionConfig::new("key-alpha".to_string());
1313 let config_b = EncryptionConfig::new("key-beta".to_string());
1314 let enc_a = StateEncryption::new(config_a).expect("create");
1315 let enc_b = StateEncryption::new(config_b).expect("create");
1316
1317 let data = b"cross-config secret";
1318 let encrypted = enc_a.encrypt(data).expect("encrypt with A");
1319
1320 let result = enc_b.decrypt(&encrypted).expect("decrypt returns fallback");
1322 assert_ne!(
1323 result,
1324 data.to_vec(),
1325 "wrong config must not produce original plaintext"
1326 );
1327 }
1328
1329 #[test]
1332 fn decrypt_truncated_after_header_fails() {
1333 let plaintext = b"data to truncate";
1334 let passphrase = "trunc-pass";
1335
1336 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1337 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1338 let raw = BASE64.decode(&encrypted_str).expect("base64");
1339
1340 let truncated = &raw[..SALT_SIZE + NONCE_SIZE + 16];
1342 let encoded = BASE64.encode(truncated);
1343
1344 assert!(
1345 decrypt(&encoded, passphrase).is_err(),
1346 "truncated ciphertext must fail decryption"
1347 );
1348 }
1349
1350 #[test]
1353 fn is_encrypted_accepts_exact_minimum_length() {
1354 let data = vec![0u8; SALT_SIZE + NONCE_SIZE + 16];
1356 let encoded = BASE64.encode(&data);
1357 assert!(
1358 is_encrypted(&encoded),
1359 "minimum-length valid base64 should pass heuristic"
1360 );
1361 }
1362
1363 #[test]
1366 fn multiple_sequential_encrypt_decrypt_cycles() {
1367 let passphrase = "cycle-test";
1368 let mut data = b"initial plaintext".to_vec();
1369
1370 for i in 0..50 {
1371 let encrypted = encrypt(&data, passphrase)
1372 .unwrap_or_else(|e| panic!("encrypt failed on cycle {i}: {e}"));
1373 let encrypted_str = String::from_utf8(encrypted)
1374 .unwrap_or_else(|e| panic!("utf8 failed on cycle {i}: {e}"));
1375 let decrypted = decrypt(&encrypted_str, passphrase)
1376 .unwrap_or_else(|e| panic!("decrypt failed on cycle {i}: {e}"));
1377 assert_eq!(data, decrypted, "mismatch on cycle {i}");
1378 data.push((i % 256) as u8);
1380 }
1381 }
1382
1383 #[test]
1386 fn roundtrip_null_bytes_in_plaintext() {
1387 let plaintext = b"before\x00middle\x00\x00after\x00";
1388 let passphrase = "null-byte-pass";
1389
1390 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1391 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1392 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1393
1394 assert_eq!(plaintext.to_vec(), decrypted);
1395 }
1396
1397 #[test]
1398 fn roundtrip_all_0xff_bytes() {
1399 let plaintext = vec![0xFFu8; 512];
1400 let passphrase = "high-entropy";
1401
1402 let encrypted = encrypt(&plaintext, passphrase).expect("encrypt");
1403 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1404 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1405
1406 assert_eq!(plaintext, decrypted);
1407 }
1408
1409 #[test]
1412 #[serial]
1413 fn env_var_passphrase_resolution() {
1414 let cfg = EncryptionConfig::from_env("SHIPPER_TEST_PASS_1".to_string());
1415 temp_env::with_var("SHIPPER_TEST_PASS_1", Some("env-secret"), || {
1416 let passphrase = cfg.get_passphrase().unwrap();
1417 assert_eq!(passphrase, Some("env-secret".to_string()));
1418 });
1419 }
1420
1421 #[test]
1422 #[serial]
1423 fn env_var_passphrase_missing_returns_none() {
1424 let cfg = EncryptionConfig::from_env("SHIPPER_TEST_MISSING_VAR".to_string());
1425 temp_env::with_var("SHIPPER_TEST_MISSING_VAR", None::<&str>, || {
1426 let passphrase = cfg.get_passphrase().unwrap();
1427 assert_eq!(passphrase, None);
1428 });
1429 }
1430
1431 #[test]
1432 #[serial]
1433 fn state_encryption_from_env_var_roundtrip() {
1434 let config = EncryptionConfig::from_env("SHIPPER_TEST_ENC_PASS".to_string());
1435 let encryption = StateEncryption::new(config).expect("create");
1436
1437 temp_env::with_var("SHIPPER_TEST_ENC_PASS", Some("my-env-key"), || {
1438 assert!(encryption.is_enabled());
1439
1440 let data = b"env-var encrypted data";
1441 let encrypted = encryption.encrypt(data).expect("encrypt");
1442 let decrypted = encryption.decrypt(&encrypted).expect("decrypt");
1443 assert_eq!(data.to_vec(), decrypted);
1444 });
1445 }
1446
1447 #[test]
1448 #[serial]
1449 fn state_encryption_env_var_takes_precedence() {
1450 let config = EncryptionConfig {
1451 enabled: true,
1452 passphrase: Some("inline-pass".to_string()),
1453 env_var: Some("SHIPPER_TEST_PRIO_PASS".to_string()),
1454 };
1455 let encryption = StateEncryption::new(config).expect("create");
1456
1457 temp_env::with_var("SHIPPER_TEST_PRIO_PASS", Some("env-pass"), || {
1458 let data = b"priority test";
1460 let encrypted = encryption.encrypt(data).expect("encrypt");
1461
1462 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
1464 assert!(
1465 decrypt(&encrypted_str, "env-pass").is_ok(),
1466 "env var passphrase should take priority"
1467 );
1468 });
1469 }
1470
1471 #[test]
1472 #[serial]
1473 fn state_encryption_file_roundtrip_with_env_var() {
1474 let td = tempdir().expect("tempdir");
1475 let path = td.path().join("env_enc.json");
1476
1477 let config = EncryptionConfig::from_env("SHIPPER_TEST_FILE_PASS".to_string());
1478 let encryption = StateEncryption::new(config).expect("create");
1479
1480 temp_env::with_var("SHIPPER_TEST_FILE_PASS", Some("file-env-key"), || {
1481 let data = br#"{"encrypted_via": "env_var"}"#;
1482 encryption.write_file(&path, data).expect("write");
1483 let content = encryption.read_file(&path).expect("read");
1484 assert_eq!(String::from_utf8_lossy(data), content);
1485 });
1486 }
1487
1488 #[test]
1491 fn salt_uniqueness_across_10_encryptions() {
1492 let plaintext = b"salt uniqueness test";
1493 let passphrase = "salt-test";
1494
1495 let mut salts = Vec::new();
1496 for _ in 0..10 {
1497 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1498 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
1499 let raw = BASE64.decode(&encrypted_str).expect("base64");
1500 let salt = raw[..SALT_SIZE].to_vec();
1501 salts.push(salt);
1502 }
1503
1504 for i in 0..salts.len() {
1506 for j in (i + 1)..salts.len() {
1507 assert_ne!(salts[i], salts[j], "salt collision at indices {i} and {j}");
1508 }
1509 }
1510 }
1511
1512 #[test]
1515 fn derive_key_unicode_passphrase_is_deterministic() {
1516 let passphrase = "пароль-密码-🔑";
1517 let salt = [0x42u8; SALT_SIZE];
1518 let key1 = derive_key(passphrase, &salt);
1519 let key2 = derive_key(passphrase, &salt);
1520 assert_eq!(key1, key2);
1521 }
1522
1523 #[test]
1524 fn derive_key_newline_passphrase_differs_from_stripped() {
1525 let salt = [0u8; SALT_SIZE];
1526 let key_with_newlines = derive_key("pass\nphrase\n", &salt);
1527 let key_stripped = derive_key("passphrase", &salt);
1528 assert_ne!(key_with_newlines, key_stripped);
1529 }
1530
1531 #[test]
1534 fn double_encrypt_roundtrip() {
1535 let plaintext = b"double layer secret";
1536 let pass1 = "outer-key";
1537 let pass2 = "inner-key";
1538
1539 let inner = encrypt(plaintext, pass1).expect("encrypt inner");
1540 let outer = encrypt(&inner, pass2).expect("encrypt outer");
1541
1542 let outer_str = String::from_utf8(outer).expect("utf8");
1543 let decrypted_outer = decrypt(&outer_str, pass2).expect("decrypt outer");
1544 let inner_str = String::from_utf8(decrypted_outer).expect("utf8");
1545 let decrypted_inner = decrypt(&inner_str, pass1).expect("decrypt inner");
1546
1547 assert_eq!(plaintext.to_vec(), decrypted_inner);
1548 }
1549
1550 #[test]
1553 fn is_encrypted_rejects_whitespace_around_base64() {
1554 let data = vec![0u8; SALT_SIZE + NONCE_SIZE + 16];
1555 let encoded = format!(" {} ", BASE64.encode(&data));
1556 assert!(!is_encrypted(&encoded));
1558 }
1559
1560 #[test]
1561 fn is_encrypted_rejects_json_object() {
1562 assert!(!is_encrypted(r#"{"plan_id":"abc","crates":[]}"#));
1563 }
1564
1565 #[test]
1568 fn state_encryption_decrypt_returns_original_on_bad_encrypted_data() {
1569 let config = EncryptionConfig::new("test-pass".to_string());
1570 let encryption = StateEncryption::new(config).expect("create");
1571
1572 let raw_json = b"plain JSON content";
1574 let result = encryption.decrypt(raw_json).expect("should fall back");
1575 assert_eq!(raw_json.to_vec(), result);
1576 }
1577
1578 #[test]
1581 fn file_roundtrip_unicode_content() {
1582 let td = tempdir().expect("tempdir");
1583 let path = td.path().join("unicode.enc");
1584
1585 let plaintext = "Ünïcödé cöntënt: 日本語テスト 🎉";
1586 write_encrypted(&path, plaintext.as_bytes(), "unicode-pass").expect("write");
1587 let decrypted = read_decrypted(&path, "unicode-pass").expect("read");
1588 assert_eq!(plaintext, decrypted);
1589 }
1590
1591 #[test]
1594 fn encrypt_decrypt_exactly_gcm_tag_size() {
1595 let plaintext = [0xCD; 16];
1597 let passphrase = "tag-size-test";
1598
1599 let encrypted = encrypt(&plaintext, passphrase).expect("encrypt");
1600 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
1601 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1602 assert_eq!(plaintext.to_vec(), decrypted);
1603 }
1604
1605 #[test]
1608 fn display_config_passphrase_takes_precedence_in_display() {
1609 let cfg = EncryptionConfig {
1610 enabled: true,
1611 passphrase: Some("my-pass".to_string()),
1612 env_var: Some("MY_ENV".to_string()),
1613 };
1614 let display = cfg.to_string();
1615 assert!(
1617 display.contains("passphrase:"),
1618 "should show passphrase branch, got: {display}"
1619 );
1620 }
1621
1622 #[test]
1625 fn mask_passphrase_four_chars() {
1626 let masked = mask_passphrase("abcd");
1627 assert_eq!(masked, "a**d");
1628 }
1629
1630 #[test]
1631 fn mask_passphrase_five_chars() {
1632 let masked = mask_passphrase("hello");
1633 assert_eq!(masked, "h***o");
1634 }
1635
1636 #[test]
1637 fn mask_passphrase_with_spaces() {
1638 let masked = mask_passphrase("a b c");
1639 assert_eq!(masked, "a***c");
1640 }
1641
1642 #[test]
1645 #[serial]
1646 fn state_encryption_disabled_ignores_env_var() {
1647 let config = EncryptionConfig {
1648 enabled: false,
1649 passphrase: None,
1650 env_var: Some("SHIPPER_TEST_IGNORED_VAR".to_string()),
1651 };
1652 let encryption = StateEncryption::new(config).expect("create");
1653
1654 temp_env::with_var("SHIPPER_TEST_IGNORED_VAR", Some("secret"), || {
1655 assert!(!encryption.is_enabled());
1656 let data = b"not encrypted";
1658 let result = encryption.decrypt(data).expect("passthrough");
1659 assert_eq!(data.to_vec(), result);
1660 });
1661 }
1662
1663 #[test]
1670 fn read_decrypted_non_utf8_plaintext_errors() {
1671 let td = tempdir().expect("tempdir");
1672 let path = td.path().join("binary.enc");
1673
1674 let binary = vec![0x80u8, 0xFE, 0xFF, 0xC0, 0x80];
1676 write_encrypted(&path, &binary, "binary-pass").expect("write");
1677
1678 let result = read_decrypted(&path, "binary-pass");
1679 assert!(
1680 result.is_err(),
1681 "non-UTF-8 decrypted data must surface as an error"
1682 );
1683 let err = result.unwrap_err().to_string();
1684 assert!(
1685 err.contains("not valid UTF-8"),
1686 "error should mention UTF-8 validation, got: {err}"
1687 );
1688 }
1689
1690 #[test]
1696 fn write_encrypted_to_invalid_path_errors() {
1697 let td = tempdir().expect("tempdir");
1698 let path = td.path().join("does_not_exist_dir").join("file.enc");
1700
1701 let result = write_encrypted(&path, b"data", "pass");
1702 assert!(result.is_err(), "writing into a missing dir must fail");
1703 let err = result.unwrap_err().to_string();
1704 assert!(
1705 err.contains("failed to write encrypted file"),
1706 "error should mention failing write context, got: {err}"
1707 );
1708 }
1709
1710 #[test]
1715 fn state_encryption_write_file_encrypted_path_io_error() {
1716 let td = tempdir().expect("tempdir");
1717 let path = td.path().join("missing_subdir").join("state.enc");
1718
1719 let config = EncryptionConfig::new("io-err-pass".to_string());
1720 let encryption = StateEncryption::new(config).expect("create");
1721
1722 let result = encryption.write_file(&path, b"payload");
1723 assert!(result.is_err(), "write to missing parent dir must fail");
1724 let err = result.unwrap_err().to_string();
1725 assert!(
1726 err.contains("failed to write encrypted file"),
1727 "error should mention failing write context, got: {err}"
1728 );
1729 }
1730
1731 #[test]
1734 fn state_encryption_write_file_plaintext_path_io_error() {
1735 let td = tempdir().expect("tempdir");
1736 let path = td.path().join("missing_subdir").join("plain.txt");
1737
1738 let config = EncryptionConfig::default();
1739 let encryption = StateEncryption::new(config).expect("create");
1740
1741 let result = encryption.write_file(&path, b"plain payload");
1742 assert!(result.is_err(), "plain write to missing dir must fail");
1743 let err = result.unwrap_err().to_string();
1744 assert!(
1745 err.contains("failed to write file"),
1746 "error should mention failing write context, got: {err}"
1747 );
1748 }
1749
1750 #[test]
1755 fn state_encryption_read_file_non_utf8_decrypted_errors() {
1756 let td = tempdir().expect("tempdir");
1757 let path = td.path().join("binary.enc");
1758
1759 let config = EncryptionConfig::new("rf-pass".to_string());
1760 let encryption = StateEncryption::new(config).expect("create");
1761
1762 let binary = vec![0xFFu8, 0xFE, 0xFD, 0x80, 0xC0];
1765 encryption
1766 .write_file(&path, &binary)
1767 .expect("write encrypted");
1768
1769 let result = encryption.read_file(&path);
1770 assert!(
1771 result.is_err(),
1772 "non-UTF-8 decrypted state should surface as Err"
1773 );
1774 let err = result.unwrap_err().to_string();
1775 assert!(
1776 err.contains("not valid UTF-8"),
1777 "error should mention UTF-8 validation, got: {err}"
1778 );
1779 }
1780
1781 #[test]
1788 fn state_encryption_read_file_enabled_returns_plain_when_not_encrypted() {
1789 let td = tempdir().expect("tempdir");
1790 let path = td.path().join("plain.json");
1791
1792 let plain = r#"{"unencrypted": "legacy state"}"#;
1795 std::fs::write(&path, plain).expect("write plain");
1796
1797 let config = EncryptionConfig::new("any-pass".to_string());
1798 let encryption = StateEncryption::new(config).expect("create");
1799
1800 let content = encryption.read_file(&path).expect("read");
1801 assert_eq!(content, plain);
1802 }
1803
1804 #[test]
1810 fn state_encryption_read_file_enabled_non_utf8_file_errors() {
1811 let td = tempdir().expect("tempdir");
1812 let path = td.path().join("garbage.bin");
1813
1814 std::fs::write(&path, [0xFFu8, 0xFE, 0xFD, 0x80, 0xC0]).expect("write");
1817
1818 let config = EncryptionConfig::new("any-pass".to_string());
1819 let encryption = StateEncryption::new(config).expect("create");
1820
1821 let result = encryption.read_file(&path);
1822 assert!(
1823 result.is_err(),
1824 "non-UTF-8 file contents must surface as Err when encryption enabled"
1825 );
1826 let err = result.unwrap_err().to_string();
1827 assert!(
1828 err.contains("failed to read file"),
1829 "error should mention failing read context, got: {err}"
1830 );
1831 }
1832
1833 #[test]
1841 fn encryption_config_enabled_no_source_returns_none_passphrase() {
1842 let cfg = EncryptionConfig {
1843 enabled: true,
1844 passphrase: None,
1845 env_var: None,
1846 };
1847 assert_eq!(cfg.get_passphrase().unwrap(), None);
1848 }
1849
1850 #[test]
1854 #[serial]
1855 fn encryption_config_env_var_unset_returns_none_not_err() {
1856 let cfg = EncryptionConfig::from_env("SHIPPER_TEST_UNSET_PASS_X1".to_string());
1857 temp_env::with_var("SHIPPER_TEST_UNSET_PASS_X1", None::<&str>, || {
1858 let result = cfg.get_passphrase();
1859 assert!(result.is_ok());
1860 assert_eq!(result.unwrap(), None);
1861 });
1862 }
1863
1864 #[test]
1871 #[serial]
1872 fn state_encryption_falls_back_to_inline_when_env_unset() {
1873 let config = EncryptionConfig {
1874 enabled: true,
1875 passphrase: Some("inline-fallback".to_string()),
1876 env_var: Some("SHIPPER_TEST_FALLBACK_VAR".to_string()),
1877 };
1878 let encryption = StateEncryption::new(config).expect("create");
1879
1880 temp_env::with_var("SHIPPER_TEST_FALLBACK_VAR", None::<&str>, || {
1881 assert!(encryption.is_enabled());
1882 let data = b"fallback test data";
1883 let encrypted = encryption.encrypt(data).expect("encrypt");
1884 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
1885
1886 assert!(
1888 decrypt(&encrypted_str, "inline-fallback").is_ok(),
1889 "must encrypt with the inline passphrase when env is unset"
1890 );
1891 });
1892 }
1893
1894 #[test]
1898 fn state_encryption_is_enabled_false_when_enabled_but_no_passphrase() {
1899 let config = EncryptionConfig {
1900 enabled: true,
1901 passphrase: None,
1902 env_var: None,
1903 };
1904 let encryption = StateEncryption::new(config).expect("create");
1905 assert!(
1906 !encryption.is_enabled(),
1907 "enabled=true with no passphrase must report not-enabled"
1908 );
1909 }
1910
1911 #[test]
1914 #[serial]
1915 fn state_encryption_is_enabled_false_when_env_var_unset_and_no_inline() {
1916 let config = EncryptionConfig {
1917 enabled: true,
1918 passphrase: None,
1919 env_var: Some("SHIPPER_TEST_NOT_SET_AT_ALL_VAR".to_string()),
1920 };
1921 let encryption = StateEncryption::new(config).expect("create");
1922 temp_env::with_var("SHIPPER_TEST_NOT_SET_AT_ALL_VAR", None::<&str>, || {
1923 assert!(!encryption.is_enabled());
1924 });
1925 }
1926
1927 #[test]
1931 fn state_encryption_write_file_no_passphrase_writes_plaintext() {
1932 let td = tempdir().expect("tempdir");
1933 let path = td.path().join("nopass.txt");
1934
1935 let config = EncryptionConfig {
1936 enabled: true,
1937 passphrase: None,
1938 env_var: None,
1939 };
1940 let encryption = StateEncryption::new(config).expect("create");
1941
1942 let data = b"would-be-encrypted but no passphrase";
1943 encryption.write_file(&path, data).expect("write plaintext");
1944 let on_disk = std::fs::read(&path).expect("read");
1945 assert_eq!(
1946 on_disk,
1947 data.to_vec(),
1948 "with no passphrase, write must NOT encrypt and must NOT use a default key"
1949 );
1950 }
1951
1952 #[test]
1957 fn encryption_config_serde_roundtrip_env_var() {
1958 let cfg = EncryptionConfig::from_env("SHIPPER_TEST_SERDE_VAR".to_string());
1959 let json = serde_json::to_string(&cfg).expect("serialize");
1960 assert!(!json.contains("passphrase"));
1961 let de: EncryptionConfig = serde_json::from_str(&json).expect("deserialize");
1962 assert!(de.enabled);
1963 assert!(de.passphrase.is_none());
1964 assert_eq!(de.env_var.as_deref(), Some("SHIPPER_TEST_SERDE_VAR"));
1965 }
1966
1967 #[test]
1970 fn encryption_config_deserialize_empty_object_is_disabled() {
1971 let cfg: EncryptionConfig = serde_json::from_str("{}").expect("deserialize empty");
1972 assert!(!cfg.enabled);
1973 assert!(cfg.passphrase.is_none());
1974 assert!(cfg.env_var.is_none());
1975 }
1976
1977 #[test]
1983 fn pbkdf2_iteration_count_pinned_at_100k() {
1984 assert_eq!(
1985 PBKDF2_ITERATIONS, 100_000,
1986 "PBKDF2 iteration count is a security parameter; \
1987 do not change without explicit security review"
1988 );
1989 }
1990
1991 #[test]
1994 fn key_size_pinned_at_32_bytes() {
1995 assert_eq!(KEY_SIZE, 32, "AES-256 requires a 256-bit (32-byte) key");
1996 }
1997
1998 #[test]
2002 fn nonce_size_pinned_at_12_bytes() {
2003 assert_eq!(NONCE_SIZE, 12, "AES-GCM standard nonce size is 96 bits");
2004 }
2005
2006 #[test]
2010 fn salt_size_pinned_at_16_bytes() {
2011 assert_eq!(SALT_SIZE, 16, "PBKDF2 salt should be at least 128 bits");
2012 }
2013
2014 #[test]
2023 fn ciphertext_layout_is_salt_nonce_ciphertext_tag() {
2024 let plaintext = b"layout pin";
2025 let passphrase = "layout-pass";
2026 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
2027 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2028 let raw = BASE64.decode(&encrypted_str).expect("base64");
2029
2030 assert!(raw.len() >= SALT_SIZE + NONCE_SIZE + 16);
2032 let salt = &raw[..SALT_SIZE];
2033 let nonce_bytes = &raw[SALT_SIZE..SALT_SIZE + NONCE_SIZE];
2034 let ct_tag = &raw[SALT_SIZE + NONCE_SIZE..];
2035
2036 let key = derive_key(passphrase, salt);
2038 let cipher = Aes256Gcm::new_from_slice(&key).expect("cipher");
2039 let nonce = Nonce::from_slice(nonce_bytes);
2040 let decrypted = cipher
2041 .decrypt(nonce, ct_tag)
2042 .expect("manual decrypt should succeed if layout matches");
2043 assert_eq!(decrypted, plaintext);
2044
2045 assert_eq!(ct_tag.len(), plaintext.len() + 16);
2047 }
2048
2049 #[test]
2054 fn ciphertext_survives_base64_decode_reencode_cycle() {
2055 let plaintext = b"base64 stability pin";
2056 let passphrase = "b64-stability";
2057 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
2058 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2059
2060 let raw = BASE64.decode(&encrypted_str).expect("base64 decode");
2063 let reencoded = BASE64.encode(&raw);
2064 assert_eq!(reencoded, encrypted_str, "base64 round-trip must be stable");
2065
2066 let decrypted = decrypt(&reencoded, passphrase).expect("decrypt");
2067 assert_eq!(decrypted, plaintext);
2068 }
2069
2070 #[test]
2077 fn is_encrypted_rejects_one_byte_below_minimum() {
2078 let data = vec![0u8; SALT_SIZE + NONCE_SIZE + 15];
2079 let encoded = BASE64.encode(&data);
2080 assert!(!is_encrypted(&encoded));
2081 }
2082
2083 #[test]
2089 #[serial]
2090 fn state_encryption_env_var_decrypt_handles_encrypted_bytes() {
2091 let config = EncryptionConfig::from_env("SHIPPER_TEST_DEC_VAR".to_string());
2092 let encryption = StateEncryption::new(config).expect("create");
2093
2094 temp_env::with_var("SHIPPER_TEST_DEC_VAR", Some("dec-env-pass"), || {
2095 let data = b"env decrypt cycle";
2096 let encrypted = encryption.encrypt(data).expect("encrypt");
2097 let decrypted = encryption.decrypt(&encrypted).expect("decrypt");
2098 assert_eq!(decrypted, data);
2099 });
2100 }
2101
2102 #[test]
2109 fn mask_passphrase_uses_char_count_not_byte_count() {
2110 let masked = mask_passphrase("αβγ");
2112 assert_eq!(masked.chars().count(), 3);
2114 assert!(masked.starts_with('α'));
2115 assert!(masked.ends_with('γ'));
2116 assert!(masked.contains('*'));
2117 }
2118
2119 #[test]
2122 fn mask_passphrase_empty_produces_one_asterisk() {
2123 assert_eq!(mask_passphrase(""), "*");
2124 }
2125
2126 #[test]
2132 fn mask_passphrase_does_not_leak_middle_characters() {
2133 let secret = "DO-NOT-LEAK-MIDDLE";
2134 let masked = mask_passphrase(secret);
2135 let secret_chars: Vec<char> = secret.chars().collect();
2136 let masked_chars: Vec<char> = masked.chars().collect();
2137
2138 assert_eq!(masked_chars.len(), secret_chars.len());
2139 assert_eq!(masked_chars.first(), secret_chars.first());
2140 assert_eq!(masked_chars.last(), secret_chars.last());
2141 assert!(
2142 masked_chars[1..masked_chars.len() - 1]
2143 .iter()
2144 .all(|ch| *ch == '*'),
2145 "masked output {masked:?} leaked middle characters"
2146 );
2147 }
2148}
2149
2150#[cfg(test)]
2153mod proptests {
2154 use super::*;
2155 use proptest::prelude::*;
2156
2157 proptest! {
2158 #[test]
2159 fn roundtrip_arbitrary_data(data in proptest::collection::vec(any::<u8>(), 0..4096)) {
2160 let passphrase = "prop-test-pass";
2161 let encrypted = encrypt(&data, passphrase).expect("encrypt");
2162 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
2163 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
2164 prop_assert_eq!(data, decrypted);
2165 }
2166
2167 #[test]
2168 fn roundtrip_arbitrary_passphrase(passphrase in "\\PC{1,200}") {
2169 let plaintext = b"fixed plaintext for passphrase fuzz";
2170 let encrypted = encrypt(plaintext, &passphrase).expect("encrypt");
2171 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
2172 let decrypted = decrypt(&encrypted_str, &passphrase).expect("decrypt");
2173 prop_assert_eq!(plaintext.to_vec(), decrypted);
2174 }
2175
2176 #[test]
2177 fn roundtrip_arbitrary_data_and_passphrase(
2178 data in proptest::collection::vec(any::<u8>(), 0..1024),
2179 passphrase in "\\PC{1,100}",
2180 ) {
2181 let encrypted = encrypt(&data, &passphrase).expect("encrypt");
2182 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
2183 let decrypted = decrypt(&encrypted_str, &passphrase).expect("decrypt");
2184 prop_assert_eq!(data, decrypted);
2185 }
2186
2187 #[test]
2188 fn encrypted_output_is_valid_base64(data in proptest::collection::vec(any::<u8>(), 0..512)) {
2189 let encrypted = encrypt(&data, "test").expect("encrypt");
2190 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
2191 prop_assert!(BASE64.decode(&encrypted_str).is_ok());
2192 }
2193
2194 #[test]
2195 fn wrong_passphrase_always_fails(
2196 data in proptest::collection::vec(any::<u8>(), 1..512),
2197 correct in "[a-z]{8,16}",
2198 wrong in "[A-Z]{8,16}",
2199 ) {
2200 prop_assume!(correct != wrong);
2202 let encrypted = encrypt(&data, &correct).expect("encrypt");
2203 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
2204 prop_assert!(decrypt(&encrypted_str, &wrong).is_err());
2205 }
2206
2207 #[test]
2208 fn encrypted_size_is_deterministic(data in proptest::collection::vec(any::<u8>(), 0..2048)) {
2209 let encrypted = encrypt(&data, "pass").expect("encrypt");
2210 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
2211 let raw = BASE64.decode(&encrypted_str).expect("base64");
2212 let expected = SALT_SIZE + NONCE_SIZE + data.len() + 16;
2214 prop_assert_eq!(raw.len(), expected);
2215 }
2216
2217 #[test]
2218 fn is_encrypted_true_for_encrypt_output(data in proptest::collection::vec(any::<u8>(), 0..512)) {
2219 let encrypted = encrypt(&data, "test-pass").expect("encrypt");
2220 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
2221 prop_assert!(is_encrypted(&encrypted_str));
2222 }
2223
2224 #[test]
2225 fn is_encrypted_never_panics(s in "\\PC{0,500}") {
2226 let _ = is_encrypted(&s);
2227 }
2228
2229 #[test]
2230 fn decrypt_arbitrary_string_never_panics(s in "\\PC{0,500}") {
2231 let _ = decrypt(&s, "passphrase");
2232 }
2233
2234 #[test]
2235 fn encrypt_output_is_always_utf8(
2236 data in proptest::collection::vec(any::<u8>(), 0..1024),
2237 passphrase in "\\PC{1,50}",
2238 ) {
2239 let encrypted = encrypt(&data, &passphrase).expect("encrypt");
2240 prop_assert!(String::from_utf8(encrypted).is_ok());
2241 }
2242
2243 #[test]
2244 fn each_encrypt_produces_unique_ciphertext(data in proptest::collection::vec(any::<u8>(), 0..256)) {
2245 let a = encrypt(&data, "same-pass").expect("encrypt");
2246 let b = encrypt(&data, "same-pass").expect("encrypt");
2247 prop_assert_ne!(a, b);
2248 }
2249
2250 #[test]
2251 fn encryption_config_serde_roundtrip_arbitrary(passphrase in "\\PC{1,100}") {
2252 let cfg = EncryptionConfig::new(passphrase.clone());
2253 let json = serde_json::to_string(&cfg).expect("serialize");
2254 let de: EncryptionConfig = serde_json::from_str(&json).expect("deserialize");
2255 prop_assert_eq!(de.enabled, true);
2256 prop_assert_eq!(de.passphrase.as_deref(), Some(passphrase.as_str()));
2257 }
2258
2259 #[test]
2260 fn state_encryption_roundtrip_arbitrary(data in proptest::collection::vec(any::<u8>(), 0..1024)) {
2261 let config = EncryptionConfig::new("state-prop-pass".to_string());
2262 let se = StateEncryption::new(config).expect("create");
2263 let encrypted = se.encrypt(&data).expect("encrypt");
2264 let decrypted = se.decrypt(&encrypted).expect("decrypt");
2265 prop_assert_eq!(data, decrypted);
2266 }
2267
2268 #[test]
2269 fn encryption_output_always_longer_than_input(data in proptest::collection::vec(any::<u8>(), 0..2048)) {
2270 let encrypted = encrypt(&data, "length-test").expect("encrypt");
2271 prop_assert!(encrypted.len() > data.len());
2273 }
2274
2275 #[test]
2276 fn tampered_ciphertext_always_fails(data in proptest::collection::vec(any::<u8>(), 1..512)) {
2277 let passphrase = "tamper-prop-test";
2278 let encrypted = encrypt(&data, passphrase).expect("encrypt");
2279 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2280
2281 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
2282 let idx = SALT_SIZE + NONCE_SIZE + (raw.len() - SALT_SIZE - NONCE_SIZE) / 2;
2284 raw[idx] ^= 0xFF;
2285 let corrupted = BASE64.encode(&raw);
2286
2287 prop_assert!(decrypt(&corrupted, passphrase).is_err());
2288 }
2289
2290 #[test]
2291 fn derive_key_always_produces_32_bytes_prop(
2292 passphrase in "\\PC{0,200}",
2293 salt in proptest::collection::vec(any::<u8>(), 0..64),
2294 ) {
2295 let key = derive_key(&passphrase, &salt);
2296 prop_assert_eq!(key.len(), KEY_SIZE);
2297 }
2298
2299 #[test]
2300 fn decrypt_truncated_ciphertext_always_fails_prop(
2301 data in proptest::collection::vec(any::<u8>(), 1..512),
2302 trim in 1usize..17,
2303 ) {
2304 let passphrase = "truncation-prop";
2305 let encrypted = encrypt(&data, passphrase).expect("encrypt");
2306 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2307 let raw = BASE64.decode(&encrypted_str).expect("base64");
2308
2309 if raw.len() > SALT_SIZE + NONCE_SIZE + 16 {
2311 let truncated = &raw[..raw.len() - trim];
2312 if truncated.len() >= SALT_SIZE + NONCE_SIZE + 16 {
2313 let encoded = BASE64.encode(truncated);
2314 prop_assert!(decrypt(&encoded, passphrase).is_err());
2315 }
2316 }
2317 }
2318
2319 #[test]
2320 fn derive_key_deterministic_prop(
2321 passphrase in "\\PC{0,100}",
2322 salt in proptest::collection::vec(any::<u8>(), 0..32),
2323 ) {
2324 let key1 = derive_key(&passphrase, &salt);
2325 let key2 = derive_key(&passphrase, &salt);
2326 prop_assert_eq!(key1, key2, "derive_key must be deterministic");
2327 }
2328
2329 #[test]
2330 fn salt_differs_across_encryptions_prop(data in proptest::collection::vec(any::<u8>(), 0..256)) {
2331 let a = encrypt(&data, "same-pass").expect("encrypt");
2332 let b = encrypt(&data, "same-pass").expect("encrypt");
2333 let raw_a = BASE64.decode(String::from_utf8(a).expect("utf8")).expect("base64");
2334 let raw_b = BASE64.decode(String::from_utf8(b).expect("utf8")).expect("base64");
2335 let salt_a = &raw_a[..SALT_SIZE];
2336 let salt_b = &raw_b[..SALT_SIZE];
2337 prop_assert_ne!(salt_a.to_vec(), salt_b.to_vec(), "salts must differ");
2338 }
2339
2340 #[test]
2341 fn double_encrypt_roundtrip_prop(data in proptest::collection::vec(any::<u8>(), 0..256)) {
2342 let pass1 = "layer-one";
2343 let pass2 = "layer-two";
2344 let enc1 = encrypt(&data, pass1).expect("encrypt 1");
2345 let enc2 = encrypt(&enc1, pass2).expect("encrypt 2");
2346 let enc2_str = String::from_utf8(enc2).expect("utf8");
2347 let dec2 = decrypt(&enc2_str, pass2).expect("decrypt 2");
2348 let dec2_str = String::from_utf8(dec2).expect("utf8");
2349 let dec1 = decrypt(&dec2_str, pass1).expect("decrypt 1");
2350 prop_assert_eq!(data, dec1);
2351 }
2352 }
2353}
2354
2355#[cfg(test)]
2358mod snapshot_tests {
2359 use super::*;
2360 use insta::{assert_debug_snapshot, assert_snapshot};
2361
2362 #[test]
2365 fn config_default_json() {
2366 let cfg = EncryptionConfig::default();
2367 let json = serde_json::to_string_pretty(&cfg).expect("serialize");
2368 assert_snapshot!(json);
2369 }
2370
2371 #[test]
2372 fn config_with_passphrase_json() {
2373 let cfg = EncryptionConfig::new("my-secret".to_string());
2374 let json = serde_json::to_string_pretty(&cfg).expect("serialize");
2375 assert_snapshot!(json);
2376 }
2377
2378 #[test]
2379 fn config_with_env_var_json() {
2380 let cfg = EncryptionConfig::from_env("SHIPPER_ENCRYPT_KEY".to_string());
2381 let json = serde_json::to_string_pretty(&cfg).expect("serialize");
2382 assert_snapshot!(json);
2383 }
2384
2385 #[test]
2386 fn config_enabled_no_passphrase_json() {
2387 let cfg = EncryptionConfig {
2388 enabled: true,
2389 passphrase: None,
2390 env_var: None,
2391 };
2392 let json = serde_json::to_string_pretty(&cfg).expect("serialize");
2393 assert_snapshot!(json);
2394 }
2395
2396 #[test]
2397 fn config_with_both_passphrase_and_env_json() {
2398 let cfg = EncryptionConfig {
2399 enabled: true,
2400 passphrase: Some("inline-pass".to_string()),
2401 env_var: Some("SHIPPER_ENCRYPT_KEY".to_string()),
2402 };
2403 let json = serde_json::to_string_pretty(&cfg).expect("serialize");
2404 assert_snapshot!(json);
2405 }
2406
2407 #[test]
2410 fn mask_passphrase_normal() {
2411 assert_snapshot!(mask_passphrase("my-secret-passphrase"));
2412 }
2413
2414 #[test]
2415 fn mask_passphrase_short_three_chars() {
2416 assert_snapshot!(mask_passphrase("abc"));
2417 }
2418
2419 #[test]
2420 fn mask_passphrase_two_chars() {
2421 assert_snapshot!(mask_passphrase("ab"));
2422 }
2423
2424 #[test]
2425 fn mask_passphrase_single_char() {
2426 assert_snapshot!(mask_passphrase("x"));
2427 }
2428
2429 #[test]
2430 fn mask_passphrase_empty() {
2431 assert_snapshot!(mask_passphrase(""));
2432 }
2433
2434 #[test]
2435 fn mask_passphrase_unicode() {
2436 assert_snapshot!(mask_passphrase("🔑secret🔒"));
2437 }
2438
2439 #[test]
2442 fn display_config_disabled() {
2443 let cfg = EncryptionConfig::default();
2444 assert_snapshot!(cfg.to_string());
2445 }
2446
2447 #[test]
2448 fn display_config_with_passphrase() {
2449 let cfg = EncryptionConfig::new("super-secret-key".to_string());
2450 assert_snapshot!(cfg.to_string());
2451 }
2452
2453 #[test]
2454 fn display_config_with_env_var() {
2455 let cfg = EncryptionConfig::from_env("SHIPPER_ENCRYPT_KEY".to_string());
2456 assert_snapshot!(cfg.to_string());
2457 }
2458
2459 #[test]
2460 fn display_config_enabled_no_source() {
2461 let cfg = EncryptionConfig {
2462 enabled: true,
2463 passphrase: None,
2464 env_var: None,
2465 };
2466 assert_snapshot!(cfg.to_string());
2467 }
2468
2469 #[test]
2470 fn display_state_encryption_wrapper() {
2471 let cfg = EncryptionConfig::new("my-passphrase".to_string());
2472 let se = StateEncryption::new(cfg).expect("create");
2473 assert_snapshot!(se.to_string());
2474 }
2475
2476 #[test]
2479 fn error_invalid_base64() {
2480 let err = decrypt("not-valid-base64!!!", "pass").unwrap_err();
2481 assert_snapshot!(err.to_string());
2482 }
2483
2484 #[test]
2485 fn error_data_too_short() {
2486 let short = BASE64.encode(vec![0u8; SALT_SIZE + NONCE_SIZE + 15]);
2487 let err = decrypt(&short, "pass").unwrap_err();
2488 assert_snapshot!(err.to_string());
2489 }
2490
2491 #[test]
2492 fn error_wrong_passphrase() {
2493 let encrypted = encrypt(b"secret data", "correct-pass").expect("encrypt");
2494 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2495 let err = decrypt(&encrypted_str, "wrong-pass").unwrap_err();
2496 assert_snapshot!(err.to_string());
2497 }
2498
2499 #[test]
2500 fn error_corrupted_ciphertext() {
2501 let encrypted = encrypt(b"data", "pass").expect("encrypt");
2502 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2503 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
2504 raw[SALT_SIZE + NONCE_SIZE + 1] ^= 0xFF;
2505 let corrupted = BASE64.encode(&raw);
2506 let err = decrypt(&corrupted, "pass").unwrap_err();
2507 assert_snapshot!(err.to_string());
2508 }
2509
2510 #[test]
2511 fn error_empty_input() {
2512 let err = decrypt("", "pass").unwrap_err();
2513 assert_snapshot!(err.to_string());
2514 }
2515
2516 #[test]
2519 fn error_corrupted_salt_message() {
2520 let encrypted = encrypt(b"snapshot salt", "pass").expect("encrypt");
2521 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2522 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
2523 raw[0] ^= 0xFF;
2524 let corrupted = BASE64.encode(&raw);
2525 let err = decrypt(&corrupted, "pass").unwrap_err();
2526 assert_snapshot!(err.to_string());
2527 }
2528
2529 #[test]
2530 fn error_corrupted_nonce_message() {
2531 let encrypted = encrypt(b"snapshot nonce", "pass").expect("encrypt");
2532 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2533 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
2534 raw[SALT_SIZE] ^= 0xFF;
2535 let corrupted = BASE64.encode(&raw);
2536 let err = decrypt(&corrupted, "pass").unwrap_err();
2537 assert_snapshot!(err.to_string());
2538 }
2539
2540 #[test]
2541 fn error_corrupted_auth_tag_message() {
2542 let encrypted = encrypt(b"snapshot tag", "pass").expect("encrypt");
2543 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2544 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
2545 let last = raw.len() - 1;
2546 raw[last] ^= 0xFF;
2547 let corrupted = BASE64.encode(&raw);
2548 let err = decrypt(&corrupted, "pass").unwrap_err();
2549 assert_snapshot!(err.to_string());
2550 }
2551
2552 #[test]
2555 fn snapshot_derive_key_output_format() {
2556 let key = derive_key("test-passphrase", &[0u8; SALT_SIZE]);
2557 let hex: String = key.iter().map(|b| format!("{b:02x}")).collect();
2559 assert_snapshot!(hex);
2560 }
2561
2562 #[test]
2563 fn snapshot_derive_key_length() {
2564 let key = derive_key("any-passphrase", &[42u8; SALT_SIZE]);
2565 assert_debug_snapshot!(key.len());
2566 }
2567
2568 #[test]
2571 fn snapshot_encryption_config_debug_default() {
2572 let cfg = EncryptionConfig::default();
2573 assert_debug_snapshot!(cfg);
2574 }
2575
2576 #[test]
2577 fn snapshot_encryption_config_debug_with_passphrase() {
2578 let cfg = EncryptionConfig::new("debug-pass".to_string());
2579 assert_debug_snapshot!(cfg);
2580 }
2581
2582 #[test]
2583 fn snapshot_encryption_config_debug_from_env() {
2584 let cfg = EncryptionConfig::from_env("MY_SECRET_VAR".to_string());
2585 assert_debug_snapshot!(cfg);
2586 }
2587
2588 #[test]
2591 fn snapshot_encrypted_data_component_sizes() {
2592 let plaintext = b"snapshot-structure-test";
2593 let encrypted = encrypt(plaintext, "snap-pass").expect("encrypt");
2594 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2595 let raw = BASE64.decode(&encrypted_str).expect("base64");
2596
2597 let info = format!(
2598 "salt_bytes={}, nonce_bytes={}, ciphertext_plus_tag_bytes={}, plaintext_len={}, overhead={}",
2599 SALT_SIZE,
2600 NONCE_SIZE,
2601 raw.len() - SALT_SIZE - NONCE_SIZE,
2602 plaintext.len(),
2603 raw.len() - plaintext.len(),
2604 );
2605 assert_snapshot!(info);
2606 }
2607
2608 #[test]
2609 fn snapshot_derive_key_alternate_passphrase() {
2610 let key = derive_key("alternate-passphrase-for-snapshot", &[0xAB; SALT_SIZE]);
2611 let hex: String = key.iter().map(|b| format!("{b:02x}")).collect();
2612 assert_snapshot!(hex);
2613 }
2614
2615 #[test]
2616 fn snapshot_is_encrypted_results() {
2617 let results = format!(
2618 "empty={}, json={}, short_b64={}, garbage={}",
2619 is_encrypted(""),
2620 is_encrypted(r#"{"key":"value"}"#),
2621 is_encrypted(&BASE64.encode(vec![0u8; 10])),
2622 is_encrypted("!!!not-base64!!!"),
2623 );
2624 assert_snapshot!(results);
2625 }
2626
2627 #[test]
2630 fn snapshot_state_encryption_no_passphrase_error() {
2631 let config = EncryptionConfig {
2632 enabled: true,
2633 passphrase: None,
2634 env_var: None,
2635 };
2636 let encryption = StateEncryption::new(config).expect("create");
2637 let err = encryption.encrypt(b"data").unwrap_err();
2638 assert_snapshot!(err.to_string());
2639 }
2640
2641 #[test]
2644 fn snapshot_display_config_with_both_sources() {
2645 let cfg = EncryptionConfig {
2646 enabled: true,
2647 passphrase: Some("inline-secret".to_string()),
2648 env_var: Some("SHIPPER_KEY".to_string()),
2649 };
2650 assert_snapshot!(cfg.to_string());
2651 }
2652
2653 #[test]
2656 fn snapshot_mask_passphrase_four_chars() {
2657 assert_snapshot!(mask_passphrase("abcd"));
2658 }
2659
2660 #[test]
2661 fn snapshot_mask_passphrase_with_spaces() {
2662 assert_snapshot!(mask_passphrase("a b c d"));
2663 }
2664
2665 #[test]
2666 fn snapshot_mask_passphrase_with_newline() {
2667 assert_snapshot!(mask_passphrase("pass\nword"));
2668 }
2669
2670 #[test]
2673 fn snapshot_encrypted_empty_plaintext_structure() {
2674 let encrypted = encrypt(b"", "snap-pass").expect("encrypt");
2675 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2676 let raw = BASE64.decode(&encrypted_str).expect("base64");
2677
2678 let info = format!(
2679 "raw_len={}, salt={}, nonce={}, ciphertext_plus_tag={}, plaintext_len=0",
2680 raw.len(),
2681 SALT_SIZE,
2682 NONCE_SIZE,
2683 raw.len() - SALT_SIZE - NONCE_SIZE,
2684 );
2685 assert_snapshot!(info);
2686 }
2687}