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 tempfile::tempdir;
410
411 #[test]
414 fn encrypt_decrypt_roundtrip() {
415 let plaintext = b"Hello, World! This is a test message.";
416 let passphrase = "test-passphrase-123";
417
418 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
419 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
420 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
421
422 assert_eq!(plaintext.to_vec(), decrypted);
423 }
424
425 #[test]
426 fn encrypt_produces_different_output_for_same_plaintext() {
427 let plaintext = b"Hello, World!";
428 let passphrase = "test-passphrase";
429
430 let encrypted1 = encrypt(plaintext, passphrase).expect("encryption should succeed");
431 let encrypted2 = encrypt(plaintext, passphrase).expect("encryption should succeed");
432
433 assert_ne!(encrypted1, encrypted2);
435
436 let decrypted1 = decrypt(
438 String::from_utf8(encrypted1).expect("valid UTF-8"),
439 passphrase,
440 )
441 .expect("decryption should succeed");
442 let decrypted2 = decrypt(
443 String::from_utf8(encrypted2).expect("valid UTF-8"),
444 passphrase,
445 )
446 .expect("decryption should succeed");
447
448 assert_eq!(decrypted1, decrypted2);
449 }
450
451 #[test]
452 fn decrypt_wrong_passphrase_fails() {
453 let plaintext = b"Secret data";
454 let passphrase = "correct-passphrase";
455 let wrong_passphrase = "wrong-passphrase";
456
457 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
458 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
459
460 let result = decrypt(&encrypted_str, wrong_passphrase);
461 assert!(result.is_err());
462 }
463
464 #[test]
467 fn encrypt_decrypt_empty_input() {
468 let plaintext = b"";
469 let passphrase = "test-passphrase";
470
471 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
472 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
473 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
474
475 assert_eq!(plaintext.to_vec(), decrypted);
476 }
477
478 #[test]
479 fn encrypt_empty_with_empty_passphrase() {
480 let plaintext = b"";
481 let passphrase = "";
482
483 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
484 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
485 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
486
487 assert_eq!(plaintext.to_vec(), decrypted);
488 }
489
490 #[test]
493 fn encrypt_decrypt_large_input() {
494 let plaintext: Vec<u8> = (0..1_048_576).map(|i| (i % 256) as u8).collect();
496 let passphrase = "large-data-passphrase";
497
498 let encrypted = encrypt(&plaintext, passphrase).expect("encryption should succeed");
499 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
500 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
501
502 assert_eq!(plaintext, decrypted);
503 }
504
505 #[test]
506 fn encrypt_decrypt_single_byte() {
507 let plaintext = b"\x42";
508 let passphrase = "single-byte";
509
510 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
511 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
512 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
513
514 assert_eq!(plaintext.to_vec(), decrypted);
515 }
516
517 #[test]
520 fn decrypt_invalid_base64_fails() {
521 let result = decrypt("not-valid-base64!!!", "passphrase");
522 assert!(result.is_err());
523 let err = result.unwrap_err().to_string();
524 assert!(
525 err.contains("base64"),
526 "error should mention base64, got: {err}"
527 );
528 }
529
530 #[test]
531 fn decrypt_too_short_data_fails() {
532 let short_data = vec![0u8; SALT_SIZE + NONCE_SIZE + 15];
534 let encoded = BASE64.encode(&short_data);
535
536 let result = decrypt(&encoded, "passphrase");
537 assert!(result.is_err());
538 let err = result.unwrap_err().to_string();
539 assert!(
540 err.contains("too short"),
541 "error should mention 'too short', got: {err}"
542 );
543 }
544
545 #[test]
546 fn decrypt_corrupted_ciphertext_fails() {
547 let plaintext = b"Some data to encrypt";
548 let passphrase = "test-pass";
549
550 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
551 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
552
553 let mut raw = BASE64.decode(&encrypted_str).expect("valid base64");
555 let idx = SALT_SIZE + NONCE_SIZE + 1;
556 raw[idx] ^= 0xFF;
557 let corrupted = BASE64.encode(&raw);
558
559 let result = decrypt(&corrupted, passphrase);
560 assert!(result.is_err());
561 }
562
563 #[test]
564 fn decrypt_corrupted_salt_fails() {
565 let plaintext = b"Some data";
566 let passphrase = "test-pass";
567
568 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
569 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
570
571 let mut raw = BASE64.decode(&encrypted_str).expect("valid base64");
573 raw[0] ^= 0xFF;
574 let corrupted = BASE64.encode(&raw);
575
576 let result = decrypt(&corrupted, passphrase);
577 assert!(result.is_err());
578 }
579
580 #[test]
581 fn decrypt_corrupted_nonce_fails() {
582 let plaintext = b"Some data";
583 let passphrase = "test-pass";
584
585 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
586 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
587
588 let mut raw = BASE64.decode(&encrypted_str).expect("valid base64");
590 raw[SALT_SIZE] ^= 0xFF;
591 let corrupted = BASE64.encode(&raw);
592
593 let result = decrypt(&corrupted, passphrase);
594 assert!(result.is_err());
595 }
596
597 #[test]
598 fn decrypt_empty_string_fails() {
599 let result = decrypt("", "passphrase");
600 assert!(result.is_err());
601 }
602
603 #[test]
604 fn decrypt_exactly_minimum_length_minus_one_fails() {
605 let data = vec![0u8; SALT_SIZE + NONCE_SIZE + 15];
607 let encoded = BASE64.encode(&data);
608 assert!(decrypt(&encoded, "pass").is_err());
609 }
610
611 #[test]
612 fn decrypt_exactly_minimum_length_fails_with_wrong_key() {
613 let data = vec![0u8; SALT_SIZE + NONCE_SIZE + 16];
615 let encoded = BASE64.encode(&data);
616 assert!(decrypt(&encoded, "pass").is_err());
618 }
619
620 #[test]
623 fn is_encrypted_detects_encrypted_data() {
624 let plaintext = b"Hello, World!";
625 let passphrase = "test-passphrase";
626
627 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
628 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
629
630 assert!(is_encrypted(&encrypted_str));
631 }
632
633 #[test]
634 fn is_encrypted_rejects_plaintext() {
635 let plaintext = r#"{"key": "value"}"#;
636 assert!(!is_encrypted(plaintext));
637 }
638
639 #[test]
640 fn is_encrypted_rejects_empty_string() {
641 assert!(!is_encrypted(""));
642 }
643
644 #[test]
645 fn is_encrypted_rejects_short_base64() {
646 let short = BASE64.encode(vec![0u8; SALT_SIZE + NONCE_SIZE + 10]);
648 assert!(!is_encrypted(&short));
649 }
650
651 #[test]
652 fn is_encrypted_rejects_non_base64() {
653 assert!(!is_encrypted("definitely not base64 $$$ !!!"));
654 }
655
656 #[test]
659 fn roundtrip_with_unicode_passphrase() {
660 let plaintext = b"Unicode passphrase test";
661 let passphrase = "pässwörd-密码-🔑";
662
663 let encrypted = encrypt(plaintext, passphrase).expect("encryption should succeed");
664 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
665 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
666
667 assert_eq!(plaintext.to_vec(), decrypted);
668 }
669
670 #[test]
671 fn roundtrip_with_very_long_passphrase() {
672 let plaintext = b"Long passphrase test";
673 let passphrase: String = "a".repeat(10_000);
674
675 let encrypted = encrypt(plaintext, &passphrase).expect("encryption should succeed");
676 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
677 let decrypted = decrypt(&encrypted_str, &passphrase).expect("decryption should succeed");
678
679 assert_eq!(plaintext.to_vec(), decrypted);
680 }
681
682 #[test]
683 fn different_passphrases_produce_different_ciphertexts_when_decoded() {
684 let plaintext = b"Same plaintext";
685 let pass1 = "passphrase-one";
686 let pass2 = "passphrase-two";
687
688 let enc1 = encrypt(plaintext, pass1).expect("encrypt");
689 let enc2 = encrypt(plaintext, pass2).expect("encrypt");
690
691 let raw1 = BASE64
693 .decode(String::from_utf8(enc1).expect("utf8"))
694 .expect("base64");
695 let raw2 = BASE64
696 .decode(String::from_utf8(enc2).expect("utf8"))
697 .expect("base64");
698
699 let ct1 = &raw1[SALT_SIZE + NONCE_SIZE..];
701 let ct2 = &raw2[SALT_SIZE + NONCE_SIZE..];
702 assert_ne!(ct1, ct2);
703 }
704
705 #[test]
708 fn roundtrip_binary_data() {
709 let plaintext: Vec<u8> = (0..=255).collect();
710 let passphrase = "binary-data-test";
711
712 let encrypted = encrypt(&plaintext, passphrase).expect("encrypt");
713 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
714 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
715
716 assert_eq!(plaintext, decrypted);
717 }
718
719 #[test]
720 fn roundtrip_all_zero_bytes() {
721 let plaintext = vec![0u8; 1024];
722 let passphrase = "zeroes";
723
724 let encrypted = encrypt(&plaintext, passphrase).expect("encrypt");
725 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
726 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
727
728 assert_eq!(plaintext, decrypted);
729 }
730
731 #[test]
734 fn derive_key_produces_consistent_output() {
735 let passphrase = "test-passphrase";
736 let salt = [0u8; SALT_SIZE];
737
738 let key1 = derive_key(passphrase, &salt);
739 let key2 = derive_key(passphrase, &salt);
740
741 assert_eq!(key1, key2);
742 }
743
744 #[test]
745 fn derive_key_different_salts_produce_different_keys() {
746 let passphrase = "test-passphrase";
747 let salt1 = [0u8; SALT_SIZE];
748 let mut salt2 = [0u8; SALT_SIZE];
749 salt2[0] = 1;
750
751 let key1 = derive_key(passphrase, &salt1);
752 let key2 = derive_key(passphrase, &salt2);
753
754 assert_ne!(key1, key2);
755 }
756
757 #[test]
758 fn derive_key_different_passphrases_produce_different_keys() {
759 let salt = [42u8; SALT_SIZE];
760
761 let key1 = derive_key("passphrase-a", &salt);
762 let key2 = derive_key("passphrase-b", &salt);
763
764 assert_ne!(key1, key2);
765 }
766
767 #[test]
768 fn derive_key_empty_passphrase() {
769 let salt = [0u8; SALT_SIZE];
770 let key1 = derive_key("", &salt);
772 let key2 = derive_key("", &salt);
773 assert_eq!(key1, key2);
774 }
775
776 #[test]
779 fn encryption_config_default_is_disabled() {
780 let cfg = EncryptionConfig::default();
781 assert!(!cfg.enabled);
782 assert!(cfg.passphrase.is_none());
783 assert!(cfg.env_var.is_none());
784 }
785
786 #[test]
787 fn encryption_config_new_is_enabled() {
788 let cfg = EncryptionConfig::new("secret".to_string());
789 assert!(cfg.enabled);
790 assert_eq!(cfg.passphrase.as_deref(), Some("secret"));
791 assert!(cfg.env_var.is_none());
792 }
793
794 #[test]
795 fn encryption_config_from_env_is_enabled() {
796 let cfg = EncryptionConfig::from_env("MY_VAR".to_string());
797 assert!(cfg.enabled);
798 assert!(cfg.passphrase.is_none());
799 assert_eq!(cfg.env_var.as_deref(), Some("MY_VAR"));
800 }
801
802 #[test]
803 fn encryption_config_get_passphrase_direct() {
804 let cfg = EncryptionConfig::new("hello".to_string());
805 assert_eq!(cfg.get_passphrase().unwrap(), Some("hello".to_string()));
806 }
807
808 #[test]
809 fn encryption_config_get_passphrase_none_when_disabled() {
810 let cfg = EncryptionConfig::default();
811 assert_eq!(cfg.get_passphrase().unwrap(), None);
812 }
813
814 #[test]
815 fn encryption_config_serde_roundtrip() {
816 let cfg = EncryptionConfig::new("test".to_string());
817 let json = serde_json::to_string(&cfg).expect("serialize");
818 let deserialized: EncryptionConfig = serde_json::from_str(&json).expect("deserialize");
819 assert_eq!(deserialized.enabled, cfg.enabled);
820 assert_eq!(deserialized.passphrase, cfg.passphrase);
821 }
822
823 #[test]
824 fn encryption_config_serde_skips_none_fields() {
825 let cfg = EncryptionConfig::default();
826 let json = serde_json::to_string(&cfg).expect("serialize");
827 assert!(!json.contains("passphrase"));
828 assert!(!json.contains("env_var"));
829 }
830
831 #[test]
834 fn state_encryption_enabled_disabled() {
835 let config = EncryptionConfig::default();
836 let encryption = StateEncryption::new(config.clone()).expect("should create");
837 assert!(!encryption.is_enabled());
838
839 let config = EncryptionConfig::new("test-passphrase".to_string());
840 let encryption = StateEncryption::new(config).expect("should create");
841 assert!(encryption.is_enabled());
842 }
843
844 #[test]
845 fn state_encryption_roundtrip() {
846 let config = EncryptionConfig::new("my-secret-passphrase".to_string());
847 let encryption = StateEncryption::new(config).expect("should create");
848
849 let data = b"Test state data";
850
851 let encrypted = encryption.encrypt(data).expect("encryption should succeed");
852 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
853 let decrypted =
854 decrypt(&encrypted_str, "my-secret-passphrase").expect("decryption should succeed");
855
856 assert_eq!(data.to_vec(), decrypted);
857 }
858
859 #[test]
860 fn state_encryption_decrypt_roundtrip() {
861 let config = EncryptionConfig::new("my-pass".to_string());
862 let encryption = StateEncryption::new(config).expect("should create");
863
864 let data = b"state data to encrypt";
865 let encrypted = encryption.encrypt(data).expect("encrypt");
866 let decrypted = encryption.decrypt(&encrypted).expect("decrypt");
867
868 assert_eq!(data.to_vec(), decrypted);
869 }
870
871 #[test]
872 fn state_encryption_disabled_passthrough() {
873 let config = EncryptionConfig::default();
874 let encryption = StateEncryption::new(config).expect("should create");
875
876 let data = b"Plain text data";
877
878 let result = encryption.decrypt(data).expect("should succeed");
879 assert_eq!(data.to_vec(), result);
880 }
881
882 #[test]
883 fn state_encryption_disabled_encrypt_passthrough_on_decrypt() {
884 let config = EncryptionConfig::default();
886 let encryption = StateEncryption::new(config).expect("should create");
887
888 let garbage = b"\x00\x01\x02\x03";
889 let result = encryption.decrypt(garbage).expect("should succeed");
890 assert_eq!(garbage.to_vec(), result);
891 }
892
893 #[test]
896 fn encrypt_produces_valid_base64() {
897 let plaintext = b"Test data";
898 let passphrase = "test";
899
900 let encrypted = encrypt(plaintext, passphrase).expect("should encrypt");
901 let encrypted_str = String::from_utf8(encrypted.clone()).expect("valid UTF-8");
902
903 let decoded = BASE64.decode(&encrypted_str).expect("valid base64");
904 assert!(decoded.len() > plaintext.len());
905 }
906
907 #[test]
908 fn encrypted_output_has_expected_structure() {
909 let plaintext = b"Hello";
910 let passphrase = "test";
911
912 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
913 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
914 let raw = BASE64.decode(&encrypted_str).expect("base64");
915
916 let expected_len = SALT_SIZE + NONCE_SIZE + plaintext.len() + 16;
918 assert_eq!(raw.len(), expected_len);
919 }
920
921 #[test]
924 fn read_write_encrypted_file() {
925 let td = tempdir().expect("tempdir");
926 let path = td.path().join("test.enc");
927
928 let plaintext = b"Secret file content";
929 let passphrase = "file-passphrase";
930
931 write_encrypted(&path, plaintext, passphrase).expect("write encrypted");
932 let decrypted = read_decrypted(&path, passphrase).expect("read decrypted");
933
934 assert_eq!(plaintext.to_vec(), decrypted.into_bytes());
935 }
936
937 #[test]
938 fn read_decrypted_wrong_passphrase_fails() {
939 let td = tempdir().expect("tempdir");
940 let path = td.path().join("test.enc");
941
942 write_encrypted(&path, b"data", "correct").expect("write");
943 let result = read_decrypted(&path, "wrong");
944 assert!(result.is_err());
945 }
946
947 #[test]
948 fn read_decrypted_nonexistent_file_fails() {
949 let td = tempdir().expect("tempdir");
950 let path = td.path().join("does-not-exist.enc");
951
952 let result = read_decrypted(&path, "pass");
953 assert!(result.is_err());
954 }
955
956 #[test]
957 fn write_encrypted_file_is_base64_on_disk() {
958 let td = tempdir().expect("tempdir");
959 let path = td.path().join("test.enc");
960
961 write_encrypted(&path, b"data", "pass").expect("write");
962 let on_disk = std::fs::read_to_string(&path).expect("read");
963
964 assert!(BASE64.decode(&on_disk).is_ok());
966 assert_ne!(on_disk, "data");
968 }
969
970 #[test]
971 fn state_encryption_file_roundtrip() {
972 let td = tempdir().expect("tempdir");
973 let path = td.path().join("state.json");
974
975 let config = EncryptionConfig::new("test-pass".to_string());
976 let encryption = StateEncryption::new(config).expect("should create");
977
978 let data = br#"{"key": "value"}"#;
979
980 encryption.write_file(&path, data).expect("write file");
981 let content = encryption.read_file(&path).expect("read file");
982
983 assert_eq!(String::from_utf8_lossy(data), content);
984 }
985
986 #[test]
987 fn state_encryption_unencrypted_fallback() {
988 let td = tempdir().expect("tempdir");
989 let path = td.path().join("plain.json");
990
991 let config = EncryptionConfig::new("test-pass".to_string());
992 let encryption = StateEncryption::new(config).expect("should create");
993
994 let data = r#"{"plain": "data"}"#;
996 std::fs::write(&path, data).expect("write plain");
997
998 let content = encryption.read_file(&path).expect("read file");
1000 assert_eq!(data, content);
1001 }
1002
1003 #[test]
1004 fn state_encryption_disabled_writes_plaintext() {
1005 let td = tempdir().expect("tempdir");
1006 let path = td.path().join("plain.json");
1007
1008 let config = EncryptionConfig::default();
1009 let encryption = StateEncryption::new(config).expect("create");
1010
1011 let data = b"plain text content";
1012 encryption.write_file(&path, data).expect("write");
1013
1014 let on_disk = std::fs::read(&path).expect("read");
1015 assert_eq!(data.to_vec(), on_disk);
1016 }
1017
1018 #[test]
1019 fn state_encryption_disabled_reads_plaintext() {
1020 let td = tempdir().expect("tempdir");
1021 let path = td.path().join("plain.txt");
1022 std::fs::write(&path, "hello").expect("write");
1023
1024 let config = EncryptionConfig::default();
1025 let encryption = StateEncryption::new(config).expect("create");
1026
1027 let content = encryption.read_file(&path).expect("read");
1028 assert_eq!(content, "hello");
1029 }
1030
1031 #[test]
1032 fn state_encryption_read_nonexistent_file_fails() {
1033 let td = tempdir().expect("tempdir");
1034 let path = td.path().join("nope.json");
1035
1036 let config = EncryptionConfig::new("pass".to_string());
1037 let encryption = StateEncryption::new(config).expect("create");
1038
1039 assert!(encryption.read_file(&path).is_err());
1040 }
1041
1042 #[test]
1045 fn encrypt_decrypt_data_over_1mb() {
1046 let plaintext: Vec<u8> = (0u64..2_097_152)
1048 .map(|i| (i.wrapping_mul(7) % 256) as u8)
1049 .collect();
1050 let passphrase = "large-2mb-passphrase";
1051
1052 let encrypted = encrypt(&plaintext, passphrase).expect("encryption should succeed");
1053 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1054 let decrypted = decrypt(&encrypted_str, passphrase).expect("decryption should succeed");
1055
1056 assert_eq!(plaintext, decrypted);
1057 }
1058
1059 #[test]
1062 fn roundtrip_single_char_passphrase() {
1063 let plaintext = b"single char key";
1064 let passphrase = "x";
1065
1066 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1067 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1068 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1069
1070 assert_eq!(plaintext.to_vec(), decrypted);
1071 }
1072
1073 #[test]
1074 fn roundtrip_whitespace_only_passphrase() {
1075 let plaintext = b"whitespace key test";
1076 let passphrase = " \t\n ";
1077
1078 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1079 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1080 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1081
1082 assert_eq!(plaintext.to_vec(), decrypted);
1083 }
1084
1085 #[test]
1086 fn roundtrip_max_reasonable_passphrase() {
1087 let plaintext = b"max key test";
1088 let passphrase: String = "Z".repeat(100_000);
1090
1091 let encrypted = encrypt(plaintext, &passphrase).expect("encrypt");
1092 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1093 let decrypted = decrypt(&encrypted_str, &passphrase).expect("decrypt");
1094
1095 assert_eq!(plaintext.to_vec(), decrypted);
1096 }
1097
1098 #[test]
1101 fn nonce_uniqueness_raw_salt_and_nonce_differ() {
1102 let plaintext = b"nonce uniqueness check";
1103 let passphrase = "same-passphrase";
1104
1105 let enc1 = encrypt(plaintext, passphrase).expect("encrypt");
1106 let enc2 = encrypt(plaintext, passphrase).expect("encrypt");
1107
1108 let raw1 = BASE64
1109 .decode(String::from_utf8(enc1).expect("utf8"))
1110 .expect("base64");
1111 let raw2 = BASE64
1112 .decode(String::from_utf8(enc2).expect("utf8"))
1113 .expect("base64");
1114
1115 let salt_nonce_1 = &raw1[..SALT_SIZE + NONCE_SIZE];
1116 let salt_nonce_2 = &raw2[..SALT_SIZE + NONCE_SIZE];
1117
1118 assert_ne!(salt_nonce_1, salt_nonce_2);
1120 }
1121
1122 #[test]
1125 fn tampered_auth_tag_detected() {
1126 let plaintext = b"auth tag tamper test";
1127 let passphrase = "test-pass";
1128
1129 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1130 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1131
1132 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
1133 let last = raw.len() - 1;
1135 raw[last] ^= 0xFF;
1136 let corrupted = BASE64.encode(&raw);
1137
1138 assert!(decrypt(&corrupted, passphrase).is_err());
1139 }
1140
1141 #[test]
1142 fn tampered_single_bit_flip_detected() {
1143 let plaintext = b"bit flip detection";
1144 let passphrase = "bit-flip-pass";
1145
1146 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1147 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1148
1149 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
1150 let mid = raw.len() / 2;
1152 raw[mid] ^= 0x01;
1153 let corrupted = BASE64.encode(&raw);
1154
1155 assert!(decrypt(&corrupted, passphrase).is_err());
1156 }
1157
1158 #[test]
1161 fn wrong_key_returns_error_not_garbage_data() {
1162 let plaintext = b"This must not leak through wrong key";
1163 let correct = "correct-key";
1164 let wrong = "wrong-key";
1165
1166 let encrypted = encrypt(plaintext, correct).expect("encrypt");
1167 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1168
1169 let result = decrypt(&encrypted_str, wrong);
1170 assert!(
1172 result.is_err(),
1173 "wrong key must return Err, not Ok with garbage"
1174 );
1175
1176 let err_msg = result.unwrap_err().to_string();
1177 assert!(
1178 err_msg.contains("wrong passphrase or corrupted data"),
1179 "error message should indicate wrong passphrase, got: {err_msg}"
1180 );
1181 }
1182
1183 #[test]
1184 fn wrong_key_similar_passphrase_returns_error() {
1185 let plaintext = b"subtle key difference";
1186 let correct = "my-passphrase-abc";
1187 let wrong = "my-passphrase-abd"; let encrypted = encrypt(plaintext, correct).expect("encrypt");
1190 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1191
1192 assert!(
1193 decrypt(&encrypted_str, wrong).is_err(),
1194 "even a single-char difference must cause decryption failure"
1195 );
1196 }
1197
1198 #[test]
1201 fn roundtrip_realistic_json_state() {
1202 let state_json = br#"{
1203 "plan_id": "abc123",
1204 "workspace": "/home/user/project",
1205 "crates": [
1206 {"name": "core", "version": "0.1.0", "status": "published"},
1207 {"name": "cli", "version": "0.2.0", "status": "pending"}
1208 ],
1209 "started_at": "2024-01-15T10:30:00Z",
1210 "token": "cio_supersecrettoken1234567890"
1211 }"#;
1212 let passphrase = "ci-pipeline-key-2024";
1213
1214 let encrypted = encrypt(state_json, passphrase).expect("encrypt");
1215 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1216 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1217
1218 assert_eq!(state_json.to_vec(), decrypted);
1219 }
1220
1221 #[test]
1222 fn roundtrip_event_log_jsonl() {
1223 let events = b"{\"event\":\"publish_start\",\"crate\":\"core\",\"ts\":1700000000}\n\
1224 {\"event\":\"publish_ok\",\"crate\":\"core\",\"ts\":1700000005}\n\
1225 {\"event\":\"publish_start\",\"crate\":\"cli\",\"ts\":1700000010}\n";
1226 let passphrase = "event-log-key";
1227
1228 let encrypted = encrypt(events, passphrase).expect("encrypt");
1229 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1230 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1231
1232 assert_eq!(events.to_vec(), decrypted);
1233 }
1234
1235 #[test]
1238 fn derive_key_always_produces_32_bytes() {
1239 for passphrase in ["", "a", "short", &"x".repeat(10_000)] {
1240 for salt in [&[0u8; 0][..], &[0u8; 1], &[0u8; SALT_SIZE], &[0xFF; 64]] {
1241 let key = derive_key(passphrase, salt);
1242 assert_eq!(
1243 key.len(),
1244 KEY_SIZE,
1245 "key must be {KEY_SIZE} bytes for passphrase len={}, salt len={}",
1246 passphrase.len(),
1247 salt.len()
1248 );
1249 }
1250 }
1251 }
1252
1253 #[test]
1254 fn derive_key_with_empty_salt() {
1255 let key1 = derive_key("passphrase", &[]);
1256 let key2 = derive_key("passphrase", &[]);
1257 assert_eq!(key1, key2, "empty salt should still be deterministic");
1258 assert_eq!(key1.len(), KEY_SIZE);
1259 }
1260
1261 #[test]
1264 fn encrypt_decrypt_exactly_aes_block_size() {
1265 let plaintext = [0xABu8; 16];
1267 let passphrase = "block-boundary";
1268
1269 let encrypted = encrypt(&plaintext, passphrase).expect("encrypt");
1270 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1271 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1272
1273 assert_eq!(plaintext.to_vec(), decrypted);
1274 }
1275
1276 #[test]
1277 fn encrypt_decrypt_multi_block_boundaries() {
1278 let passphrase = "multi-block";
1279 for size in [15, 16, 17, 31, 32, 33, 48, 64, 128, 255, 256, 257] {
1280 let plaintext: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
1281 let encrypted = encrypt(&plaintext, passphrase).expect("encrypt");
1282 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1283 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1284 assert_eq!(plaintext, decrypted, "roundtrip failed for size {size}");
1285 }
1286 }
1287
1288 #[test]
1291 fn state_encryption_encrypt_enabled_no_passphrase_errors() {
1292 let config = EncryptionConfig {
1293 enabled: true,
1294 passphrase: None,
1295 env_var: None,
1296 };
1297 let encryption = StateEncryption::new(config).expect("create");
1298 assert!(!encryption.is_enabled());
1299
1300 let result = encryption.encrypt(b"data");
1301 assert!(result.is_err(), "encrypt with no passphrase should fail");
1302 let err = result.unwrap_err().to_string();
1303 assert!(
1304 err.contains("no passphrase"),
1305 "error should mention missing passphrase, got: {err}"
1306 );
1307 }
1308
1309 #[test]
1310 fn state_encryption_cross_config_decrypt_fails() {
1311 let config_a = EncryptionConfig::new("key-alpha".to_string());
1312 let config_b = EncryptionConfig::new("key-beta".to_string());
1313 let enc_a = StateEncryption::new(config_a).expect("create");
1314 let enc_b = StateEncryption::new(config_b).expect("create");
1315
1316 let data = b"cross-config secret";
1317 let encrypted = enc_a.encrypt(data).expect("encrypt with A");
1318
1319 let result = enc_b.decrypt(&encrypted).expect("decrypt returns fallback");
1321 assert_ne!(
1322 result,
1323 data.to_vec(),
1324 "wrong config must not produce original plaintext"
1325 );
1326 }
1327
1328 #[test]
1331 fn decrypt_truncated_after_header_fails() {
1332 let plaintext = b"data to truncate";
1333 let passphrase = "trunc-pass";
1334
1335 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1336 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1337 let raw = BASE64.decode(&encrypted_str).expect("base64");
1338
1339 let truncated = &raw[..SALT_SIZE + NONCE_SIZE + 16];
1341 let encoded = BASE64.encode(truncated);
1342
1343 assert!(
1344 decrypt(&encoded, passphrase).is_err(),
1345 "truncated ciphertext must fail decryption"
1346 );
1347 }
1348
1349 #[test]
1352 fn is_encrypted_accepts_exact_minimum_length() {
1353 let data = vec![0u8; SALT_SIZE + NONCE_SIZE + 16];
1355 let encoded = BASE64.encode(&data);
1356 assert!(
1357 is_encrypted(&encoded),
1358 "minimum-length valid base64 should pass heuristic"
1359 );
1360 }
1361
1362 #[test]
1365 fn multiple_sequential_encrypt_decrypt_cycles() {
1366 let passphrase = "cycle-test";
1367 let mut data = b"initial plaintext".to_vec();
1368
1369 for i in 0..50 {
1370 let encrypted = encrypt(&data, passphrase)
1371 .unwrap_or_else(|e| panic!("encrypt failed on cycle {i}: {e}"));
1372 let encrypted_str = String::from_utf8(encrypted)
1373 .unwrap_or_else(|e| panic!("utf8 failed on cycle {i}: {e}"));
1374 let decrypted = decrypt(&encrypted_str, passphrase)
1375 .unwrap_or_else(|e| panic!("decrypt failed on cycle {i}: {e}"));
1376 assert_eq!(data, decrypted, "mismatch on cycle {i}");
1377 data.push((i % 256) as u8);
1379 }
1380 }
1381
1382 #[test]
1385 fn roundtrip_null_bytes_in_plaintext() {
1386 let plaintext = b"before\x00middle\x00\x00after\x00";
1387 let passphrase = "null-byte-pass";
1388
1389 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1390 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1391 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1392
1393 assert_eq!(plaintext.to_vec(), decrypted);
1394 }
1395
1396 #[test]
1397 fn roundtrip_all_0xff_bytes() {
1398 let plaintext = vec![0xFFu8; 512];
1399 let passphrase = "high-entropy";
1400
1401 let encrypted = encrypt(&plaintext, passphrase).expect("encrypt");
1402 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1403 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1404
1405 assert_eq!(plaintext, decrypted);
1406 }
1407
1408 #[test]
1411 fn env_var_passphrase_resolution() {
1412 let cfg = EncryptionConfig::from_env("SHIPPER_TEST_PASS_1".to_string());
1413 temp_env::with_var("SHIPPER_TEST_PASS_1", Some("env-secret"), || {
1414 let passphrase = cfg.get_passphrase().unwrap();
1415 assert_eq!(passphrase, Some("env-secret".to_string()));
1416 });
1417 }
1418
1419 #[test]
1420 fn env_var_passphrase_missing_returns_none() {
1421 let cfg = EncryptionConfig::from_env("SHIPPER_TEST_MISSING_VAR".to_string());
1422 temp_env::with_var("SHIPPER_TEST_MISSING_VAR", None::<&str>, || {
1423 let passphrase = cfg.get_passphrase().unwrap();
1424 assert_eq!(passphrase, None);
1425 });
1426 }
1427
1428 #[test]
1429 fn state_encryption_from_env_var_roundtrip() {
1430 let config = EncryptionConfig::from_env("SHIPPER_TEST_ENC_PASS".to_string());
1431 let encryption = StateEncryption::new(config).expect("create");
1432
1433 temp_env::with_var("SHIPPER_TEST_ENC_PASS", Some("my-env-key"), || {
1434 assert!(encryption.is_enabled());
1435
1436 let data = b"env-var encrypted data";
1437 let encrypted = encryption.encrypt(data).expect("encrypt");
1438 let decrypted = encryption.decrypt(&encrypted).expect("decrypt");
1439 assert_eq!(data.to_vec(), decrypted);
1440 });
1441 }
1442
1443 #[test]
1444 fn state_encryption_env_var_takes_precedence() {
1445 let config = EncryptionConfig {
1446 enabled: true,
1447 passphrase: Some("inline-pass".to_string()),
1448 env_var: Some("SHIPPER_TEST_PRIO_PASS".to_string()),
1449 };
1450 let encryption = StateEncryption::new(config).expect("create");
1451
1452 temp_env::with_var("SHIPPER_TEST_PRIO_PASS", Some("env-pass"), || {
1453 let data = b"priority test";
1455 let encrypted = encryption.encrypt(data).expect("encrypt");
1456
1457 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
1459 assert!(
1460 decrypt(&encrypted_str, "env-pass").is_ok(),
1461 "env var passphrase should take priority"
1462 );
1463 });
1464 }
1465
1466 #[test]
1467 fn state_encryption_file_roundtrip_with_env_var() {
1468 let td = tempdir().expect("tempdir");
1469 let path = td.path().join("env_enc.json");
1470
1471 let config = EncryptionConfig::from_env("SHIPPER_TEST_FILE_PASS".to_string());
1472 let encryption = StateEncryption::new(config).expect("create");
1473
1474 temp_env::with_var("SHIPPER_TEST_FILE_PASS", Some("file-env-key"), || {
1475 let data = br#"{"encrypted_via": "env_var"}"#;
1476 encryption.write_file(&path, data).expect("write");
1477 let content = encryption.read_file(&path).expect("read");
1478 assert_eq!(String::from_utf8_lossy(data), content);
1479 });
1480 }
1481
1482 #[test]
1485 fn salt_uniqueness_across_10_encryptions() {
1486 let plaintext = b"salt uniqueness test";
1487 let passphrase = "salt-test";
1488
1489 let mut salts = Vec::new();
1490 for _ in 0..10 {
1491 let encrypted = encrypt(plaintext, passphrase).expect("encrypt");
1492 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
1493 let raw = BASE64.decode(&encrypted_str).expect("base64");
1494 let salt = raw[..SALT_SIZE].to_vec();
1495 salts.push(salt);
1496 }
1497
1498 for i in 0..salts.len() {
1500 for j in (i + 1)..salts.len() {
1501 assert_ne!(salts[i], salts[j], "salt collision at indices {i} and {j}");
1502 }
1503 }
1504 }
1505
1506 #[test]
1509 fn derive_key_unicode_passphrase_is_deterministic() {
1510 let passphrase = "пароль-密码-🔑";
1511 let salt = [0x42u8; SALT_SIZE];
1512 let key1 = derive_key(passphrase, &salt);
1513 let key2 = derive_key(passphrase, &salt);
1514 assert_eq!(key1, key2);
1515 }
1516
1517 #[test]
1518 fn derive_key_newline_passphrase_differs_from_stripped() {
1519 let salt = [0u8; SALT_SIZE];
1520 let key_with_newlines = derive_key("pass\nphrase\n", &salt);
1521 let key_stripped = derive_key("passphrase", &salt);
1522 assert_ne!(key_with_newlines, key_stripped);
1523 }
1524
1525 #[test]
1528 fn double_encrypt_roundtrip() {
1529 let plaintext = b"double layer secret";
1530 let pass1 = "outer-key";
1531 let pass2 = "inner-key";
1532
1533 let inner = encrypt(plaintext, pass1).expect("encrypt inner");
1534 let outer = encrypt(&inner, pass2).expect("encrypt outer");
1535
1536 let outer_str = String::from_utf8(outer).expect("utf8");
1537 let decrypted_outer = decrypt(&outer_str, pass2).expect("decrypt outer");
1538 let inner_str = String::from_utf8(decrypted_outer).expect("utf8");
1539 let decrypted_inner = decrypt(&inner_str, pass1).expect("decrypt inner");
1540
1541 assert_eq!(plaintext.to_vec(), decrypted_inner);
1542 }
1543
1544 #[test]
1547 fn is_encrypted_rejects_whitespace_around_base64() {
1548 let data = vec![0u8; SALT_SIZE + NONCE_SIZE + 16];
1549 let encoded = format!(" {} ", BASE64.encode(&data));
1550 assert!(!is_encrypted(&encoded));
1552 }
1553
1554 #[test]
1555 fn is_encrypted_rejects_json_object() {
1556 assert!(!is_encrypted(r#"{"plan_id":"abc","crates":[]}"#));
1557 }
1558
1559 #[test]
1562 fn state_encryption_decrypt_returns_original_on_bad_encrypted_data() {
1563 let config = EncryptionConfig::new("test-pass".to_string());
1564 let encryption = StateEncryption::new(config).expect("create");
1565
1566 let raw_json = b"plain JSON content";
1568 let result = encryption.decrypt(raw_json).expect("should fall back");
1569 assert_eq!(raw_json.to_vec(), result);
1570 }
1571
1572 #[test]
1575 fn file_roundtrip_unicode_content() {
1576 let td = tempdir().expect("tempdir");
1577 let path = td.path().join("unicode.enc");
1578
1579 let plaintext = "Ünïcödé cöntënt: 日本語テスト 🎉";
1580 write_encrypted(&path, plaintext.as_bytes(), "unicode-pass").expect("write");
1581 let decrypted = read_decrypted(&path, "unicode-pass").expect("read");
1582 assert_eq!(plaintext, decrypted);
1583 }
1584
1585 #[test]
1588 fn encrypt_decrypt_exactly_gcm_tag_size() {
1589 let plaintext = [0xCD; 16];
1591 let passphrase = "tag-size-test";
1592
1593 let encrypted = encrypt(&plaintext, passphrase).expect("encrypt");
1594 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
1595 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1596 assert_eq!(plaintext.to_vec(), decrypted);
1597 }
1598
1599 #[test]
1602 fn display_config_passphrase_takes_precedence_in_display() {
1603 let cfg = EncryptionConfig {
1604 enabled: true,
1605 passphrase: Some("my-pass".to_string()),
1606 env_var: Some("MY_ENV".to_string()),
1607 };
1608 let display = cfg.to_string();
1609 assert!(
1611 display.contains("passphrase:"),
1612 "should show passphrase branch, got: {display}"
1613 );
1614 }
1615
1616 #[test]
1619 fn mask_passphrase_four_chars() {
1620 let masked = mask_passphrase("abcd");
1621 assert_eq!(masked, "a**d");
1622 }
1623
1624 #[test]
1625 fn mask_passphrase_five_chars() {
1626 let masked = mask_passphrase("hello");
1627 assert_eq!(masked, "h***o");
1628 }
1629
1630 #[test]
1631 fn mask_passphrase_with_spaces() {
1632 let masked = mask_passphrase("a b c");
1633 assert_eq!(masked, "a***c");
1634 }
1635
1636 #[test]
1639 fn state_encryption_disabled_ignores_env_var() {
1640 let config = EncryptionConfig {
1641 enabled: false,
1642 passphrase: None,
1643 env_var: Some("SHIPPER_TEST_IGNORED_VAR".to_string()),
1644 };
1645 let encryption = StateEncryption::new(config).expect("create");
1646
1647 temp_env::with_var("SHIPPER_TEST_IGNORED_VAR", Some("secret"), || {
1648 assert!(!encryption.is_enabled());
1649 let data = b"not encrypted";
1651 let result = encryption.decrypt(data).expect("passthrough");
1652 assert_eq!(data.to_vec(), result);
1653 });
1654 }
1655}
1656
1657#[cfg(test)]
1660mod proptests {
1661 use super::*;
1662 use proptest::prelude::*;
1663
1664 proptest! {
1665 #[test]
1666 fn roundtrip_arbitrary_data(data in proptest::collection::vec(any::<u8>(), 0..4096)) {
1667 let passphrase = "prop-test-pass";
1668 let encrypted = encrypt(&data, passphrase).expect("encrypt");
1669 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1670 let decrypted = decrypt(&encrypted_str, passphrase).expect("decrypt");
1671 prop_assert_eq!(data, decrypted);
1672 }
1673
1674 #[test]
1675 fn roundtrip_arbitrary_passphrase(passphrase in "\\PC{1,200}") {
1676 let plaintext = b"fixed plaintext for passphrase fuzz";
1677 let encrypted = encrypt(plaintext, &passphrase).expect("encrypt");
1678 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1679 let decrypted = decrypt(&encrypted_str, &passphrase).expect("decrypt");
1680 prop_assert_eq!(plaintext.to_vec(), decrypted);
1681 }
1682
1683 #[test]
1684 fn roundtrip_arbitrary_data_and_passphrase(
1685 data in proptest::collection::vec(any::<u8>(), 0..1024),
1686 passphrase in "\\PC{1,100}",
1687 ) {
1688 let encrypted = encrypt(&data, &passphrase).expect("encrypt");
1689 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1690 let decrypted = decrypt(&encrypted_str, &passphrase).expect("decrypt");
1691 prop_assert_eq!(data, decrypted);
1692 }
1693
1694 #[test]
1695 fn encrypted_output_is_valid_base64(data in proptest::collection::vec(any::<u8>(), 0..512)) {
1696 let encrypted = encrypt(&data, "test").expect("encrypt");
1697 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1698 prop_assert!(BASE64.decode(&encrypted_str).is_ok());
1699 }
1700
1701 #[test]
1702 fn wrong_passphrase_always_fails(
1703 data in proptest::collection::vec(any::<u8>(), 1..512),
1704 correct in "[a-z]{8,16}",
1705 wrong in "[A-Z]{8,16}",
1706 ) {
1707 prop_assume!(correct != wrong);
1709 let encrypted = encrypt(&data, &correct).expect("encrypt");
1710 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1711 prop_assert!(decrypt(&encrypted_str, &wrong).is_err());
1712 }
1713
1714 #[test]
1715 fn encrypted_size_is_deterministic(data in proptest::collection::vec(any::<u8>(), 0..2048)) {
1716 let encrypted = encrypt(&data, "pass").expect("encrypt");
1717 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1718 let raw = BASE64.decode(&encrypted_str).expect("base64");
1719 let expected = SALT_SIZE + NONCE_SIZE + data.len() + 16;
1721 prop_assert_eq!(raw.len(), expected);
1722 }
1723
1724 #[test]
1725 fn is_encrypted_true_for_encrypt_output(data in proptest::collection::vec(any::<u8>(), 0..512)) {
1726 let encrypted = encrypt(&data, "test-pass").expect("encrypt");
1727 let encrypted_str = String::from_utf8(encrypted).expect("valid UTF-8");
1728 prop_assert!(is_encrypted(&encrypted_str));
1729 }
1730
1731 #[test]
1732 fn is_encrypted_never_panics(s in "\\PC{0,500}") {
1733 let _ = is_encrypted(&s);
1734 }
1735
1736 #[test]
1737 fn decrypt_arbitrary_string_never_panics(s in "\\PC{0,500}") {
1738 let _ = decrypt(&s, "passphrase");
1739 }
1740
1741 #[test]
1742 fn encrypt_output_is_always_utf8(
1743 data in proptest::collection::vec(any::<u8>(), 0..1024),
1744 passphrase in "\\PC{1,50}",
1745 ) {
1746 let encrypted = encrypt(&data, &passphrase).expect("encrypt");
1747 prop_assert!(String::from_utf8(encrypted).is_ok());
1748 }
1749
1750 #[test]
1751 fn each_encrypt_produces_unique_ciphertext(data in proptest::collection::vec(any::<u8>(), 0..256)) {
1752 let a = encrypt(&data, "same-pass").expect("encrypt");
1753 let b = encrypt(&data, "same-pass").expect("encrypt");
1754 prop_assert_ne!(a, b);
1755 }
1756
1757 #[test]
1758 fn encryption_config_serde_roundtrip_arbitrary(passphrase in "\\PC{1,100}") {
1759 let cfg = EncryptionConfig::new(passphrase.clone());
1760 let json = serde_json::to_string(&cfg).expect("serialize");
1761 let de: EncryptionConfig = serde_json::from_str(&json).expect("deserialize");
1762 prop_assert_eq!(de.enabled, true);
1763 prop_assert_eq!(de.passphrase.as_deref(), Some(passphrase.as_str()));
1764 }
1765
1766 #[test]
1767 fn state_encryption_roundtrip_arbitrary(data in proptest::collection::vec(any::<u8>(), 0..1024)) {
1768 let config = EncryptionConfig::new("state-prop-pass".to_string());
1769 let se = StateEncryption::new(config).expect("create");
1770 let encrypted = se.encrypt(&data).expect("encrypt");
1771 let decrypted = se.decrypt(&encrypted).expect("decrypt");
1772 prop_assert_eq!(data, decrypted);
1773 }
1774
1775 #[test]
1776 fn encryption_output_always_longer_than_input(data in proptest::collection::vec(any::<u8>(), 0..2048)) {
1777 let encrypted = encrypt(&data, "length-test").expect("encrypt");
1778 prop_assert!(encrypted.len() > data.len());
1780 }
1781
1782 #[test]
1783 fn tampered_ciphertext_always_fails(data in proptest::collection::vec(any::<u8>(), 1..512)) {
1784 let passphrase = "tamper-prop-test";
1785 let encrypted = encrypt(&data, passphrase).expect("encrypt");
1786 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
1787
1788 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
1789 let idx = SALT_SIZE + NONCE_SIZE + (raw.len() - SALT_SIZE - NONCE_SIZE) / 2;
1791 raw[idx] ^= 0xFF;
1792 let corrupted = BASE64.encode(&raw);
1793
1794 prop_assert!(decrypt(&corrupted, passphrase).is_err());
1795 }
1796
1797 #[test]
1798 fn derive_key_always_produces_32_bytes_prop(
1799 passphrase in "\\PC{0,200}",
1800 salt in proptest::collection::vec(any::<u8>(), 0..64),
1801 ) {
1802 let key = derive_key(&passphrase, &salt);
1803 prop_assert_eq!(key.len(), KEY_SIZE);
1804 }
1805
1806 #[test]
1807 fn decrypt_truncated_ciphertext_always_fails_prop(
1808 data in proptest::collection::vec(any::<u8>(), 1..512),
1809 trim in 1usize..17,
1810 ) {
1811 let passphrase = "truncation-prop";
1812 let encrypted = encrypt(&data, passphrase).expect("encrypt");
1813 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
1814 let raw = BASE64.decode(&encrypted_str).expect("base64");
1815
1816 if raw.len() > SALT_SIZE + NONCE_SIZE + 16 {
1818 let truncated = &raw[..raw.len() - trim];
1819 if truncated.len() >= SALT_SIZE + NONCE_SIZE + 16 {
1820 let encoded = BASE64.encode(truncated);
1821 prop_assert!(decrypt(&encoded, passphrase).is_err());
1822 }
1823 }
1824 }
1825
1826 #[test]
1827 fn derive_key_deterministic_prop(
1828 passphrase in "\\PC{0,100}",
1829 salt in proptest::collection::vec(any::<u8>(), 0..32),
1830 ) {
1831 let key1 = derive_key(&passphrase, &salt);
1832 let key2 = derive_key(&passphrase, &salt);
1833 prop_assert_eq!(key1, key2, "derive_key must be deterministic");
1834 }
1835
1836 #[test]
1837 fn salt_differs_across_encryptions_prop(data in proptest::collection::vec(any::<u8>(), 0..256)) {
1838 let a = encrypt(&data, "same-pass").expect("encrypt");
1839 let b = encrypt(&data, "same-pass").expect("encrypt");
1840 let raw_a = BASE64.decode(String::from_utf8(a).expect("utf8")).expect("base64");
1841 let raw_b = BASE64.decode(String::from_utf8(b).expect("utf8")).expect("base64");
1842 let salt_a = &raw_a[..SALT_SIZE];
1843 let salt_b = &raw_b[..SALT_SIZE];
1844 prop_assert_ne!(salt_a.to_vec(), salt_b.to_vec(), "salts must differ");
1845 }
1846
1847 #[test]
1848 fn double_encrypt_roundtrip_prop(data in proptest::collection::vec(any::<u8>(), 0..256)) {
1849 let pass1 = "layer-one";
1850 let pass2 = "layer-two";
1851 let enc1 = encrypt(&data, pass1).expect("encrypt 1");
1852 let enc2 = encrypt(&enc1, pass2).expect("encrypt 2");
1853 let enc2_str = String::from_utf8(enc2).expect("utf8");
1854 let dec2 = decrypt(&enc2_str, pass2).expect("decrypt 2");
1855 let dec2_str = String::from_utf8(dec2).expect("utf8");
1856 let dec1 = decrypt(&dec2_str, pass1).expect("decrypt 1");
1857 prop_assert_eq!(data, dec1);
1858 }
1859 }
1860}
1861
1862#[cfg(test)]
1865mod snapshot_tests {
1866 use super::*;
1867 use insta::{assert_debug_snapshot, assert_snapshot};
1868
1869 #[test]
1872 fn config_default_json() {
1873 let cfg = EncryptionConfig::default();
1874 let json = serde_json::to_string_pretty(&cfg).expect("serialize");
1875 assert_snapshot!(json);
1876 }
1877
1878 #[test]
1879 fn config_with_passphrase_json() {
1880 let cfg = EncryptionConfig::new("my-secret".to_string());
1881 let json = serde_json::to_string_pretty(&cfg).expect("serialize");
1882 assert_snapshot!(json);
1883 }
1884
1885 #[test]
1886 fn config_with_env_var_json() {
1887 let cfg = EncryptionConfig::from_env("SHIPPER_ENCRYPT_KEY".to_string());
1888 let json = serde_json::to_string_pretty(&cfg).expect("serialize");
1889 assert_snapshot!(json);
1890 }
1891
1892 #[test]
1893 fn config_enabled_no_passphrase_json() {
1894 let cfg = EncryptionConfig {
1895 enabled: true,
1896 passphrase: None,
1897 env_var: None,
1898 };
1899 let json = serde_json::to_string_pretty(&cfg).expect("serialize");
1900 assert_snapshot!(json);
1901 }
1902
1903 #[test]
1904 fn config_with_both_passphrase_and_env_json() {
1905 let cfg = EncryptionConfig {
1906 enabled: true,
1907 passphrase: Some("inline-pass".to_string()),
1908 env_var: Some("SHIPPER_ENCRYPT_KEY".to_string()),
1909 };
1910 let json = serde_json::to_string_pretty(&cfg).expect("serialize");
1911 assert_snapshot!(json);
1912 }
1913
1914 #[test]
1917 fn mask_passphrase_normal() {
1918 assert_snapshot!(mask_passphrase("my-secret-passphrase"));
1919 }
1920
1921 #[test]
1922 fn mask_passphrase_short_three_chars() {
1923 assert_snapshot!(mask_passphrase("abc"));
1924 }
1925
1926 #[test]
1927 fn mask_passphrase_two_chars() {
1928 assert_snapshot!(mask_passphrase("ab"));
1929 }
1930
1931 #[test]
1932 fn mask_passphrase_single_char() {
1933 assert_snapshot!(mask_passphrase("x"));
1934 }
1935
1936 #[test]
1937 fn mask_passphrase_empty() {
1938 assert_snapshot!(mask_passphrase(""));
1939 }
1940
1941 #[test]
1942 fn mask_passphrase_unicode() {
1943 assert_snapshot!(mask_passphrase("🔑secret🔒"));
1944 }
1945
1946 #[test]
1949 fn display_config_disabled() {
1950 let cfg = EncryptionConfig::default();
1951 assert_snapshot!(cfg.to_string());
1952 }
1953
1954 #[test]
1955 fn display_config_with_passphrase() {
1956 let cfg = EncryptionConfig::new("super-secret-key".to_string());
1957 assert_snapshot!(cfg.to_string());
1958 }
1959
1960 #[test]
1961 fn display_config_with_env_var() {
1962 let cfg = EncryptionConfig::from_env("SHIPPER_ENCRYPT_KEY".to_string());
1963 assert_snapshot!(cfg.to_string());
1964 }
1965
1966 #[test]
1967 fn display_config_enabled_no_source() {
1968 let cfg = EncryptionConfig {
1969 enabled: true,
1970 passphrase: None,
1971 env_var: None,
1972 };
1973 assert_snapshot!(cfg.to_string());
1974 }
1975
1976 #[test]
1977 fn display_state_encryption_wrapper() {
1978 let cfg = EncryptionConfig::new("my-passphrase".to_string());
1979 let se = StateEncryption::new(cfg).expect("create");
1980 assert_snapshot!(se.to_string());
1981 }
1982
1983 #[test]
1986 fn error_invalid_base64() {
1987 let err = decrypt("not-valid-base64!!!", "pass").unwrap_err();
1988 assert_snapshot!(err.to_string());
1989 }
1990
1991 #[test]
1992 fn error_data_too_short() {
1993 let short = BASE64.encode(vec![0u8; SALT_SIZE + NONCE_SIZE + 15]);
1994 let err = decrypt(&short, "pass").unwrap_err();
1995 assert_snapshot!(err.to_string());
1996 }
1997
1998 #[test]
1999 fn error_wrong_passphrase() {
2000 let encrypted = encrypt(b"secret data", "correct-pass").expect("encrypt");
2001 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2002 let err = decrypt(&encrypted_str, "wrong-pass").unwrap_err();
2003 assert_snapshot!(err.to_string());
2004 }
2005
2006 #[test]
2007 fn error_corrupted_ciphertext() {
2008 let encrypted = encrypt(b"data", "pass").expect("encrypt");
2009 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2010 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
2011 raw[SALT_SIZE + NONCE_SIZE + 1] ^= 0xFF;
2012 let corrupted = BASE64.encode(&raw);
2013 let err = decrypt(&corrupted, "pass").unwrap_err();
2014 assert_snapshot!(err.to_string());
2015 }
2016
2017 #[test]
2018 fn error_empty_input() {
2019 let err = decrypt("", "pass").unwrap_err();
2020 assert_snapshot!(err.to_string());
2021 }
2022
2023 #[test]
2026 fn error_corrupted_salt_message() {
2027 let encrypted = encrypt(b"snapshot salt", "pass").expect("encrypt");
2028 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2029 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
2030 raw[0] ^= 0xFF;
2031 let corrupted = BASE64.encode(&raw);
2032 let err = decrypt(&corrupted, "pass").unwrap_err();
2033 assert_snapshot!(err.to_string());
2034 }
2035
2036 #[test]
2037 fn error_corrupted_nonce_message() {
2038 let encrypted = encrypt(b"snapshot nonce", "pass").expect("encrypt");
2039 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2040 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
2041 raw[SALT_SIZE] ^= 0xFF;
2042 let corrupted = BASE64.encode(&raw);
2043 let err = decrypt(&corrupted, "pass").unwrap_err();
2044 assert_snapshot!(err.to_string());
2045 }
2046
2047 #[test]
2048 fn error_corrupted_auth_tag_message() {
2049 let encrypted = encrypt(b"snapshot tag", "pass").expect("encrypt");
2050 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2051 let mut raw = BASE64.decode(&encrypted_str).expect("base64");
2052 let last = raw.len() - 1;
2053 raw[last] ^= 0xFF;
2054 let corrupted = BASE64.encode(&raw);
2055 let err = decrypt(&corrupted, "pass").unwrap_err();
2056 assert_snapshot!(err.to_string());
2057 }
2058
2059 #[test]
2062 fn snapshot_derive_key_output_format() {
2063 let key = derive_key("test-passphrase", &[0u8; SALT_SIZE]);
2064 let hex: String = key.iter().map(|b| format!("{b:02x}")).collect();
2066 assert_snapshot!(hex);
2067 }
2068
2069 #[test]
2070 fn snapshot_derive_key_length() {
2071 let key = derive_key("any-passphrase", &[42u8; SALT_SIZE]);
2072 assert_debug_snapshot!(key.len());
2073 }
2074
2075 #[test]
2078 fn snapshot_encryption_config_debug_default() {
2079 let cfg = EncryptionConfig::default();
2080 assert_debug_snapshot!(cfg);
2081 }
2082
2083 #[test]
2084 fn snapshot_encryption_config_debug_with_passphrase() {
2085 let cfg = EncryptionConfig::new("debug-pass".to_string());
2086 assert_debug_snapshot!(cfg);
2087 }
2088
2089 #[test]
2090 fn snapshot_encryption_config_debug_from_env() {
2091 let cfg = EncryptionConfig::from_env("MY_SECRET_VAR".to_string());
2092 assert_debug_snapshot!(cfg);
2093 }
2094
2095 #[test]
2098 fn snapshot_encrypted_data_component_sizes() {
2099 let plaintext = b"snapshot-structure-test";
2100 let encrypted = encrypt(plaintext, "snap-pass").expect("encrypt");
2101 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2102 let raw = BASE64.decode(&encrypted_str).expect("base64");
2103
2104 let info = format!(
2105 "salt_bytes={}, nonce_bytes={}, ciphertext_plus_tag_bytes={}, plaintext_len={}, overhead={}",
2106 SALT_SIZE,
2107 NONCE_SIZE,
2108 raw.len() - SALT_SIZE - NONCE_SIZE,
2109 plaintext.len(),
2110 raw.len() - plaintext.len(),
2111 );
2112 assert_snapshot!(info);
2113 }
2114
2115 #[test]
2116 fn snapshot_derive_key_alternate_passphrase() {
2117 let key = derive_key("alternate-passphrase-for-snapshot", &[0xAB; SALT_SIZE]);
2118 let hex: String = key.iter().map(|b| format!("{b:02x}")).collect();
2119 assert_snapshot!(hex);
2120 }
2121
2122 #[test]
2123 fn snapshot_is_encrypted_results() {
2124 let results = format!(
2125 "empty={}, json={}, short_b64={}, garbage={}",
2126 is_encrypted(""),
2127 is_encrypted(r#"{"key":"value"}"#),
2128 is_encrypted(&BASE64.encode(vec![0u8; 10])),
2129 is_encrypted("!!!not-base64!!!"),
2130 );
2131 assert_snapshot!(results);
2132 }
2133
2134 #[test]
2137 fn snapshot_state_encryption_no_passphrase_error() {
2138 let config = EncryptionConfig {
2139 enabled: true,
2140 passphrase: None,
2141 env_var: None,
2142 };
2143 let encryption = StateEncryption::new(config).expect("create");
2144 let err = encryption.encrypt(b"data").unwrap_err();
2145 assert_snapshot!(err.to_string());
2146 }
2147
2148 #[test]
2151 fn snapshot_display_config_with_both_sources() {
2152 let cfg = EncryptionConfig {
2153 enabled: true,
2154 passphrase: Some("inline-secret".to_string()),
2155 env_var: Some("SHIPPER_KEY".to_string()),
2156 };
2157 assert_snapshot!(cfg.to_string());
2158 }
2159
2160 #[test]
2163 fn snapshot_mask_passphrase_four_chars() {
2164 assert_snapshot!(mask_passphrase("abcd"));
2165 }
2166
2167 #[test]
2168 fn snapshot_mask_passphrase_with_spaces() {
2169 assert_snapshot!(mask_passphrase("a b c d"));
2170 }
2171
2172 #[test]
2173 fn snapshot_mask_passphrase_with_newline() {
2174 assert_snapshot!(mask_passphrase("pass\nword"));
2175 }
2176
2177 #[test]
2180 fn snapshot_encrypted_empty_plaintext_structure() {
2181 let encrypted = encrypt(b"", "snap-pass").expect("encrypt");
2182 let encrypted_str = String::from_utf8(encrypted).expect("utf8");
2183 let raw = BASE64.decode(&encrypted_str).expect("base64");
2184
2185 let info = format!(
2186 "raw_len={}, salt={}, nonce={}, ciphertext_plus_tag={}, plaintext_len=0",
2187 raw.len(),
2188 SALT_SIZE,
2189 NONCE_SIZE,
2190 raw.len() - SALT_SIZE - NONCE_SIZE,
2191 );
2192 assert_snapshot!(info);
2193 }
2194}