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 AdaptiveOptimiser, AmnesiaPipeline, ArchiveHandler, CanaryService as CanaryServicePort,
19 CapacityAnalyser, CompressionSimulator, CorpusIndex, CoverProfileMatcher, DeadDropEncoder,
20 DeniableEmbedder, Distributor, EmbedTechnique, Encryptor, ExtractTechnique,
21 ForensicWatermarker, GeographicDistributor, PanicWiper, Reconstructor, Signer, StyloScrubber,
22 SymmetricCipher, TimeLockService as TimeLockServicePort,
23};
24use crate::domain::types::{
25 AnalysisReport, ArchiveFormat, CanaryShard, CorpusEntry, CoverMedia, DeniableKeySet,
26 DeniablePayloadPair, EmbeddingProfile, GeographicManifest, KeyPair, PanicWipeConfig, Payload,
27 PlatformProfile, Signature, SpectralKey, StegoTechnique, StyloProfile, TimeLockPuzzle,
28 WatermarkReceipt, WatermarkTripwireTag,
29};
30
31#[derive(Debug, Error)]
35pub enum AppError {
36 #[error("cli: {reason}")]
38 Cli {
39 reason: String,
41 },
42 #[error("crypto: {0}")]
44 Crypto(#[from] CryptoError),
45 #[error("stego: {0}")]
47 Stego(#[from] StegoError),
48 #[error("distribution: {0}")]
50 Distribution(#[from] DistributionError),
51 #[error("reconstruction: {0}")]
53 Reconstruction(#[from] ReconstructionError),
54 #[error("correction: {0}")]
56 Correction(#[from] CorrectionError),
57 #[error("analysis: {0}")]
59 Analysis(#[from] AnalysisError),
60 #[error("archive: {0}")]
62 Archive(#[from] ArchiveError),
63 #[error("opsec: {0}")]
65 Opsec(#[from] OpsecError),
66 #[error("scrubber: {0}")]
68 Scrubber(#[from] ScrubberError),
69 #[error("adaptive: {0}")]
71 Adaptive(#[from] AdaptiveError),
72 #[error("deniable: {0}")]
74 Deniable(#[from] DeniableError),
75 #[error("canary: {0}")]
77 Canary(#[from] CanaryError),
78 #[error("dead-drop: {0}")]
80 DeadDrop(#[from] DeadDropError),
81 #[error("time-lock: {0}")]
83 TimeLock(#[from] TimeLockError),
84 #[error("corpus: {0}")]
86 Corpus(#[from] CorpusError),
87}
88
89pub struct EmbedService;
93
94impl EmbedService {
95 pub fn embed(
100 cover: CoverMedia,
101 payload: &Payload,
102 embedder: &dyn EmbedTechnique,
103 ) -> Result<CoverMedia, AppError> {
104 Ok(embedder.embed(cover, payload)?)
105 }
106}
107
108pub struct ExtractService;
112
113impl ExtractService {
114 pub fn extract(
119 stego: &CoverMedia,
120 extractor: &dyn ExtractTechnique,
121 ) -> Result<Payload, AppError> {
122 Ok(extractor.extract(stego)?)
123 }
124}
125
126pub struct KeyGenService;
130
131impl KeyGenService {
132 pub fn generate_keypair(encryptor: &dyn Encryptor) -> Result<KeyPair, AppError> {
137 Ok(encryptor.generate_keypair()?)
138 }
139
140 pub fn generate_signing_keypair(signer: &dyn Signer) -> Result<KeyPair, AppError> {
145 Ok(signer.generate_keypair()?)
146 }
147
148 pub fn sign(
153 signer: &dyn Signer,
154 secret_key: &[u8],
155 message: &[u8],
156 ) -> Result<Signature, AppError> {
157 Ok(signer.sign(secret_key, message)?)
158 }
159
160 pub fn verify(
165 signer: &dyn Signer,
166 public_key: &[u8],
167 message: &[u8],
168 signature: &Signature,
169 ) -> Result<bool, AppError> {
170 Ok(signer.verify(public_key, message, signature)?)
171 }
172}
173
174pub struct CipherService;
178
179impl CipherService {
180 pub fn encrypt(
185 cipher: &dyn SymmetricCipher,
186 key: &[u8],
187 nonce: &[u8],
188 plaintext: &[u8],
189 ) -> Result<Bytes, AppError> {
190 Ok(cipher.encrypt(key, nonce, plaintext)?)
191 }
192
193 pub fn decrypt(
198 cipher: &dyn SymmetricCipher,
199 key: &[u8],
200 nonce: &[u8],
201 ciphertext: &[u8],
202 ) -> Result<Bytes, AppError> {
203 Ok(cipher.decrypt(key, nonce, ciphertext)?)
204 }
205}
206
207pub struct AdaptiveProfileDeps<'a> {
211 pub matcher: &'a dyn CoverProfileMatcher,
213 pub optimiser: &'a dyn AdaptiveOptimiser,
215 pub compressor: &'a dyn CompressionSimulator,
217}
218
219pub struct DistributeService;
221
222impl DistributeService {
223 pub fn distribute(
228 payload: &Payload,
229 covers: Vec<CoverMedia>,
230 profile: &EmbeddingProfile,
231 distributor: &dyn Distributor,
232 embedder: &dyn EmbedTechnique,
233 ) -> Result<Vec<CoverMedia>, AppError> {
234 Ok(distributor.distribute(payload, profile, covers, embedder)?)
235 }
236
237 pub fn distribute_with_geographic_manifest(
242 payload: &Payload,
243 covers: Vec<CoverMedia>,
244 manifest: &GeographicManifest,
245 embedder: &dyn EmbedTechnique,
246 distributor: &dyn GeographicDistributor,
247 ) -> Result<Vec<CoverMedia>, AppError> {
248 Ok(distributor.distribute_with_manifest(payload, covers, manifest, embedder)?)
249 }
250
251 pub fn distribute_with_profile_hardening(
261 payload: &Payload,
262 covers: Vec<CoverMedia>,
263 profile: &EmbeddingProfile,
264 distributor: &dyn Distributor,
265 embedder: &dyn EmbedTechnique,
266 deps: &AdaptiveProfileDeps<'_>,
267 ) -> Result<Vec<CoverMedia>, AppError> {
268 let prepared_covers: Vec<CoverMedia> = match profile {
272 EmbeddingProfile::Standard | EmbeddingProfile::CorpusBased => covers,
273 _ => covers
274 .into_iter()
275 .map(|cover| {
276 if let Some(matched) = deps.matcher.profile_for(&cover) {
277 deps.matcher.apply_profile(cover, &matched)
278 } else {
279 Ok(cover)
280 }
281 })
282 .collect::<Result<_, _>>()?,
283 };
284
285 let original_cover_count = prepared_covers.len();
286 let original_covers_opt: Option<Vec<CoverMedia>> =
289 matches!(profile, EmbeddingProfile::Adaptive { .. }).then(|| prepared_covers.clone());
290 let distributed = distributor.distribute(payload, profile, prepared_covers, embedder)?;
291
292 if distributed.len() != original_cover_count {
293 return Err(AppError::Adaptive(
294 AdaptiveError::DistributionCountMismatch {
295 got: distributed.len(),
296 expected: original_cover_count,
297 },
298 ));
299 }
300
301 match profile {
302 EmbeddingProfile::Standard | EmbeddingProfile::CorpusBased => Ok(distributed),
303 EmbeddingProfile::Adaptive {
304 max_detectability_db,
305 } => {
306 let original_covers = original_covers_opt.unwrap_or_default();
307 distributed
308 .into_iter()
309 .zip(original_covers)
310 .map(|(stego, original)| {
311 deps.optimiser
312 .optimise(stego, &original, *max_detectability_db)
313 })
314 .collect::<Result<Vec<_>, _>>()
315 .map_err(AppError::from)
316 }
317 EmbeddingProfile::CompressionSurvivable { platform } => distributed
318 .into_iter()
319 .map(|stego| deps.compressor.simulate(stego, platform))
320 .collect::<Result<Vec<_>, _>>()
321 .map_err(AppError::from),
322 }
323 }
324}
325
326pub struct ReconstructService;
330
331impl ReconstructService {
332 pub fn reconstruct(
337 stego_covers: Vec<CoverMedia>,
338 extractor: &dyn ExtractTechnique,
339 reconstructor: &dyn Reconstructor,
340 progress_cb: &dyn Fn(usize, usize),
341 ) -> Result<Payload, AppError> {
342 Ok(reconstructor.reconstruct(stego_covers, extractor, progress_cb)?)
343 }
344}
345
346pub struct AnalyseService;
350
351impl AnalyseService {
352 pub fn analyse(
357 cover: &CoverMedia,
358 technique: StegoTechnique,
359 analyser: &dyn CapacityAnalyser,
360 ) -> Result<AnalysisReport, AppError> {
361 Ok(analyser.analyse(cover, technique)?)
362 }
363}
364
365pub struct ScrubService;
369
370impl ScrubService {
371 pub fn scrub(
376 text: &str,
377 profile: &StyloProfile,
378 scrubber: &dyn StyloScrubber,
379 ) -> Result<String, AppError> {
380 Ok(scrubber.scrub(text, profile)?)
381 }
382}
383
384pub struct ArchiveService;
388
389impl ArchiveService {
390 pub fn pack(
395 files: &[(&str, &[u8])],
396 format: ArchiveFormat,
397 handler: &dyn ArchiveHandler,
398 ) -> Result<Bytes, AppError> {
399 Ok(handler.pack(files, format)?)
400 }
401
402 pub fn unpack(
407 archive: &[u8],
408 format: ArchiveFormat,
409 handler: &dyn ArchiveHandler,
410 ) -> Result<Vec<(String, Bytes)>, AppError> {
411 Ok(handler.unpack(archive, format)?)
412 }
413}
414
415pub struct DeniableEmbedService;
419
420impl DeniableEmbedService {
421 pub fn embed_dual(
426 cover: CoverMedia,
427 pair: &DeniablePayloadPair,
428 keys: &DeniableKeySet,
429 embedder: &dyn EmbedTechnique,
430 deniable: &dyn DeniableEmbedder,
431 ) -> Result<CoverMedia, AppError> {
432 Ok(deniable.embed_dual(cover, pair, keys, embedder)?)
433 }
434
435 pub fn extract_with_key(
440 stego: &CoverMedia,
441 key: &[u8],
442 extractor: &dyn ExtractTechnique,
443 deniable: &dyn DeniableEmbedder,
444 ) -> Result<Payload, AppError> {
445 Ok(deniable.extract_with_key(stego, key, extractor)?)
446 }
447}
448
449pub struct DeadDropService;
453
454impl DeadDropService {
455 pub fn encode(
460 cover: CoverMedia,
461 payload: &Payload,
462 platform: &PlatformProfile,
463 embedder: &dyn EmbedTechnique,
464 encoder: &dyn DeadDropEncoder,
465 ) -> Result<CoverMedia, AppError> {
466 Ok(encoder.encode_for_platform(cover, payload, platform, embedder)?)
467 }
468}
469
470pub struct TimeLockServiceApp;
474
475impl TimeLockServiceApp {
476 pub fn lock(
481 payload: &Payload,
482 unlock_at: DateTime<Utc>,
483 service: &dyn TimeLockServicePort,
484 ) -> Result<TimeLockPuzzle, AppError> {
485 Ok(service.lock(payload, unlock_at)?)
486 }
487
488 pub fn unlock(
493 puzzle: &TimeLockPuzzle,
494 service: &dyn TimeLockServicePort,
495 ) -> Result<Payload, AppError> {
496 Ok(service.unlock(puzzle)?)
497 }
498
499 pub fn try_unlock(
504 puzzle: &TimeLockPuzzle,
505 service: &dyn TimeLockServicePort,
506 ) -> Result<Option<Payload>, AppError> {
507 Ok(service.try_unlock(puzzle)?)
508 }
509}
510
511pub struct CanaryShardService;
515
516impl CanaryShardService {
517 pub fn embed_canary(
522 covers: Vec<CoverMedia>,
523 embedder: &dyn EmbedTechnique,
524 canary: &dyn CanaryServicePort,
525 ) -> Result<(Vec<CoverMedia>, CanaryShard), AppError> {
526 Ok(canary.embed_canary(covers, embedder)?)
527 }
528
529 pub fn check_canary(shard: &CanaryShard, canary: &dyn CanaryServicePort) -> bool {
531 canary.check_canary(shard)
532 }
533}
534
535pub struct ForensicService;
539
540impl ForensicService {
541 pub fn embed_tripwire(
546 cover: CoverMedia,
547 tag: &WatermarkTripwireTag,
548 watermarker: &dyn ForensicWatermarker,
549 ) -> Result<CoverMedia, AppError> {
550 Ok(watermarker.embed_tripwire(cover, tag)?)
551 }
552
553 pub fn identify_recipient(
558 stego: &CoverMedia,
559 tags: &[WatermarkTripwireTag],
560 watermarker: &dyn ForensicWatermarker,
561 ) -> Result<Option<WatermarkReceipt>, AppError> {
562 Ok(watermarker.identify_recipient(stego, tags)?)
563 }
564}
565
566pub struct AmnesiaPipelineService;
570
571impl AmnesiaPipelineService {
572 pub fn embed_in_memory(
577 payload_input: &mut dyn Read,
578 cover_input: &mut dyn Read,
579 output: &mut dyn Write,
580 technique: &dyn EmbedTechnique,
581 pipeline: &dyn AmnesiaPipeline,
582 ) -> Result<(), AppError> {
583 Ok(pipeline.embed_in_memory(payload_input, cover_input, output, technique)?)
584 }
585}
586
587pub struct PanicWipeService;
591
592impl PanicWipeService {
593 pub fn wipe(config: &PanicWipeConfig, wiper: &dyn PanicWiper) -> Result<(), AppError> {
598 Ok(wiper.wipe(config)?)
599 }
600}
601
602pub struct CorpusService;
606
607impl CorpusService {
608 pub fn build_index(
613 index: &dyn CorpusIndex,
614 corpus_dir: &std::path::Path,
615 ) -> Result<usize, AppError> {
616 Ok(index.build_index(corpus_dir)?)
617 }
618
619 pub fn search(
624 index: &dyn CorpusIndex,
625 payload: &Payload,
626 technique: StegoTechnique,
627 max_results: usize,
628 ) -> Result<Vec<CorpusEntry>, AppError> {
629 Ok(index.search(payload, technique, max_results)?)
630 }
631
632 pub fn search_for_model(
637 index: &dyn CorpusIndex,
638 payload: &Payload,
639 model_id: &str,
640 resolution: (u32, u32),
641 max_results: usize,
642 ) -> Result<Vec<CorpusEntry>, AppError> {
643 Ok(index.search_for_model(payload, model_id, resolution, max_results)?)
644 }
645
646 pub fn model_stats(index: &dyn CorpusIndex) -> Vec<(SpectralKey, usize)> {
648 index.model_stats()
649 }
650}
651
652#[cfg(test)]
655mod tests {
656 use super::*;
657 use crate::domain::types::{Capacity, CoverMediaKind, GeoShardEntry, Shard};
658 use std::collections::HashMap;
659 use std::sync::Arc;
660 use std::sync::atomic::{AtomicUsize, Ordering};
661 use uuid::Uuid;
662
663 type TestResult = Result<(), Box<dyn std::error::Error>>;
664
665 struct MockEmbedder;
668
669 impl EmbedTechnique for MockEmbedder {
670 fn technique(&self) -> StegoTechnique {
671 StegoTechnique::LsbImage
672 }
673
674 fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
675 Ok(Capacity {
676 bytes: cover.data.len() as u64,
677 technique: StegoTechnique::LsbImage,
678 })
679 }
680
681 fn embed(&self, cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
682 let mut data = cover.data.to_vec();
683 #[expect(clippy::cast_possible_truncation, reason = "test data < 4 GiB")]
684 let len = payload.len() as u32;
685 data.extend_from_slice(&len.to_le_bytes());
686 data.extend_from_slice(payload.as_bytes());
687 Ok(CoverMedia {
688 kind: cover.kind,
689 data: Bytes::from(data),
690 metadata: cover.metadata,
691 })
692 }
693 }
694
695 struct MockExtractor {
696 cover_prefix_len: usize,
697 }
698
699 impl ExtractTechnique for MockExtractor {
700 fn technique(&self) -> StegoTechnique {
701 StegoTechnique::LsbImage
702 }
703
704 fn extract(&self, stego: &CoverMedia) -> Result<Payload, StegoError> {
705 let data = &stego.data;
706 if data.len() <= self.cover_prefix_len + 4 {
707 return Err(StegoError::NoPayloadFound);
708 }
709 let offset = self.cover_prefix_len;
710 let len_bytes: [u8; 4] = data
711 .get(offset..offset + 4)
712 .ok_or(StegoError::NoPayloadFound)?
713 .try_into()
714 .map_err(|_| StegoError::NoPayloadFound)?;
715 let len = u32::from_le_bytes(len_bytes) as usize;
716 let start = offset + 4;
717 let payload_data = data
718 .get(start..start + len)
719 .ok_or(StegoError::NoPayloadFound)?;
720 Ok(Payload::from_bytes(payload_data.to_vec()))
721 }
722 }
723
724 fn make_cover(size: usize) -> CoverMedia {
725 CoverMedia {
726 kind: CoverMediaKind::PngImage,
727 data: Bytes::from(vec![0u8; size]),
728 metadata: HashMap::new(),
729 }
730 }
731
732 #[test]
735 fn embed_extract_round_trip() -> TestResult {
736 let cover = make_cover(128);
737 let payload = Payload::from_bytes(b"secret message".to_vec());
738 let embedder = MockEmbedder;
739 let extractor = MockExtractor {
740 cover_prefix_len: 128,
741 };
742
743 let stego = EmbedService::embed(cover, &payload, &embedder)?;
744 let extracted = ExtractService::extract(&stego, &extractor)?;
745 assert_eq!(extracted.as_bytes(), b"secret message");
746 Ok(())
747 }
748
749 #[test]
752 fn analyse_returns_report() -> TestResult {
753 let data: Vec<u8> = (0..=255).cycle().take(8192).collect();
754 let cover = CoverMedia {
755 kind: CoverMediaKind::PngImage,
756 data: Bytes::from(data),
757 metadata: HashMap::new(),
758 };
759 let analyser = crate::adapters::analysis::CapacityAnalyserImpl::new();
760 let report = AnalyseService::analyse(&cover, StegoTechnique::LsbImage, &analyser)?;
761 assert!(report.cover_capacity.bytes > 0);
762 Ok(())
763 }
764
765 #[test]
768 fn scrub_service_normalises_text() -> TestResult {
769 let stylo_scrubber = crate::adapters::scrubber::StyloScrubberImpl::new();
770 let profile = StyloProfile {
771 normalize_punctuation: true,
772 target_avg_sentence_len: 15.0,
773 target_vocab_size: 1000,
774 };
775 let scrubbed = ScrubService::scrub("He can't stop!!!", &profile, &stylo_scrubber)?;
776 assert!(!scrubbed.contains(" "));
777 assert!(scrubbed.contains("cannot"));
778 Ok(())
779 }
780
781 #[test]
784 fn archive_service_round_trip() -> TestResult {
785 let handler = crate::adapters::archive::ArchiveHandlerImpl::new();
786 let files = vec![("test.txt", b"data" as &[u8])];
787 let packed = ArchiveService::pack(&files, ArchiveFormat::Zip, &handler)?;
788 let unpacked = ArchiveService::unpack(&packed, ArchiveFormat::Zip, &handler)?;
789 assert_eq!(unpacked.len(), 1);
790 assert_eq!(
791 unpacked.first().ok_or("index out of bounds")?.1.as_ref(),
792 b"data"
793 );
794 Ok(())
795 }
796
797 #[test]
800 fn app_error_cli_display_contains_prefix_and_reason() {
801 let err = AppError::Cli {
802 reason: "missing --output flag".into(),
803 };
804 let text = err.to_string();
805 assert!(
806 text.starts_with("cli: "),
807 "expected 'cli: ' prefix, got: {text}"
808 );
809 assert!(
810 text.contains("missing --output flag"),
811 "expected reason in display, got: {text}"
812 );
813 }
814
815 #[test]
816 fn app_error_wraps_stego() {
817 let stego_err = StegoError::NoPayloadFound;
818 let app_err = AppError::from(stego_err);
819 assert!(matches!(app_err, AppError::Stego(_)));
820 }
821
822 #[test]
823 fn app_error_wraps_crypto() {
824 let crypto_err = CryptoError::KeyGenFailed {
825 reason: "test".into(),
826 };
827 let app_err = AppError::from(crypto_err);
828 assert!(matches!(app_err, AppError::Crypto(_)));
829 }
830
831 #[test]
832 fn app_error_wraps_distribution() {
833 let dist_err = DistributionError::InsufficientCovers { needed: 3, got: 1 };
834 let app_err = AppError::from(dist_err);
835 assert!(matches!(app_err, AppError::Distribution(_)));
836 }
837
838 #[test]
839 fn app_error_wraps_deniable() {
840 let den_err = DeniableError::InsufficientCapacity;
841 let app_err = AppError::from(den_err);
842 assert!(matches!(app_err, AppError::Deniable(_)));
843 }
844
845 #[test]
846 fn app_error_wraps_time_lock() {
847 let tl_err = TimeLockError::ComputationFailed {
848 reason: "test".into(),
849 };
850 let app_err = AppError::from(tl_err);
851 assert!(matches!(app_err, AppError::TimeLock(_)));
852 }
853
854 #[test]
855 fn app_error_wraps_corpus() {
856 let c_err = CorpusError::IndexError {
857 reason: "test".into(),
858 };
859 let app_err = AppError::from(c_err);
860 assert!(matches!(app_err, AppError::Corpus(_)));
861 }
862
863 struct MockEncryptor;
866
867 impl Encryptor for MockEncryptor {
868 fn generate_keypair(&self) -> Result<KeyPair, CryptoError> {
869 Ok(KeyPair {
870 public_key: vec![1u8; 32],
871 secret_key: vec![2u8; 64],
872 })
873 }
874
875 fn encapsulate(&self, _public_key: &[u8]) -> Result<(Bytes, Bytes), CryptoError> {
876 Ok((Bytes::from(vec![3u8; 32]), Bytes::from(vec![4u8; 32])))
877 }
878
879 fn decapsulate(
880 &self,
881 _secret_key: &[u8],
882 _ciphertext: &[u8],
883 ) -> Result<Bytes, CryptoError> {
884 Ok(Bytes::from(vec![4u8; 32]))
885 }
886 }
887
888 struct MockSigner;
889
890 impl Signer for MockSigner {
891 fn generate_keypair(&self) -> Result<KeyPair, CryptoError> {
892 Ok(KeyPair {
893 public_key: vec![5u8; 32],
894 secret_key: vec![6u8; 32],
895 })
896 }
897
898 fn sign(&self, _secret_key: &[u8], _message: &[u8]) -> Result<Signature, CryptoError> {
899 Ok(Signature(Bytes::from(vec![7u8; 64])))
900 }
901
902 fn verify(
903 &self,
904 _public_key: &[u8],
905 _message: &[u8],
906 _signature: &Signature,
907 ) -> Result<bool, CryptoError> {
908 Ok(true)
909 }
910 }
911
912 struct MockSelectiveSigner;
913
914 impl Signer for MockSelectiveSigner {
915 fn generate_keypair(&self) -> Result<KeyPair, CryptoError> {
916 Ok(KeyPair {
917 public_key: vec![5u8; 32],
918 secret_key: vec![6u8; 32],
919 })
920 }
921
922 fn sign(&self, _secret_key: &[u8], message: &[u8]) -> Result<Signature, CryptoError> {
923 let mut sig = b"sig:".to_vec();
924 sig.extend_from_slice(message);
925 Ok(Signature(Bytes::from(sig)))
926 }
927
928 fn verify(
929 &self,
930 _public_key: &[u8],
931 message: &[u8],
932 signature: &Signature,
933 ) -> Result<bool, CryptoError> {
934 let mut expected = b"sig:".to_vec();
935 expected.extend_from_slice(message);
936 Ok(signature.0.as_ref() == expected.as_slice())
937 }
938 }
939
940 #[test]
941 fn keygen_generate_keypair() -> TestResult {
942 let encryptor = MockEncryptor;
943 let kp = KeyGenService::generate_keypair(&encryptor)?;
944 assert_eq!(kp.public_key.len(), 32);
945 assert_eq!(kp.secret_key.len(), 64);
946 Ok(())
947 }
948
949 #[test]
950 fn keygen_generate_signing_keypair() -> TestResult {
951 let signer = MockSigner;
952 let kp = KeyGenService::generate_signing_keypair(&signer)?;
953 assert_eq!(kp.public_key.len(), 32);
954 assert_eq!(kp.secret_key.len(), 32);
955 Ok(())
956 }
957
958 #[test]
959 fn keygen_sign_and_verify() -> TestResult {
960 let signer = MockSigner;
961 let signature = KeyGenService::sign(&signer, &[0u8; 32], b"test message")?;
962 assert_eq!(signature.0.len(), 64);
963 let valid = KeyGenService::verify(&signer, &[0u8; 32], b"test message", &signature)?;
964 assert!(valid);
965 Ok(())
966 }
967
968 #[test]
969 fn keygen_verify_invalid_signature_returns_false() -> TestResult {
970 let signer = MockSelectiveSigner;
971 let invalid_signature = Signature(Bytes::from_static(b"sig:wrong message"));
972 let valid =
973 KeyGenService::verify(&signer, &[0u8; 32], b"test message", &invalid_signature)?;
974 assert!(!valid);
975 Ok(())
976 }
977
978 #[test]
979 fn keygen_verify_tampered_message_returns_false() -> TestResult {
980 let signer = MockSelectiveSigner;
981 let signature = KeyGenService::sign(&signer, &[0u8; 32], b"test message")?;
982 let valid = KeyGenService::verify(&signer, &[0u8; 32], b"tampered message", &signature)?;
983 assert!(!valid);
984 Ok(())
985 }
986
987 struct MockDistributor;
990
991 impl crate::domain::ports::Distributor for MockDistributor {
992 fn distribute(
993 &self,
994 _payload: &Payload,
995 _profile: &EmbeddingProfile,
996 covers: Vec<CoverMedia>,
997 _embedder: &dyn EmbedTechnique,
998 ) -> Result<Vec<CoverMedia>, DistributionError> {
999 Ok(covers)
1000 }
1001 }
1002
1003 struct MockGeographicDistributor;
1004
1005 impl GeographicDistributor for MockGeographicDistributor {
1006 fn distribute_with_manifest(
1007 &self,
1008 _payload: &Payload,
1009 covers: Vec<CoverMedia>,
1010 _manifest: &GeographicManifest,
1011 _embedder: &dyn EmbedTechnique,
1012 ) -> Result<Vec<CoverMedia>, OpsecError> {
1013 Ok(covers)
1014 }
1015 }
1016
1017 #[test]
1018 fn distribute_service_returns_covers() -> TestResult {
1019 let payload = Payload::from_bytes(b"payload".to_vec());
1020 let covers = vec![make_cover(64), make_cover(64)];
1021 let profile = EmbeddingProfile::Standard;
1022 let distributor = MockDistributor;
1023 let embedder = MockEmbedder;
1024 let result =
1025 DistributeService::distribute(&payload, covers, &profile, &distributor, &embedder)?;
1026 assert_eq!(result.len(), 2);
1027 Ok(())
1028 }
1029
1030 #[test]
1031 fn distribute_service_geographic_manifest_returns_covers() -> TestResult {
1032 let payload = Payload::from_bytes(b"payload".to_vec());
1033 let covers = vec![make_cover(64), make_cover(64)];
1034 let manifest = GeographicManifest {
1035 shards: vec![GeoShardEntry {
1036 shard_index: 0,
1037 jurisdiction: "US".to_string(),
1038 holder_description: "test holder".to_string(),
1039 }],
1040 minimum_jurisdictions: 1,
1041 };
1042 let distributor = MockGeographicDistributor;
1043 let embedder = MockEmbedder;
1044
1045 let result = DistributeService::distribute_with_geographic_manifest(
1046 &payload,
1047 covers,
1048 &manifest,
1049 &embedder,
1050 &distributor,
1051 )?;
1052 assert_eq!(result.len(), 2);
1053 Ok(())
1054 }
1055
1056 struct MockCoverProfileMatcher {
1057 calls: Arc<AtomicUsize>,
1058 }
1059
1060 impl CoverProfileMatcher for MockCoverProfileMatcher {
1061 fn profile_for(&self, _cover: &CoverMedia) -> Option<crate::domain::ports::CoverProfile> {
1062 self.calls.fetch_add(1, Ordering::Relaxed);
1063 None
1064 }
1065
1066 fn apply_profile(
1067 &self,
1068 cover: CoverMedia,
1069 _profile: &crate::domain::ports::CoverProfile,
1070 ) -> Result<CoverMedia, AdaptiveError> {
1071 Ok(cover)
1072 }
1073 }
1074
1075 struct MockAdaptiveOptimiser {
1076 calls: Arc<AtomicUsize>,
1077 }
1078
1079 impl AdaptiveOptimiser for MockAdaptiveOptimiser {
1080 fn optimise(
1081 &self,
1082 stego: CoverMedia,
1083 _original: &CoverMedia,
1084 _target_db: f64,
1085 ) -> Result<CoverMedia, AdaptiveError> {
1086 self.calls.fetch_add(1, Ordering::Relaxed);
1087 Ok(stego)
1088 }
1089 }
1090
1091 struct MockCompressionSimulator {
1092 simulate_calls: Arc<AtomicUsize>,
1093 }
1094
1095 impl CompressionSimulator for MockCompressionSimulator {
1096 fn simulate(
1097 &self,
1098 cover: CoverMedia,
1099 _platform: &PlatformProfile,
1100 ) -> Result<CoverMedia, AdaptiveError> {
1101 self.simulate_calls.fetch_add(1, Ordering::Relaxed);
1102 Ok(cover)
1103 }
1104
1105 fn survivable_capacity(
1106 &self,
1107 cover: &CoverMedia,
1108 _platform: &PlatformProfile,
1109 ) -> Result<Capacity, AdaptiveError> {
1110 Ok(Capacity {
1111 bytes: cover.data.len() as u64,
1112 technique: StegoTechnique::LsbImage,
1113 })
1114 }
1115 }
1116
1117 #[test]
1118 fn distribute_with_profile_hardening_uses_optimiser_for_adaptive() -> TestResult {
1119 let payload = Payload::from_bytes(b"payload".to_vec());
1120 let covers = vec![make_cover(64), make_cover(64)];
1121 let profile = EmbeddingProfile::Adaptive {
1122 max_detectability_db: -12.0,
1123 };
1124 let distributor = MockDistributor;
1125 let embedder = MockEmbedder;
1126 let matcher_calls = Arc::new(AtomicUsize::new(0));
1127 let optimiser_calls = Arc::new(AtomicUsize::new(0));
1128 let simulator_calls = Arc::new(AtomicUsize::new(0));
1129 let matcher = MockCoverProfileMatcher {
1130 calls: Arc::clone(&matcher_calls),
1131 };
1132 let optimiser = MockAdaptiveOptimiser {
1133 calls: Arc::clone(&optimiser_calls),
1134 };
1135 let compressor = MockCompressionSimulator {
1136 simulate_calls: Arc::clone(&simulator_calls),
1137 };
1138
1139 let result = DistributeService::distribute_with_profile_hardening(
1140 &payload,
1141 covers,
1142 &profile,
1143 &distributor,
1144 &embedder,
1145 &AdaptiveProfileDeps {
1146 matcher: &matcher,
1147 optimiser: &optimiser,
1148 compressor: &compressor,
1149 },
1150 )?;
1151
1152 assert_eq!(result.len(), 2);
1153 assert_eq!(matcher_calls.load(Ordering::Relaxed), 2);
1154 assert_eq!(optimiser_calls.load(Ordering::Relaxed), 2);
1155 assert_eq!(simulator_calls.load(Ordering::Relaxed), 0);
1156 Ok(())
1157 }
1158
1159 #[test]
1160 fn distribute_with_profile_hardening_uses_simulator_for_survivable() -> TestResult {
1161 let payload = Payload::from_bytes(b"payload".to_vec());
1162 let covers = vec![make_cover(64), make_cover(64), make_cover(64)];
1163 let profile = EmbeddingProfile::CompressionSurvivable {
1164 platform: PlatformProfile::Instagram,
1165 };
1166 let distributor = MockDistributor;
1167 let embedder = MockEmbedder;
1168 let matcher_calls = Arc::new(AtomicUsize::new(0));
1169 let optimiser_calls = Arc::new(AtomicUsize::new(0));
1170 let simulator_calls = Arc::new(AtomicUsize::new(0));
1171 let matcher = MockCoverProfileMatcher {
1172 calls: Arc::clone(&matcher_calls),
1173 };
1174 let optimiser = MockAdaptiveOptimiser {
1175 calls: Arc::clone(&optimiser_calls),
1176 };
1177 let compressor = MockCompressionSimulator {
1178 simulate_calls: Arc::clone(&simulator_calls),
1179 };
1180
1181 let result = DistributeService::distribute_with_profile_hardening(
1182 &payload,
1183 covers,
1184 &profile,
1185 &distributor,
1186 &embedder,
1187 &AdaptiveProfileDeps {
1188 matcher: &matcher,
1189 optimiser: &optimiser,
1190 compressor: &compressor,
1191 },
1192 )?;
1193
1194 assert_eq!(result.len(), 3);
1195 assert_eq!(matcher_calls.load(Ordering::Relaxed), 3);
1196 assert_eq!(optimiser_calls.load(Ordering::Relaxed), 0);
1197 assert_eq!(simulator_calls.load(Ordering::Relaxed), 3);
1198 Ok(())
1199 }
1200
1201 struct MockReconstructor;
1204
1205 impl crate::domain::ports::Reconstructor for MockReconstructor {
1206 fn reconstruct(
1207 &self,
1208 _covers: Vec<CoverMedia>,
1209 _extractor: &dyn ExtractTechnique,
1210 _progress_cb: &dyn Fn(usize, usize),
1211 ) -> Result<Payload, ReconstructionError> {
1212 Ok(Payload::from_bytes(b"reconstructed".to_vec()))
1213 }
1214 }
1215
1216 #[test]
1217 fn reconstruct_service_returns_payload() -> TestResult {
1218 let stego = vec![make_cover(128)];
1219 let extractor = MockExtractor {
1220 cover_prefix_len: 128,
1221 };
1222 let reconstructor = MockReconstructor;
1223 let payload =
1224 ReconstructService::reconstruct(stego, &extractor, &reconstructor, &|_, _| {})?;
1225 assert_eq!(payload.as_bytes(), b"reconstructed");
1226 Ok(())
1227 }
1228
1229 struct MockDeniableEmbedder;
1232
1233 impl DeniableEmbedder for MockDeniableEmbedder {
1234 fn embed_dual(
1235 &self,
1236 cover: CoverMedia,
1237 _pair: &DeniablePayloadPair,
1238 _keys: &DeniableKeySet,
1239 _embedder: &dyn EmbedTechnique,
1240 ) -> Result<CoverMedia, crate::domain::errors::DeniableError> {
1241 Ok(cover)
1242 }
1243
1244 fn extract_with_key(
1245 &self,
1246 _stego: &CoverMedia,
1247 _key: &[u8],
1248 _extractor: &dyn ExtractTechnique,
1249 ) -> Result<Payload, crate::domain::errors::DeniableError> {
1250 Ok(Payload::from_bytes(b"deniable".to_vec()))
1251 }
1252 }
1253
1254 #[test]
1255 fn deniable_embed_service_round_trip() -> TestResult {
1256 let cover = make_cover(256);
1257 let pair = DeniablePayloadPair {
1258 real_payload: b"real".to_vec(),
1259 decoy_payload: b"decoy".to_vec(),
1260 };
1261 let keys = DeniableKeySet {
1262 primary_key: vec![1u8; 32],
1263 decoy_key: vec![2u8; 32],
1264 };
1265 let embedder = MockEmbedder;
1266 let deniable = MockDeniableEmbedder;
1267
1268 let stego = DeniableEmbedService::embed_dual(cover, &pair, &keys, &embedder, &deniable)?;
1269 let extracted = DeniableEmbedService::extract_with_key(
1270 &stego,
1271 &[1u8; 32],
1272 &MockExtractor {
1273 cover_prefix_len: 256,
1274 },
1275 &deniable,
1276 )?;
1277 assert_eq!(extracted.as_bytes(), b"deniable");
1278 Ok(())
1279 }
1280
1281 struct MockDeadDropEncoder;
1284
1285 impl DeadDropEncoder for MockDeadDropEncoder {
1286 fn encode_for_platform(
1287 &self,
1288 cover: CoverMedia,
1289 _payload: &Payload,
1290 _platform: &PlatformProfile,
1291 _embedder: &dyn EmbedTechnique,
1292 ) -> Result<CoverMedia, DeadDropError> {
1293 Ok(cover)
1294 }
1295 }
1296
1297 #[test]
1298 fn dead_drop_service_encode() -> TestResult {
1299 let cover = make_cover(128);
1300 let payload = Payload::from_bytes(b"secret".to_vec());
1301 let platform = PlatformProfile::Instagram;
1302 let embedder = MockEmbedder;
1303 let encoder = MockDeadDropEncoder;
1304 let result = DeadDropService::encode(cover, &payload, &platform, &embedder, &encoder)?;
1305 assert_eq!(result.kind, CoverMediaKind::PngImage);
1306 Ok(())
1307 }
1308
1309 struct MockTimeLockService;
1312
1313 impl TimeLockServicePort for MockTimeLockService {
1314 fn lock(
1315 &self,
1316 payload: &Payload,
1317 unlock_at: DateTime<Utc>,
1318 ) -> Result<TimeLockPuzzle, TimeLockError> {
1319 Ok(TimeLockPuzzle {
1320 ciphertext: Bytes::from(payload.as_bytes().to_vec()),
1321 modulus: vec![1],
1322 start_value: vec![2],
1323 squarings_required: 100,
1324 created_at: Utc::now(),
1325 unlock_at,
1326 })
1327 }
1328
1329 fn unlock(&self, puzzle: &TimeLockPuzzle) -> Result<Payload, TimeLockError> {
1330 Ok(Payload::from_bytes(puzzle.ciphertext.to_vec()))
1331 }
1332
1333 fn try_unlock(&self, puzzle: &TimeLockPuzzle) -> Result<Option<Payload>, TimeLockError> {
1334 Ok(Some(Payload::from_bytes(puzzle.ciphertext.to_vec())))
1335 }
1336 }
1337
1338 #[test]
1339 fn time_lock_service_lock_and_unlock() -> TestResult {
1340 let payload = Payload::from_bytes(b"time locked".to_vec());
1341 let service = MockTimeLockService;
1342 let puzzle = TimeLockServiceApp::lock(&payload, Utc::now(), &service)?;
1343 let recovered = TimeLockServiceApp::unlock(&puzzle, &service)?;
1344 assert_eq!(recovered.as_bytes(), b"time locked");
1345 Ok(())
1346 }
1347
1348 #[test]
1349 fn time_lock_service_try_unlock() -> TestResult {
1350 let payload = Payload::from_bytes(b"try me".to_vec());
1351 let service = MockTimeLockService;
1352 let puzzle = TimeLockServiceApp::lock(&payload, Utc::now(), &service)?;
1353 let result = TimeLockServiceApp::try_unlock(&puzzle, &service)?;
1354 let recovered = result.ok_or("expected Some")?;
1355 assert_eq!(recovered.as_bytes(), b"try me");
1356 Ok(())
1357 }
1358
1359 struct MockCanaryService;
1362
1363 impl CanaryServicePort for MockCanaryService {
1364 fn embed_canary(
1365 &self,
1366 covers: Vec<CoverMedia>,
1367 _embedder: &dyn EmbedTechnique,
1368 ) -> Result<(Vec<CoverMedia>, CanaryShard), CanaryError> {
1369 let shard = CanaryShard {
1370 shard: Shard {
1371 index: 99,
1372 total: 100,
1373 data: vec![0u8; 16],
1374 hmac_tag: [0u8; 32],
1375 },
1376 canary_id: Uuid::new_v4(),
1377 notify_url: Some("https://example.com/canary".into()),
1378 };
1379 Ok((covers, shard))
1380 }
1381
1382 fn check_canary(&self, _shard: &CanaryShard) -> bool {
1383 false
1384 }
1385 }
1386
1387 #[test]
1388 fn canary_shard_service_embed_and_check() -> TestResult {
1389 let covers = vec![make_cover(64)];
1390 let embedder = MockEmbedder;
1391 let canary = MockCanaryService;
1392 let (result_covers, shard) = CanaryShardService::embed_canary(covers, &embedder, &canary)?;
1393 assert_eq!(result_covers.len(), 1);
1394 assert_eq!(shard.shard.index, 99);
1395 assert!(!CanaryShardService::check_canary(&shard, &canary));
1396 Ok(())
1397 }
1398
1399 struct MockForensicWatermarker;
1402
1403 impl ForensicWatermarker for MockForensicWatermarker {
1404 fn embed_tripwire(
1405 &self,
1406 cover: CoverMedia,
1407 _tag: &WatermarkTripwireTag,
1408 ) -> Result<CoverMedia, OpsecError> {
1409 Ok(cover)
1410 }
1411
1412 fn identify_recipient(
1413 &self,
1414 _stego: &CoverMedia,
1415 tags: &[WatermarkTripwireTag],
1416 ) -> Result<Option<WatermarkReceipt>, OpsecError> {
1417 if tags.is_empty() {
1418 return Ok(None);
1419 }
1420 Ok(Some(WatermarkReceipt {
1421 recipient: tags
1422 .first()
1423 .map_or_else(String::new, |t| t.recipient_id.to_string()),
1424 algorithm: "lsb".into(),
1425 shards: vec![0],
1426 created_at: Utc::now(),
1427 }))
1428 }
1429 }
1430
1431 #[test]
1432 fn forensic_service_embed_and_identify() -> TestResult {
1433 let cover = make_cover(128);
1434 let tag = WatermarkTripwireTag {
1435 recipient_id: Uuid::new_v4(),
1436 embedding_seed: vec![9u8; 16],
1437 };
1438 let watermarker = MockForensicWatermarker;
1439
1440 let stego = ForensicService::embed_tripwire(cover, &tag, &watermarker)?;
1441 let receipt = ForensicService::identify_recipient(&stego, &[tag], &watermarker)?;
1442 let r = receipt.ok_or("expected Some")?;
1443 assert!(!r.recipient.is_empty());
1444 assert_eq!(r.algorithm, "lsb");
1445 Ok(())
1446 }
1447
1448 #[test]
1449 fn forensic_service_identify_no_tags() -> TestResult {
1450 let cover = make_cover(128);
1451 let watermarker = MockForensicWatermarker;
1452 let receipt = ForensicService::identify_recipient(&cover, &[], &watermarker)?;
1453 assert!(receipt.is_none());
1454 Ok(())
1455 }
1456
1457 struct MockPanicWiper;
1460
1461 impl PanicWiper for MockPanicWiper {
1462 fn wipe(&self, _config: &PanicWipeConfig) -> Result<(), OpsecError> {
1463 Ok(())
1464 }
1465 }
1466
1467 #[test]
1468 fn panic_wipe_service_succeeds() -> TestResult {
1469 let wiper = MockPanicWiper;
1470 let config = PanicWipeConfig {
1471 key_paths: vec![],
1472 config_paths: vec![],
1473 temp_dirs: vec![],
1474 };
1475 PanicWipeService::wipe(&config, &wiper)?;
1476 Ok(())
1477 }
1478
1479 struct MockAmnesiaPipeline;
1482
1483 impl AmnesiaPipeline for MockAmnesiaPipeline {
1484 fn embed_in_memory(
1485 &self,
1486 payload_input: &mut dyn Read,
1487 _cover_input: &mut dyn Read,
1488 output: &mut dyn Write,
1489 _technique: &dyn EmbedTechnique,
1490 ) -> Result<(), OpsecError> {
1491 let mut buf = Vec::new();
1492 payload_input
1493 .read_to_end(&mut buf)
1494 .map_err(|e| OpsecError::PipelineError {
1495 reason: e.to_string(),
1496 })?;
1497 output
1498 .write_all(&buf)
1499 .map_err(|e| OpsecError::PipelineError {
1500 reason: e.to_string(),
1501 })?;
1502 Ok(())
1503 }
1504 }
1505
1506 #[test]
1507 fn amnesia_pipeline_service_embed() -> TestResult {
1508 let pipeline = MockAmnesiaPipeline;
1509 let embedder = MockEmbedder;
1510 let mut payload_input = std::io::Cursor::new(b"secret payload");
1511 let mut cover_input = std::io::Cursor::new(vec![0u8; 128]);
1512 let mut output = Vec::new();
1513
1514 AmnesiaPipelineService::embed_in_memory(
1515 &mut payload_input,
1516 &mut cover_input,
1517 &mut output,
1518 &embedder,
1519 &pipeline,
1520 )?;
1521
1522 assert_eq!(output, b"secret payload");
1523 Ok(())
1524 }
1525
1526 #[test]
1529 fn app_error_wraps_reconstruction() {
1530 let err = ReconstructionError::InsufficientCovers { needed: 3, got: 1 };
1531 let app_err = AppError::from(err);
1532 assert!(matches!(app_err, AppError::Reconstruction(_)));
1533 }
1534
1535 #[test]
1536 fn app_error_wraps_correction() {
1537 let err = CorrectionError::InsufficientShards {
1538 needed: 3,
1539 available: 1,
1540 };
1541 let app_err = AppError::from(err);
1542 assert!(matches!(app_err, AppError::Correction(_)));
1543 }
1544
1545 #[test]
1546 fn app_error_wraps_analysis() {
1547 let err = AnalysisError::UnsupportedCoverType {
1548 reason: "test".into(),
1549 };
1550 let app_err = AppError::from(err);
1551 assert!(matches!(app_err, AppError::Analysis(_)));
1552 }
1553
1554 #[test]
1555 fn app_error_wraps_archive() {
1556 let err = ArchiveError::PackFailed {
1557 reason: "test".into(),
1558 };
1559 let app_err = AppError::from(err);
1560 assert!(matches!(app_err, AppError::Archive(_)));
1561 }
1562
1563 #[test]
1564 fn app_error_wraps_opsec() {
1565 let err = OpsecError::PipelineError {
1566 reason: "test".into(),
1567 };
1568 let app_err = AppError::from(err);
1569 assert!(matches!(app_err, AppError::Opsec(_)));
1570 }
1571
1572 #[test]
1573 fn app_error_wraps_scrubber() {
1574 let err = ScrubberError::ProfileNotSatisfied {
1575 reason: "test".into(),
1576 };
1577 let app_err = AppError::from(err);
1578 assert!(matches!(app_err, AppError::Scrubber(_)));
1579 }
1580
1581 #[test]
1582 fn app_error_wraps_adaptive() {
1583 let err = AdaptiveError::BudgetNotMet {
1584 achieved_db: -5.0,
1585 target_db: -10.0,
1586 };
1587 let app_err = AppError::from(err);
1588 assert!(matches!(app_err, AppError::Adaptive(_)));
1589 }
1590
1591 #[test]
1592 fn app_error_wraps_canary() {
1593 let err = CanaryError::EmbedFailed {
1594 source: StegoError::NoPayloadFound,
1595 };
1596 let app_err = AppError::from(err);
1597 assert!(matches!(app_err, AppError::Canary(_)));
1598 }
1599
1600 #[test]
1601 fn app_error_wraps_dead_drop() {
1602 let err = DeadDropError::EncodeFailed {
1603 reason: "test".into(),
1604 };
1605 let app_err = AppError::from(err);
1606 assert!(matches!(app_err, AppError::DeadDrop(_)));
1607 }
1608
1609 #[test]
1610 fn app_error_display_formats() {
1611 let err = AppError::Crypto(CryptoError::KeyGenFailed {
1612 reason: "oops".into(),
1613 });
1614 let msg = format!("{err}");
1615 assert!(msg.contains("crypto"));
1616 assert!(msg.contains("oops"));
1617 }
1618
1619 struct MockCorpusIndex {
1622 build_count: usize,
1623 search_entries: Vec<CorpusEntry>,
1624 }
1625
1626 impl MockCorpusIndex {
1627 fn new(build_count: usize, search_entries: Vec<CorpusEntry>) -> Self {
1628 Self {
1629 build_count,
1630 search_entries,
1631 }
1632 }
1633 }
1634
1635 impl crate::domain::ports::CorpusIndex for MockCorpusIndex {
1636 fn build_index(
1637 &self,
1638 _dir: &std::path::Path,
1639 ) -> Result<usize, crate::domain::errors::CorpusError> {
1640 Ok(self.build_count)
1641 }
1642 fn add_to_index(
1643 &self,
1644 path: &std::path::Path,
1645 ) -> Result<CorpusEntry, crate::domain::errors::CorpusError> {
1646 Ok(CorpusEntry {
1647 file_hash: [0u8; 32],
1648 path: path.to_string_lossy().into_owned(),
1649 cover_kind: CoverMediaKind::PngImage,
1650 precomputed_bit_pattern: bytes::Bytes::new(),
1651 spectral_key: None,
1652 })
1653 }
1654 fn search(
1655 &self,
1656 _payload: &Payload,
1657 _technique: StegoTechnique,
1658 _max: usize,
1659 ) -> Result<Vec<CorpusEntry>, crate::domain::errors::CorpusError> {
1660 Ok(self.search_entries.clone())
1661 }
1662 fn search_for_model(
1663 &self,
1664 _payload: &Payload,
1665 _model_id: &str,
1666 _res: (u32, u32),
1667 _max: usize,
1668 ) -> Result<Vec<CorpusEntry>, crate::domain::errors::CorpusError> {
1669 Ok(self.search_entries.clone())
1670 }
1671 fn model_stats(&self) -> Vec<(SpectralKey, usize)> {
1672 vec![(
1673 SpectralKey {
1674 model_id: "test-model".into(),
1675 resolution: (1920, 1080),
1676 },
1677 self.build_count,
1678 )]
1679 }
1680 }
1681
1682 fn make_corpus_entry(path: &str) -> CorpusEntry {
1683 CorpusEntry {
1684 file_hash: [0u8; 32],
1685 path: path.to_owned(),
1686 cover_kind: CoverMediaKind::PngImage,
1687 precomputed_bit_pattern: bytes::Bytes::new(),
1688 spectral_key: None,
1689 }
1690 }
1691
1692 #[test]
1693 fn corpus_service_build_index() -> TestResult {
1694 let idx = MockCorpusIndex::new(42, vec![]);
1695 let count = CorpusService::build_index(&idx, std::path::Path::new("/some/dir"))?;
1696 assert_eq!(count, 42);
1697 Ok(())
1698 }
1699
1700 #[test]
1701 fn corpus_service_search() -> TestResult {
1702 let entry = make_corpus_entry("covers/img001.png");
1703 let idx = MockCorpusIndex::new(1, vec![entry.clone()]);
1704 let payload = Payload::from_bytes(bytes::Bytes::from_static(b"hello"));
1705 let results = CorpusService::search(&idx, &payload, StegoTechnique::LsbImage, 5)?;
1706 assert_eq!(results.len(), 1);
1707 assert_eq!(
1708 results.first().map(|it| it.path.as_str()),
1709 Some(entry.path.as_str())
1710 );
1711 Ok(())
1712 }
1713
1714 #[test]
1715 fn corpus_service_search_for_model() -> TestResult {
1716 let entry = make_corpus_entry("covers/gemini001.png");
1717 let idx = MockCorpusIndex::new(1, vec![entry.clone()]);
1718 let payload = Payload::from_bytes(bytes::Bytes::from_static(b"hello"));
1719 let results = CorpusService::search_for_model(&idx, &payload, "gemini", (1920, 1080), 3)?;
1720 assert_eq!(results.len(), 1);
1721 assert_eq!(
1722 results.first().map(|it| it.path.as_str()),
1723 Some(entry.path.as_str())
1724 );
1725 Ok(())
1726 }
1727
1728 #[test]
1729 fn corpus_service_model_stats() {
1730 let idx = MockCorpusIndex::new(7, vec![]);
1731 let stats = CorpusService::model_stats(&idx);
1732 assert_eq!(stats.len(), 1);
1733 assert_eq!(
1734 stats.first().map(|(k, _)| k.model_id.as_str()),
1735 Some("test-model")
1736 );
1737 assert_eq!(stats.first().map(|(_, c)| *c), Some(7));
1738 }
1739
1740 #[test]
1743 fn cipher_service_encrypt_decrypt_roundtrip() -> TestResult {
1744 use crate::adapters::crypto::Aes256GcmCipher;
1745 let cipher = Aes256GcmCipher;
1746 let key = vec![0u8; 32];
1747 let nonce = vec![1u8; 12];
1748 let plaintext = b"secret cipher payload";
1749 let ct = CipherService::encrypt(&cipher, &key, &nonce, plaintext)?;
1750 let pt = CipherService::decrypt(&cipher, &key, &nonce, &ct)?;
1751 assert_eq!(pt.as_ref(), plaintext);
1752 Ok(())
1753 }
1754
1755 #[test]
1756 fn cipher_service_decrypt_fails_on_tamper() -> TestResult {
1757 use crate::adapters::crypto::Aes256GcmCipher;
1758 let cipher = Aes256GcmCipher;
1759 let key = vec![0u8; 32];
1760 let nonce = vec![1u8; 12];
1761 let plaintext = b"secret cipher payload";
1762 let mut ct = CipherService::encrypt(&cipher, &key, &nonce, plaintext)?.to_vec();
1763 *ct.get_mut(0).ok_or("empty ciphertext")? ^= 0xFF;
1764 let result = CipherService::decrypt(&cipher, &key, &nonce, &ct);
1765 assert!(result.is_err(), "tampered ciphertext must fail decryption");
1766 Ok(())
1767 }
1768}