Skip to main content

shadowforge_lib/application/
services.rs

1//! Application-layer use-case orchestrators.
2//!
3//! Each service is a thin wrapper that coordinates domain ports.
4//! No file I/O or async runtime — callers provide loaded data.
5
6use 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// ─── AppError ─────────────────────────────────────────────────────────────────
30
31/// Unified application error wrapping all domain errors.
32#[derive(Debug, Error)]
33pub enum AppError {
34    /// Crypto subsystem error.
35    #[error("crypto: {0}")]
36    Crypto(#[from] CryptoError),
37    /// Steganography error.
38    #[error("stego: {0}")]
39    Stego(#[from] StegoError),
40    /// Distribution error.
41    #[error("distribution: {0}")]
42    Distribution(#[from] DistributionError),
43    /// Reconstruction error.
44    #[error("reconstruction: {0}")]
45    Reconstruction(#[from] ReconstructionError),
46    /// Error-correction error.
47    #[error("correction: {0}")]
48    Correction(#[from] CorrectionError),
49    /// Analysis error.
50    #[error("analysis: {0}")]
51    Analysis(#[from] AnalysisError),
52    /// Archive error.
53    #[error("archive: {0}")]
54    Archive(#[from] ArchiveError),
55    /// Operational security error.
56    #[error("opsec: {0}")]
57    Opsec(#[from] OpsecError),
58    /// Scrubber error.
59    #[error("scrubber: {0}")]
60    Scrubber(#[from] ScrubberError),
61    /// Adaptive embedding error.
62    #[error("adaptive: {0}")]
63    Adaptive(#[from] AdaptiveError),
64    /// Deniable steganography error.
65    #[error("deniable: {0}")]
66    Deniable(#[from] DeniableError),
67    /// Canary shard error.
68    #[error("canary: {0}")]
69    Canary(#[from] CanaryError),
70    /// Dead drop error.
71    #[error("dead-drop: {0}")]
72    DeadDrop(#[from] DeadDropError),
73    /// Time-lock puzzle error.
74    #[error("time-lock: {0}")]
75    TimeLock(#[from] TimeLockError),
76    /// Corpus selection error.
77    #[error("corpus: {0}")]
78    Corpus(#[from] CorpusError),
79}
80
81// ─── EmbedService ─────────────────────────────────────────────────────────────
82
83/// Embeds a payload into a cover medium.
84pub struct EmbedService;
85
86impl EmbedService {
87    /// Embed `payload` into `cover` using the provided embedder.
88    ///
89    /// # Errors
90    /// Returns [`AppError::Stego`] on embedding failure.
91    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
100// ─── ExtractService ───────────────────────────────────────────────────────────
101
102/// Extracts a hidden payload from a stego cover.
103pub struct ExtractService;
104
105impl ExtractService {
106    /// Extract payload from `stego`.
107    ///
108    /// # Errors
109    /// Returns [`AppError::Stego`] on extraction failure.
110    pub fn extract(
111        stego: &CoverMedia,
112        extractor: &dyn ExtractTechnique,
113    ) -> Result<Payload, AppError> {
114        Ok(extractor.extract(stego)?)
115    }
116}
117
118// ─── KeyGenService ────────────────────────────────────────────────────────────
119
120/// Key-pair generation orchestrator.
121pub struct KeyGenService;
122
123impl KeyGenService {
124    /// Generate a fresh KEM key pair.
125    ///
126    /// # Errors
127    /// Returns [`AppError::Crypto`] on key-generation failure.
128    pub fn generate_keypair(encryptor: &dyn Encryptor) -> Result<KeyPair, AppError> {
129        Ok(encryptor.generate_keypair()?)
130    }
131
132    /// Generate a signing key pair.
133    ///
134    /// # Errors
135    /// Returns [`AppError::Crypto`] on key-generation failure.
136    pub fn generate_signing_keypair(signer: &dyn Signer) -> Result<KeyPair, AppError> {
137        Ok(signer.generate_keypair()?)
138    }
139
140    /// Sign a message.
141    ///
142    /// # Errors
143    /// Returns [`AppError::Crypto`] on signing failure.
144    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    /// Verify a signature.
153    ///
154    /// # Errors
155    /// Returns [`AppError::Crypto`] on verification failure.
156    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
166// ─── DistributeService ────────────────────────────────────────────────────────
167
168/// Distribute a payload across multiple covers.
169pub struct DistributeService;
170
171impl DistributeService {
172    /// Distribute `payload` across `covers`.
173    ///
174    /// # Errors
175    /// Returns [`AppError::Distribution`] on failure.
176    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
187// ─── ReconstructService ───────────────────────────────────────────────────────
188
189/// Reconstruct a payload from distributed stego covers.
190pub struct ReconstructService;
191
192impl ReconstructService {
193    /// Reconstruct payload from stego covers.
194    ///
195    /// # Errors
196    /// Returns [`AppError::Reconstruction`] on failure.
197    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
207// ─── AnalyseService ───────────────────────────────────────────────────────────
208
209/// Analyse a cover for stego capacity and detectability.
210pub struct AnalyseService;
211
212impl AnalyseService {
213    /// Analyse `cover` for the given `technique`.
214    ///
215    /// # Errors
216    /// Returns [`AppError::Analysis`] on failure.
217    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
226// ─── ScrubService ─────────────────────────────────────────────────────────────
227
228/// Scrub text to remove stylometric fingerprints.
229pub struct ScrubService;
230
231impl ScrubService {
232    /// Scrub text via the provided scrubber port.
233    ///
234    /// # Errors
235    /// Returns [`AppError::Scrubber`] on failure.
236    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
245// ─── ArchiveService ───────────────────────────────────────────────────────────
246
247/// Pack and unpack archive bundles.
248pub struct ArchiveService;
249
250impl ArchiveService {
251    /// Pack files into an archive.
252    ///
253    /// # Errors
254    /// Returns [`AppError::Archive`] on failure.
255    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    /// Unpack an archive into named files.
264    ///
265    /// # Errors
266    /// Returns [`AppError::Archive`] on failure.
267    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
276// ─── DeniableEmbedService ─────────────────────────────────────────────────────
277
278/// Dual-payload deniable steganography orchestrator.
279pub struct DeniableEmbedService;
280
281impl DeniableEmbedService {
282    /// Embed both a real and a decoy payload.
283    ///
284    /// # Errors
285    /// Returns [`AppError::Deniable`] on failure.
286    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    /// Extract a payload using the given key.
297    ///
298    /// # Errors
299    /// Returns [`AppError::Deniable`] on failure.
300    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
310// ─── DeadDropService ──────────────────────────────────────────────────────────
311
312/// Platform-aware dead drop orchestrator.
313pub struct DeadDropService;
314
315impl DeadDropService {
316    /// Encode a payload for posting on a public platform.
317    ///
318    /// # Errors
319    /// Returns [`AppError::DeadDrop`] on failure.
320    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
331// ─── TimeLockServiceApp ───────────────────────────────────────────────────────
332
333/// Time-lock puzzle orchestrator.
334pub struct TimeLockServiceApp;
335
336impl TimeLockServiceApp {
337    /// Wrap a payload in a time-lock puzzle.
338    ///
339    /// # Errors
340    /// Returns [`AppError::TimeLock`] on failure.
341    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    /// Solve a time-lock puzzle (blocking).
350    ///
351    /// # Errors
352    /// Returns [`AppError::TimeLock`] on failure.
353    pub fn unlock(
354        puzzle: &TimeLockPuzzle,
355        service: &dyn TimeLockServicePort,
356    ) -> Result<Payload, AppError> {
357        Ok(service.unlock(puzzle)?)
358    }
359
360    /// Non-blocking puzzle check.
361    ///
362    /// # Errors
363    /// Returns [`AppError::TimeLock`] on failure.
364    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
372// ─── CanaryShardService ───────────────────────────────────────────────────────
373
374/// Canary shard tripwire orchestrator.
375pub struct CanaryShardService;
376
377impl CanaryShardService {
378    /// Embed a canary shard alongside distributed covers.
379    ///
380    /// # Errors
381    /// Returns [`AppError::Canary`] on failure.
382    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    /// Check whether a canary has been accessed.
391    pub fn check_canary(shard: &CanaryShard, canary: &dyn CanaryServicePort) -> bool {
392        canary.check_canary(shard)
393    }
394}
395
396// ─── ForensicService ──────────────────────────────────────────────────────────
397
398/// Forensic watermark tripwire orchestrator.
399pub struct ForensicService;
400
401impl ForensicService {
402    /// Embed a per-recipient watermark into a cover.
403    ///
404    /// # Errors
405    /// Returns [`AppError::Opsec`] on failure.
406    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    /// Identify which recipient leaked a stego cover.
415    ///
416    /// # Errors
417    /// Returns [`AppError::Opsec`] on failure.
418    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
427// ─── AmnesiaPipelineService ───────────────────────────────────────────────────
428
429/// Amnesiac in-memory embed/extract orchestrator.
430pub struct AmnesiaPipelineService;
431
432impl AmnesiaPipelineService {
433    /// Embed a payload entirely in memory — no filesystem writes.
434    ///
435    /// # Errors
436    /// Returns [`AppError::Opsec`] on pipeline failure.
437    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
448// ─── PanicWipeService ─────────────────────────────────────────────────────────
449
450/// Emergency panic wipe orchestrator.
451pub struct PanicWipeService;
452
453impl PanicWipeService {
454    /// Securely wipe all paths in `config`.
455    ///
456    /// # Errors
457    /// Returns [`AppError::Opsec`] on failure.
458    pub fn wipe(config: &PanicWipeConfig, wiper: &dyn PanicWiper) -> Result<(), AppError> {
459        Ok(wiper.wipe(config)?)
460    }
461}
462
463// ─── Tests ────────────────────────────────────────────────────────────────────
464
465#[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    // ─── Mock Embedder / Extractor ────────────────────────────────────────
475
476    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    // ─── Embed + Extract ──────────────────────────────────────────────────
542
543    #[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    // ─── Analyse ──────────────────────────────────────────────────────────
559
560    #[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    // ─── Scrub ────────────────────────────────────────────────────────────
575
576    #[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    // ─── Archive ──────────────────────────────────────────────────────────
591
592    #[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    // ─── AppError wraps all domain errors ─────────────────────────────────
607
608    #[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    // ─── KeyGenService ────────────────────────────────────────────────────
657
658    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    // ─── DistributeService ────────────────────────────────────────────────
734
735    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    // ─── ReconstructService ───────────────────────────────────────────────
763
764    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    // ─── DeniableEmbedService ─────────────────────────────────────────────
791
792    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    // ─── DeadDropService ──────────────────────────────────────────────────
843
844    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    // ─── TimeLockServiceApp ───────────────────────────────────────────────
871
872    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    // ─── CanaryShardService ───────────────────────────────────────────────
921
922    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    // ─── ForensicService ──────────────────────────────────────────────────
961
962    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    // ─── PanicWipeService ─────────────────────────────────────────────────
1019
1020    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    // ─── AmnesiaPipelineService ───────────────────────────────────────────
1041
1042    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    // ─── Remaining AppError coverage ──────────────────────────────────────
1088
1089    #[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}