1use std::io::{Read, Write};
7
8use bytes::Bytes;
9use chrono::{DateTime, Utc};
10use thiserror::Error;
11
12use crate::domain::errors::{
13 AdaptiveError, AnalysisError, ArchiveError, CanaryError, CorpusError, CorrectionError,
14 CryptoError, DeadDropError, DeniableError, DistributionError, OpsecError, ReconstructionError,
15 ScrubberError, StegoError, TimeLockError,
16};
17use crate::domain::ports::{
18 AmnesiaPipeline, ArchiveHandler, CanaryService as CanaryServicePort, CapacityAnalyser,
19 DeadDropEncoder, DeniableEmbedder, Distributor, EmbedTechnique, Encryptor, ExtractTechnique,
20 ForensicWatermarker, PanicWiper, Reconstructor, Signer, StyloScrubber,
21 TimeLockService as TimeLockServicePort,
22};
23use crate::domain::types::{
24 AnalysisReport, ArchiveFormat, CanaryShard, CoverMedia, DeniableKeySet, DeniablePayloadPair,
25 EmbeddingProfile, KeyPair, PanicWipeConfig, Payload, PlatformProfile, Signature,
26 StegoTechnique, StyloProfile, TimeLockPuzzle, WatermarkReceipt, WatermarkTripwireTag,
27};
28
29#[derive(Debug, Error)]
33pub enum AppError {
34 #[error("crypto: {0}")]
36 Crypto(#[from] CryptoError),
37 #[error("stego: {0}")]
39 Stego(#[from] StegoError),
40 #[error("distribution: {0}")]
42 Distribution(#[from] DistributionError),
43 #[error("reconstruction: {0}")]
45 Reconstruction(#[from] ReconstructionError),
46 #[error("correction: {0}")]
48 Correction(#[from] CorrectionError),
49 #[error("analysis: {0}")]
51 Analysis(#[from] AnalysisError),
52 #[error("archive: {0}")]
54 Archive(#[from] ArchiveError),
55 #[error("opsec: {0}")]
57 Opsec(#[from] OpsecError),
58 #[error("scrubber: {0}")]
60 Scrubber(#[from] ScrubberError),
61 #[error("adaptive: {0}")]
63 Adaptive(#[from] AdaptiveError),
64 #[error("deniable: {0}")]
66 Deniable(#[from] DeniableError),
67 #[error("canary: {0}")]
69 Canary(#[from] CanaryError),
70 #[error("dead-drop: {0}")]
72 DeadDrop(#[from] DeadDropError),
73 #[error("time-lock: {0}")]
75 TimeLock(#[from] TimeLockError),
76 #[error("corpus: {0}")]
78 Corpus(#[from] CorpusError),
79}
80
81pub struct EmbedService;
85
86impl EmbedService {
87 pub fn embed(
92 cover: CoverMedia,
93 payload: &Payload,
94 embedder: &dyn EmbedTechnique,
95 ) -> Result<CoverMedia, AppError> {
96 Ok(embedder.embed(cover, payload)?)
97 }
98}
99
100pub struct ExtractService;
104
105impl ExtractService {
106 pub fn extract(
111 stego: &CoverMedia,
112 extractor: &dyn ExtractTechnique,
113 ) -> Result<Payload, AppError> {
114 Ok(extractor.extract(stego)?)
115 }
116}
117
118pub struct KeyGenService;
122
123impl KeyGenService {
124 pub fn generate_keypair(encryptor: &dyn Encryptor) -> Result<KeyPair, AppError> {
129 Ok(encryptor.generate_keypair()?)
130 }
131
132 pub fn generate_signing_keypair(signer: &dyn Signer) -> Result<KeyPair, AppError> {
137 Ok(signer.generate_keypair()?)
138 }
139
140 pub fn sign(
145 signer: &dyn Signer,
146 secret_key: &[u8],
147 message: &[u8],
148 ) -> Result<Signature, AppError> {
149 Ok(signer.sign(secret_key, message)?)
150 }
151
152 pub fn verify(
157 signer: &dyn Signer,
158 public_key: &[u8],
159 message: &[u8],
160 signature: &Signature,
161 ) -> Result<bool, AppError> {
162 Ok(signer.verify(public_key, message, signature)?)
163 }
164}
165
166pub struct DistributeService;
170
171impl DistributeService {
172 pub fn distribute(
177 payload: &Payload,
178 covers: Vec<CoverMedia>,
179 profile: &EmbeddingProfile,
180 distributor: &dyn Distributor,
181 embedder: &dyn EmbedTechnique,
182 ) -> Result<Vec<CoverMedia>, AppError> {
183 Ok(distributor.distribute(payload, profile, covers, embedder)?)
184 }
185}
186
187pub struct ReconstructService;
191
192impl ReconstructService {
193 pub fn reconstruct(
198 stego_covers: Vec<CoverMedia>,
199 extractor: &dyn ExtractTechnique,
200 reconstructor: &dyn Reconstructor,
201 progress_cb: &dyn Fn(usize, usize),
202 ) -> Result<Payload, AppError> {
203 Ok(reconstructor.reconstruct(stego_covers, extractor, progress_cb)?)
204 }
205}
206
207pub struct AnalyseService;
211
212impl AnalyseService {
213 pub fn analyse(
218 cover: &CoverMedia,
219 technique: StegoTechnique,
220 analyser: &dyn CapacityAnalyser,
221 ) -> Result<AnalysisReport, AppError> {
222 Ok(analyser.analyse(cover, technique)?)
223 }
224}
225
226pub struct ScrubService;
230
231impl ScrubService {
232 pub fn scrub(
237 text: &str,
238 profile: &StyloProfile,
239 scrubber: &dyn StyloScrubber,
240 ) -> Result<String, AppError> {
241 Ok(scrubber.scrub(text, profile)?)
242 }
243}
244
245pub struct ArchiveService;
249
250impl ArchiveService {
251 pub fn pack(
256 files: &[(&str, &[u8])],
257 format: ArchiveFormat,
258 handler: &dyn ArchiveHandler,
259 ) -> Result<Bytes, AppError> {
260 Ok(handler.pack(files, format)?)
261 }
262
263 pub fn unpack(
268 archive: &[u8],
269 format: ArchiveFormat,
270 handler: &dyn ArchiveHandler,
271 ) -> Result<Vec<(String, Bytes)>, AppError> {
272 Ok(handler.unpack(archive, format)?)
273 }
274}
275
276pub struct DeniableEmbedService;
280
281impl DeniableEmbedService {
282 pub fn embed_dual(
287 cover: CoverMedia,
288 pair: &DeniablePayloadPair,
289 keys: &DeniableKeySet,
290 embedder: &dyn EmbedTechnique,
291 deniable: &dyn DeniableEmbedder,
292 ) -> Result<CoverMedia, AppError> {
293 Ok(deniable.embed_dual(cover, pair, keys, embedder)?)
294 }
295
296 pub fn extract_with_key(
301 stego: &CoverMedia,
302 key: &[u8],
303 extractor: &dyn ExtractTechnique,
304 deniable: &dyn DeniableEmbedder,
305 ) -> Result<Payload, AppError> {
306 Ok(deniable.extract_with_key(stego, key, extractor)?)
307 }
308}
309
310pub struct DeadDropService;
314
315impl DeadDropService {
316 pub fn encode(
321 cover: CoverMedia,
322 payload: &Payload,
323 platform: &PlatformProfile,
324 embedder: &dyn EmbedTechnique,
325 encoder: &dyn DeadDropEncoder,
326 ) -> Result<CoverMedia, AppError> {
327 Ok(encoder.encode_for_platform(cover, payload, platform, embedder)?)
328 }
329}
330
331pub struct TimeLockServiceApp;
335
336impl TimeLockServiceApp {
337 pub fn lock(
342 payload: &Payload,
343 unlock_at: DateTime<Utc>,
344 service: &dyn TimeLockServicePort,
345 ) -> Result<TimeLockPuzzle, AppError> {
346 Ok(service.lock(payload, unlock_at)?)
347 }
348
349 pub fn unlock(
354 puzzle: &TimeLockPuzzle,
355 service: &dyn TimeLockServicePort,
356 ) -> Result<Payload, AppError> {
357 Ok(service.unlock(puzzle)?)
358 }
359
360 pub fn try_unlock(
365 puzzle: &TimeLockPuzzle,
366 service: &dyn TimeLockServicePort,
367 ) -> Result<Option<Payload>, AppError> {
368 Ok(service.try_unlock(puzzle)?)
369 }
370}
371
372pub struct CanaryShardService;
376
377impl CanaryShardService {
378 pub fn embed_canary(
383 covers: Vec<CoverMedia>,
384 embedder: &dyn EmbedTechnique,
385 canary: &dyn CanaryServicePort,
386 ) -> Result<(Vec<CoverMedia>, CanaryShard), AppError> {
387 Ok(canary.embed_canary(covers, embedder)?)
388 }
389
390 pub fn check_canary(shard: &CanaryShard, canary: &dyn CanaryServicePort) -> bool {
392 canary.check_canary(shard)
393 }
394}
395
396pub struct ForensicService;
400
401impl ForensicService {
402 pub fn embed_tripwire(
407 cover: CoverMedia,
408 tag: &WatermarkTripwireTag,
409 watermarker: &dyn ForensicWatermarker,
410 ) -> Result<CoverMedia, AppError> {
411 Ok(watermarker.embed_tripwire(cover, tag)?)
412 }
413
414 pub fn identify_recipient(
419 stego: &CoverMedia,
420 tags: &[WatermarkTripwireTag],
421 watermarker: &dyn ForensicWatermarker,
422 ) -> Result<Option<WatermarkReceipt>, AppError> {
423 Ok(watermarker.identify_recipient(stego, tags)?)
424 }
425}
426
427pub struct AmnesiaPipelineService;
431
432impl AmnesiaPipelineService {
433 pub fn embed_in_memory(
438 payload_input: &mut dyn Read,
439 cover_input: &mut dyn Read,
440 output: &mut dyn Write,
441 technique: &dyn EmbedTechnique,
442 pipeline: &dyn AmnesiaPipeline,
443 ) -> Result<(), AppError> {
444 Ok(pipeline.embed_in_memory(payload_input, cover_input, output, technique)?)
445 }
446}
447
448pub struct PanicWipeService;
452
453impl PanicWipeService {
454 pub fn wipe(config: &PanicWipeConfig, wiper: &dyn PanicWiper) -> Result<(), AppError> {
459 Ok(wiper.wipe(config)?)
460 }
461}
462
463#[cfg(test)]
466mod tests {
467 use super::*;
468 use crate::domain::types::{Capacity, CoverMediaKind, Shard};
469 use std::collections::HashMap;
470 use uuid::Uuid;
471
472 type TestResult = Result<(), Box<dyn std::error::Error>>;
473
474 struct MockEmbedder;
477
478 impl EmbedTechnique for MockEmbedder {
479 fn technique(&self) -> StegoTechnique {
480 StegoTechnique::LsbImage
481 }
482
483 fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
484 Ok(Capacity {
485 bytes: cover.data.len() as u64,
486 technique: StegoTechnique::LsbImage,
487 })
488 }
489
490 fn embed(&self, cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
491 let mut data = cover.data.to_vec();
492 #[expect(clippy::cast_possible_truncation, reason = "test data < 4 GiB")]
493 let len = payload.len() as u32;
494 data.extend_from_slice(&len.to_le_bytes());
495 data.extend_from_slice(payload.as_bytes());
496 Ok(CoverMedia {
497 kind: cover.kind,
498 data: Bytes::from(data),
499 metadata: cover.metadata,
500 })
501 }
502 }
503
504 struct MockExtractor {
505 cover_prefix_len: usize,
506 }
507
508 impl ExtractTechnique for MockExtractor {
509 fn technique(&self) -> StegoTechnique {
510 StegoTechnique::LsbImage
511 }
512
513 fn extract(&self, stego: &CoverMedia) -> Result<Payload, StegoError> {
514 let data = &stego.data;
515 if data.len() <= self.cover_prefix_len + 4 {
516 return Err(StegoError::NoPayloadFound);
517 }
518 let offset = self.cover_prefix_len;
519 let len_bytes: [u8; 4] = data
520 .get(offset..offset + 4)
521 .ok_or(StegoError::NoPayloadFound)?
522 .try_into()
523 .map_err(|_| StegoError::NoPayloadFound)?;
524 let len = u32::from_le_bytes(len_bytes) as usize;
525 let start = offset + 4;
526 let payload_data = data
527 .get(start..start + len)
528 .ok_or(StegoError::NoPayloadFound)?;
529 Ok(Payload::from_bytes(payload_data.to_vec()))
530 }
531 }
532
533 fn make_cover(size: usize) -> CoverMedia {
534 CoverMedia {
535 kind: CoverMediaKind::PngImage,
536 data: Bytes::from(vec![0u8; size]),
537 metadata: HashMap::new(),
538 }
539 }
540
541 #[test]
544 fn embed_extract_round_trip() -> TestResult {
545 let cover = make_cover(128);
546 let payload = Payload::from_bytes(b"secret message".to_vec());
547 let embedder = MockEmbedder;
548 let extractor = MockExtractor {
549 cover_prefix_len: 128,
550 };
551
552 let stego = EmbedService::embed(cover, &payload, &embedder)?;
553 let extracted = ExtractService::extract(&stego, &extractor)?;
554 assert_eq!(extracted.as_bytes(), b"secret message");
555 Ok(())
556 }
557
558 #[test]
561 fn analyse_returns_report() -> TestResult {
562 let data: Vec<u8> = (0..=255).cycle().take(8192).collect();
563 let cover = CoverMedia {
564 kind: CoverMediaKind::PngImage,
565 data: Bytes::from(data),
566 metadata: HashMap::new(),
567 };
568 let analyser = crate::adapters::analysis::CapacityAnalyserImpl::new();
569 let report = AnalyseService::analyse(&cover, StegoTechnique::LsbImage, &analyser)?;
570 assert!(report.cover_capacity.bytes > 0);
571 Ok(())
572 }
573
574 #[test]
577 fn scrub_service_normalises_text() -> TestResult {
578 let stylo_scrubber = crate::adapters::scrubber::StyloScrubberImpl::new();
579 let profile = StyloProfile {
580 normalize_punctuation: true,
581 target_avg_sentence_len: 15.0,
582 target_vocab_size: 1000,
583 };
584 let scrubbed = ScrubService::scrub("He can't stop!!!", &profile, &stylo_scrubber)?;
585 assert!(!scrubbed.contains(" "));
586 assert!(scrubbed.contains("cannot"));
587 Ok(())
588 }
589
590 #[test]
593 fn archive_service_round_trip() -> TestResult {
594 let handler = crate::adapters::archive::ArchiveHandlerImpl::new();
595 let files = vec![("test.txt", b"data" as &[u8])];
596 let packed = ArchiveService::pack(&files, ArchiveFormat::Zip, &handler)?;
597 let unpacked = ArchiveService::unpack(&packed, ArchiveFormat::Zip, &handler)?;
598 assert_eq!(unpacked.len(), 1);
599 assert_eq!(
600 unpacked.first().ok_or("index out of bounds")?.1.as_ref(),
601 b"data"
602 );
603 Ok(())
604 }
605
606 #[test]
609 fn app_error_wraps_stego() {
610 let stego_err = StegoError::NoPayloadFound;
611 let app_err = AppError::from(stego_err);
612 assert!(matches!(app_err, AppError::Stego(_)));
613 }
614
615 #[test]
616 fn app_error_wraps_crypto() {
617 let crypto_err = CryptoError::KeyGenFailed {
618 reason: "test".into(),
619 };
620 let app_err = AppError::from(crypto_err);
621 assert!(matches!(app_err, AppError::Crypto(_)));
622 }
623
624 #[test]
625 fn app_error_wraps_distribution() {
626 let dist_err = DistributionError::InsufficientCovers { needed: 3, got: 1 };
627 let app_err = AppError::from(dist_err);
628 assert!(matches!(app_err, AppError::Distribution(_)));
629 }
630
631 #[test]
632 fn app_error_wraps_deniable() {
633 let den_err = DeniableError::InsufficientCapacity;
634 let app_err = AppError::from(den_err);
635 assert!(matches!(app_err, AppError::Deniable(_)));
636 }
637
638 #[test]
639 fn app_error_wraps_time_lock() {
640 let tl_err = TimeLockError::ComputationFailed {
641 reason: "test".into(),
642 };
643 let app_err = AppError::from(tl_err);
644 assert!(matches!(app_err, AppError::TimeLock(_)));
645 }
646
647 #[test]
648 fn app_error_wraps_corpus() {
649 let c_err = CorpusError::IndexError {
650 reason: "test".into(),
651 };
652 let app_err = AppError::from(c_err);
653 assert!(matches!(app_err, AppError::Corpus(_)));
654 }
655
656 struct MockEncryptor;
659
660 impl Encryptor for MockEncryptor {
661 fn generate_keypair(&self) -> Result<KeyPair, CryptoError> {
662 Ok(KeyPair {
663 public_key: vec![1u8; 32],
664 secret_key: vec![2u8; 64],
665 })
666 }
667
668 fn encapsulate(&self, _public_key: &[u8]) -> Result<(Bytes, Bytes), CryptoError> {
669 Ok((Bytes::from(vec![3u8; 32]), Bytes::from(vec![4u8; 32])))
670 }
671
672 fn decapsulate(
673 &self,
674 _secret_key: &[u8],
675 _ciphertext: &[u8],
676 ) -> Result<Bytes, CryptoError> {
677 Ok(Bytes::from(vec![4u8; 32]))
678 }
679 }
680
681 struct MockSigner;
682
683 impl Signer for MockSigner {
684 fn generate_keypair(&self) -> Result<KeyPair, CryptoError> {
685 Ok(KeyPair {
686 public_key: vec![5u8; 32],
687 secret_key: vec![6u8; 32],
688 })
689 }
690
691 fn sign(&self, _secret_key: &[u8], _message: &[u8]) -> Result<Signature, CryptoError> {
692 Ok(Signature(Bytes::from(vec![7u8; 64])))
693 }
694
695 fn verify(
696 &self,
697 _public_key: &[u8],
698 _message: &[u8],
699 _signature: &Signature,
700 ) -> Result<bool, CryptoError> {
701 Ok(true)
702 }
703 }
704
705 #[test]
706 fn keygen_generate_keypair() -> TestResult {
707 let encryptor = MockEncryptor;
708 let kp = KeyGenService::generate_keypair(&encryptor)?;
709 assert_eq!(kp.public_key.len(), 32);
710 assert_eq!(kp.secret_key.len(), 64);
711 Ok(())
712 }
713
714 #[test]
715 fn keygen_generate_signing_keypair() -> TestResult {
716 let signer = MockSigner;
717 let kp = KeyGenService::generate_signing_keypair(&signer)?;
718 assert_eq!(kp.public_key.len(), 32);
719 assert_eq!(kp.secret_key.len(), 32);
720 Ok(())
721 }
722
723 #[test]
724 fn keygen_sign_and_verify() -> TestResult {
725 let signer = MockSigner;
726 let signature = KeyGenService::sign(&signer, &[0u8; 32], b"test message")?;
727 assert_eq!(signature.0.len(), 64);
728 let valid = KeyGenService::verify(&signer, &[0u8; 32], b"test message", &signature)?;
729 assert!(valid);
730 Ok(())
731 }
732
733 struct MockDistributor;
736
737 impl crate::domain::ports::Distributor for MockDistributor {
738 fn distribute(
739 &self,
740 _payload: &Payload,
741 _profile: &EmbeddingProfile,
742 covers: Vec<CoverMedia>,
743 _embedder: &dyn EmbedTechnique,
744 ) -> Result<Vec<CoverMedia>, DistributionError> {
745 Ok(covers)
746 }
747 }
748
749 #[test]
750 fn distribute_service_returns_covers() -> TestResult {
751 let payload = Payload::from_bytes(b"payload".to_vec());
752 let covers = vec![make_cover(64), make_cover(64)];
753 let profile = EmbeddingProfile::Standard;
754 let distributor = MockDistributor;
755 let embedder = MockEmbedder;
756 let result =
757 DistributeService::distribute(&payload, covers, &profile, &distributor, &embedder)?;
758 assert_eq!(result.len(), 2);
759 Ok(())
760 }
761
762 struct MockReconstructor;
765
766 impl crate::domain::ports::Reconstructor for MockReconstructor {
767 fn reconstruct(
768 &self,
769 _covers: Vec<CoverMedia>,
770 _extractor: &dyn ExtractTechnique,
771 _progress_cb: &dyn Fn(usize, usize),
772 ) -> Result<Payload, ReconstructionError> {
773 Ok(Payload::from_bytes(b"reconstructed".to_vec()))
774 }
775 }
776
777 #[test]
778 fn reconstruct_service_returns_payload() -> TestResult {
779 let stego = vec![make_cover(128)];
780 let extractor = MockExtractor {
781 cover_prefix_len: 128,
782 };
783 let reconstructor = MockReconstructor;
784 let payload =
785 ReconstructService::reconstruct(stego, &extractor, &reconstructor, &|_, _| {})?;
786 assert_eq!(payload.as_bytes(), b"reconstructed");
787 Ok(())
788 }
789
790 struct MockDeniableEmbedder;
793
794 impl DeniableEmbedder for MockDeniableEmbedder {
795 fn embed_dual(
796 &self,
797 cover: CoverMedia,
798 _pair: &DeniablePayloadPair,
799 _keys: &DeniableKeySet,
800 _embedder: &dyn EmbedTechnique,
801 ) -> Result<CoverMedia, crate::domain::errors::DeniableError> {
802 Ok(cover)
803 }
804
805 fn extract_with_key(
806 &self,
807 _stego: &CoverMedia,
808 _key: &[u8],
809 _extractor: &dyn ExtractTechnique,
810 ) -> Result<Payload, crate::domain::errors::DeniableError> {
811 Ok(Payload::from_bytes(b"deniable".to_vec()))
812 }
813 }
814
815 #[test]
816 fn deniable_embed_service_round_trip() -> TestResult {
817 let cover = make_cover(256);
818 let pair = DeniablePayloadPair {
819 real_payload: b"real".to_vec(),
820 decoy_payload: b"decoy".to_vec(),
821 };
822 let keys = DeniableKeySet {
823 primary_key: vec![1u8; 32],
824 decoy_key: vec![2u8; 32],
825 };
826 let embedder = MockEmbedder;
827 let deniable = MockDeniableEmbedder;
828
829 let stego = DeniableEmbedService::embed_dual(cover, &pair, &keys, &embedder, &deniable)?;
830 let extracted = DeniableEmbedService::extract_with_key(
831 &stego,
832 &[1u8; 32],
833 &MockExtractor {
834 cover_prefix_len: 256,
835 },
836 &deniable,
837 )?;
838 assert_eq!(extracted.as_bytes(), b"deniable");
839 Ok(())
840 }
841
842 struct MockDeadDropEncoder;
845
846 impl DeadDropEncoder for MockDeadDropEncoder {
847 fn encode_for_platform(
848 &self,
849 cover: CoverMedia,
850 _payload: &Payload,
851 _platform: &PlatformProfile,
852 _embedder: &dyn EmbedTechnique,
853 ) -> Result<CoverMedia, DeadDropError> {
854 Ok(cover)
855 }
856 }
857
858 #[test]
859 fn dead_drop_service_encode() -> TestResult {
860 let cover = make_cover(128);
861 let payload = Payload::from_bytes(b"secret".to_vec());
862 let platform = PlatformProfile::Instagram;
863 let embedder = MockEmbedder;
864 let encoder = MockDeadDropEncoder;
865 let result = DeadDropService::encode(cover, &payload, &platform, &embedder, &encoder)?;
866 assert_eq!(result.kind, CoverMediaKind::PngImage);
867 Ok(())
868 }
869
870 struct MockTimeLockService;
873
874 impl TimeLockServicePort for MockTimeLockService {
875 fn lock(
876 &self,
877 payload: &Payload,
878 unlock_at: DateTime<Utc>,
879 ) -> Result<TimeLockPuzzle, TimeLockError> {
880 Ok(TimeLockPuzzle {
881 ciphertext: Bytes::from(payload.as_bytes().to_vec()),
882 modulus: vec![1],
883 start_value: vec![2],
884 squarings_required: 100,
885 created_at: Utc::now(),
886 unlock_at,
887 })
888 }
889
890 fn unlock(&self, puzzle: &TimeLockPuzzle) -> Result<Payload, TimeLockError> {
891 Ok(Payload::from_bytes(puzzle.ciphertext.to_vec()))
892 }
893
894 fn try_unlock(&self, puzzle: &TimeLockPuzzle) -> Result<Option<Payload>, TimeLockError> {
895 Ok(Some(Payload::from_bytes(puzzle.ciphertext.to_vec())))
896 }
897 }
898
899 #[test]
900 fn time_lock_service_lock_and_unlock() -> TestResult {
901 let payload = Payload::from_bytes(b"time locked".to_vec());
902 let service = MockTimeLockService;
903 let puzzle = TimeLockServiceApp::lock(&payload, Utc::now(), &service)?;
904 let recovered = TimeLockServiceApp::unlock(&puzzle, &service)?;
905 assert_eq!(recovered.as_bytes(), b"time locked");
906 Ok(())
907 }
908
909 #[test]
910 fn time_lock_service_try_unlock() -> TestResult {
911 let payload = Payload::from_bytes(b"try me".to_vec());
912 let service = MockTimeLockService;
913 let puzzle = TimeLockServiceApp::lock(&payload, Utc::now(), &service)?;
914 let result = TimeLockServiceApp::try_unlock(&puzzle, &service)?;
915 let recovered = result.ok_or("expected Some")?;
916 assert_eq!(recovered.as_bytes(), b"try me");
917 Ok(())
918 }
919
920 struct MockCanaryService;
923
924 impl CanaryServicePort for MockCanaryService {
925 fn embed_canary(
926 &self,
927 covers: Vec<CoverMedia>,
928 _embedder: &dyn EmbedTechnique,
929 ) -> Result<(Vec<CoverMedia>, CanaryShard), CanaryError> {
930 let shard = CanaryShard {
931 shard: Shard {
932 index: 99,
933 total: 100,
934 data: vec![0u8; 16],
935 hmac_tag: [0u8; 32],
936 },
937 canary_id: Uuid::new_v4(),
938 notify_url: Some("https://example.com/canary".into()),
939 };
940 Ok((covers, shard))
941 }
942
943 fn check_canary(&self, _shard: &CanaryShard) -> bool {
944 false
945 }
946 }
947
948 #[test]
949 fn canary_shard_service_embed_and_check() -> TestResult {
950 let covers = vec![make_cover(64)];
951 let embedder = MockEmbedder;
952 let canary = MockCanaryService;
953 let (result_covers, shard) = CanaryShardService::embed_canary(covers, &embedder, &canary)?;
954 assert_eq!(result_covers.len(), 1);
955 assert_eq!(shard.shard.index, 99);
956 assert!(!CanaryShardService::check_canary(&shard, &canary));
957 Ok(())
958 }
959
960 struct MockForensicWatermarker;
963
964 impl ForensicWatermarker for MockForensicWatermarker {
965 fn embed_tripwire(
966 &self,
967 cover: CoverMedia,
968 _tag: &WatermarkTripwireTag,
969 ) -> Result<CoverMedia, OpsecError> {
970 Ok(cover)
971 }
972
973 fn identify_recipient(
974 &self,
975 _stego: &CoverMedia,
976 tags: &[WatermarkTripwireTag],
977 ) -> Result<Option<WatermarkReceipt>, OpsecError> {
978 if tags.is_empty() {
979 return Ok(None);
980 }
981 Ok(Some(WatermarkReceipt {
982 recipient: tags
983 .first()
984 .map_or_else(String::new, |t| t.recipient_id.to_string()),
985 algorithm: "lsb".into(),
986 shards: vec![0],
987 created_at: Utc::now(),
988 }))
989 }
990 }
991
992 #[test]
993 fn forensic_service_embed_and_identify() -> TestResult {
994 let cover = make_cover(128);
995 let tag = WatermarkTripwireTag {
996 recipient_id: Uuid::new_v4(),
997 embedding_seed: vec![9u8; 16],
998 };
999 let watermarker = MockForensicWatermarker;
1000
1001 let stego = ForensicService::embed_tripwire(cover, &tag, &watermarker)?;
1002 let receipt = ForensicService::identify_recipient(&stego, &[tag], &watermarker)?;
1003 let r = receipt.ok_or("expected Some")?;
1004 assert!(!r.recipient.is_empty());
1005 assert_eq!(r.algorithm, "lsb");
1006 Ok(())
1007 }
1008
1009 #[test]
1010 fn forensic_service_identify_no_tags() -> TestResult {
1011 let cover = make_cover(128);
1012 let watermarker = MockForensicWatermarker;
1013 let receipt = ForensicService::identify_recipient(&cover, &[], &watermarker)?;
1014 assert!(receipt.is_none());
1015 Ok(())
1016 }
1017
1018 struct MockPanicWiper;
1021
1022 impl PanicWiper for MockPanicWiper {
1023 fn wipe(&self, _config: &PanicWipeConfig) -> Result<(), OpsecError> {
1024 Ok(())
1025 }
1026 }
1027
1028 #[test]
1029 fn panic_wipe_service_succeeds() -> TestResult {
1030 let wiper = MockPanicWiper;
1031 let config = PanicWipeConfig {
1032 key_paths: vec![],
1033 config_paths: vec![],
1034 temp_dirs: vec![],
1035 };
1036 PanicWipeService::wipe(&config, &wiper)?;
1037 Ok(())
1038 }
1039
1040 struct MockAmnesiaPipeline;
1043
1044 impl AmnesiaPipeline for MockAmnesiaPipeline {
1045 fn embed_in_memory(
1046 &self,
1047 payload_input: &mut dyn Read,
1048 _cover_input: &mut dyn Read,
1049 output: &mut dyn Write,
1050 _technique: &dyn EmbedTechnique,
1051 ) -> Result<(), OpsecError> {
1052 let mut buf = Vec::new();
1053 payload_input
1054 .read_to_end(&mut buf)
1055 .map_err(|e| OpsecError::PipelineError {
1056 reason: e.to_string(),
1057 })?;
1058 output
1059 .write_all(&buf)
1060 .map_err(|e| OpsecError::PipelineError {
1061 reason: e.to_string(),
1062 })?;
1063 Ok(())
1064 }
1065 }
1066
1067 #[test]
1068 fn amnesia_pipeline_service_embed() -> TestResult {
1069 let pipeline = MockAmnesiaPipeline;
1070 let embedder = MockEmbedder;
1071 let mut payload_input = std::io::Cursor::new(b"secret payload");
1072 let mut cover_input = std::io::Cursor::new(vec![0u8; 128]);
1073 let mut output = Vec::new();
1074
1075 AmnesiaPipelineService::embed_in_memory(
1076 &mut payload_input,
1077 &mut cover_input,
1078 &mut output,
1079 &embedder,
1080 &pipeline,
1081 )?;
1082
1083 assert_eq!(output, b"secret payload");
1084 Ok(())
1085 }
1086
1087 #[test]
1090 fn app_error_wraps_reconstruction() {
1091 let err = ReconstructionError::InsufficientCovers { needed: 3, got: 1 };
1092 let app_err = AppError::from(err);
1093 assert!(matches!(app_err, AppError::Reconstruction(_)));
1094 }
1095
1096 #[test]
1097 fn app_error_wraps_correction() {
1098 let err = CorrectionError::InsufficientShards {
1099 needed: 3,
1100 available: 1,
1101 };
1102 let app_err = AppError::from(err);
1103 assert!(matches!(app_err, AppError::Correction(_)));
1104 }
1105
1106 #[test]
1107 fn app_error_wraps_analysis() {
1108 let err = AnalysisError::UnsupportedCoverType {
1109 reason: "test".into(),
1110 };
1111 let app_err = AppError::from(err);
1112 assert!(matches!(app_err, AppError::Analysis(_)));
1113 }
1114
1115 #[test]
1116 fn app_error_wraps_archive() {
1117 let err = ArchiveError::PackFailed {
1118 reason: "test".into(),
1119 };
1120 let app_err = AppError::from(err);
1121 assert!(matches!(app_err, AppError::Archive(_)));
1122 }
1123
1124 #[test]
1125 fn app_error_wraps_opsec() {
1126 let err = OpsecError::PipelineError {
1127 reason: "test".into(),
1128 };
1129 let app_err = AppError::from(err);
1130 assert!(matches!(app_err, AppError::Opsec(_)));
1131 }
1132
1133 #[test]
1134 fn app_error_wraps_scrubber() {
1135 let err = ScrubberError::ProfileNotSatisfied {
1136 reason: "test".into(),
1137 };
1138 let app_err = AppError::from(err);
1139 assert!(matches!(app_err, AppError::Scrubber(_)));
1140 }
1141
1142 #[test]
1143 fn app_error_wraps_adaptive() {
1144 let err = AdaptiveError::BudgetNotMet {
1145 achieved_db: -5.0,
1146 target_db: -10.0,
1147 };
1148 let app_err = AppError::from(err);
1149 assert!(matches!(app_err, AppError::Adaptive(_)));
1150 }
1151
1152 #[test]
1153 fn app_error_wraps_canary() {
1154 let err = CanaryError::EmbedFailed {
1155 source: StegoError::NoPayloadFound,
1156 };
1157 let app_err = AppError::from(err);
1158 assert!(matches!(app_err, AppError::Canary(_)));
1159 }
1160
1161 #[test]
1162 fn app_error_wraps_dead_drop() {
1163 let err = DeadDropError::EncodeFailed {
1164 reason: "test".into(),
1165 };
1166 let app_err = AppError::from(err);
1167 assert!(matches!(app_err, AppError::DeadDrop(_)));
1168 }
1169
1170 #[test]
1171 fn app_error_display_formats() {
1172 let err = AppError::Crypto(CryptoError::KeyGenFailed {
1173 reason: "oops".into(),
1174 });
1175 let msg = format!("{err}");
1176 assert!(msg.contains("crypto"));
1177 assert!(msg.contains("oops"));
1178 }
1179}