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