1use crate::domain::errors::{DeniableError, StegoError};
4use crate::domain::ports::{DeniableEmbedder, EmbedTechnique, ExtractTechnique, PdfProcessor};
5use crate::domain::types::{
6 Capacity, CoverMedia, CoverMediaKind, DeniableKeySet, DeniablePayloadPair, Payload,
7 StegoTechnique,
8};
9use rand::seq::SliceRandom;
10use rand_chacha::ChaCha20Rng;
11use rand_core::SeedableRng;
12use sha2::{Digest, Sha256};
13
14pub struct PdfContentStreamLsb {
19 processor: Box<dyn PdfProcessor>,
20}
21
22impl PdfContentStreamLsb {
23 #[must_use]
25 pub fn new(processor: Box<dyn PdfProcessor>) -> Self {
26 Self { processor }
27 }
28}
29
30impl EmbedTechnique for PdfContentStreamLsb {
31 fn technique(&self) -> StegoTechnique {
32 StegoTechnique::PdfContentStream
33 }
34
35 fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
36 if cover.kind != CoverMediaKind::PdfDocument {
38 return Err(StegoError::UnsupportedCoverType {
39 reason: format!(
40 "PDF content-stream LSB requires PdfDocument, got {:?}",
41 cover.kind
42 ),
43 });
44 }
45
46 Ok(Capacity {
50 bytes: 1024, technique: StegoTechnique::PdfContentStream,
52 })
53 }
54
55 fn embed(&self, cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
56 if cover.kind != CoverMediaKind::PdfDocument {
57 return Err(StegoError::UnsupportedCoverType {
58 reason: format!(
59 "PDF content-stream LSB requires PdfDocument, got {:?}",
60 cover.kind
61 ),
62 });
63 }
64
65 self.processor
66 .embed_in_content_stream(cover, payload)
67 .map_err(|e| StegoError::MalformedCoverData {
68 reason: format!("embed failed: {e}"),
69 })
70 }
71}
72
73impl ExtractTechnique for PdfContentStreamLsb {
74 fn technique(&self) -> StegoTechnique {
75 StegoTechnique::PdfContentStream
76 }
77
78 fn extract(&self, cover: &CoverMedia) -> Result<Payload, StegoError> {
79 if cover.kind != CoverMediaKind::PdfDocument {
80 return Err(StegoError::UnsupportedCoverType {
81 reason: format!(
82 "PDF content-stream LSB requires PdfDocument, got {:?}",
83 cover.kind
84 ),
85 });
86 }
87
88 self.processor
89 .extract_from_content_stream(cover)
90 .map_err(|e| StegoError::IntegrityCheckFailed {
91 reason: format!("extract failed: {e}"),
92 })
93 }
94}
95
96pub struct PdfMetadataEmbed {
101 processor: Box<dyn PdfProcessor>,
102}
103
104impl PdfMetadataEmbed {
105 #[must_use]
107 pub fn new(processor: Box<dyn PdfProcessor>) -> Self {
108 Self { processor }
109 }
110}
111
112impl EmbedTechnique for PdfMetadataEmbed {
113 fn technique(&self) -> StegoTechnique {
114 StegoTechnique::PdfMetadata
115 }
116
117 fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
118 if cover.kind != CoverMediaKind::PdfDocument {
119 return Err(StegoError::UnsupportedCoverType {
120 reason: format!(
121 "PDF metadata embedding requires PdfDocument, got {:?}",
122 cover.kind
123 ),
124 });
125 }
126
127 Ok(Capacity {
130 bytes: 1_000_000, technique: StegoTechnique::PdfMetadata,
132 })
133 }
134
135 fn embed(&self, cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
136 if cover.kind != CoverMediaKind::PdfDocument {
137 return Err(StegoError::UnsupportedCoverType {
138 reason: format!(
139 "PDF metadata embedding requires PdfDocument, got {:?}",
140 cover.kind
141 ),
142 });
143 }
144
145 self.processor
146 .embed_in_metadata(cover, payload)
147 .map_err(|e| StegoError::MalformedCoverData {
148 reason: format!("embed failed: {e}"),
149 })
150 }
151}
152
153impl ExtractTechnique for PdfMetadataEmbed {
154 fn technique(&self) -> StegoTechnique {
155 StegoTechnique::PdfMetadata
156 }
157
158 fn extract(&self, cover: &CoverMedia) -> Result<Payload, StegoError> {
159 if cover.kind != CoverMediaKind::PdfDocument {
160 return Err(StegoError::UnsupportedCoverType {
161 reason: format!(
162 "PDF metadata embedding requires PdfDocument, got {:?}",
163 cover.kind
164 ),
165 });
166 }
167
168 self.processor
169 .extract_from_metadata(cover)
170 .map_err(|e| StegoError::IntegrityCheckFailed {
171 reason: format!("extract failed: {e}"),
172 })
173 }
174}
175
176#[derive(Debug, Default)]
181pub struct LsbImage;
182
183impl LsbImage {
184 #[must_use]
186 pub const fn new() -> Self {
187 Self
188 }
189}
190
191impl EmbedTechnique for LsbImage {
192 fn technique(&self) -> StegoTechnique {
193 StegoTechnique::LsbImage
194 }
195
196 fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
197 match cover.kind {
199 CoverMediaKind::PngImage | CoverMediaKind::BmpImage => {}
200 _ => {
201 return Err(StegoError::UnsupportedCoverType {
202 reason: format!("LSB image requires PNG or BMP, got {:?}", cover.kind),
203 });
204 }
205 }
206
207 let width: u32 = cover
209 .metadata
210 .get("width")
211 .ok_or_else(|| StegoError::MalformedCoverData {
212 reason: "missing width metadata".to_string(),
213 })?
214 .parse()
215 .map_err(
216 |e: std::num::ParseIntError| StegoError::MalformedCoverData {
217 reason: format!("invalid width: {e}"),
218 },
219 )?;
220
221 let height: u32 = cover
222 .metadata
223 .get("height")
224 .ok_or_else(|| StegoError::MalformedCoverData {
225 reason: "missing height metadata".to_string(),
226 })?
227 .parse()
228 .map_err(
229 |e: std::num::ParseIntError| StegoError::MalformedCoverData {
230 reason: format!("invalid height: {e}"),
231 },
232 )?;
233
234 let pixel_count =
235 width
236 .checked_mul(height)
237 .ok_or_else(|| StegoError::MalformedCoverData {
238 reason: "pixel count overflow".to_string(),
239 })?;
240
241 let bits = pixel_count
244 .checked_mul(3)
245 .and_then(|b| b.checked_sub(32))
246 .ok_or_else(|| StegoError::MalformedCoverData {
247 reason: "capacity calculation overflow".to_string(),
248 })?;
249
250 let bytes = u64::from(bits / 8);
251
252 Ok(Capacity {
253 bytes,
254 technique: StegoTechnique::LsbImage,
255 })
256 }
257
258 fn embed(&self, mut cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
259 match cover.kind {
261 CoverMediaKind::PngImage | CoverMediaKind::BmpImage => {}
262 _ => {
263 return Err(StegoError::UnsupportedCoverType {
264 reason: format!("LSB image requires PNG or BMP, got {:?}", cover.kind),
265 });
266 }
267 }
268
269 let cap = self.capacity(&cover)?;
271 let payload_len = payload.as_bytes().len() as u64;
272 if payload_len > cap.bytes {
273 return Err(StegoError::PayloadTooLarge {
274 needed: payload_len,
275 available: cap.bytes,
276 });
277 }
278
279 if payload_len > u64::from(u32::MAX) {
281 return Err(StegoError::PayloadTooLarge {
282 needed: payload_len,
283 available: u64::from(u32::MAX),
284 });
285 }
286
287 let data = cover.data.to_vec();
289 let mut pixels = data;
290
291 #[expect(
293 clippy::cast_possible_truncation,
294 reason = "checked above: payload_len <= u32::MAX"
295 )]
296 let len_bytes = (payload_len as u32).to_be_bytes();
297 for (byte_idx, byte) in len_bytes.iter().enumerate() {
298 for bit_idx in 0..8 {
299 let bit = (byte >> (7 - bit_idx)) & 1;
300 let pixel_idx = byte_idx * 8 + bit_idx;
301
302 let channel_idx = pixel_idx / 3;
304 let rgb_offset = pixel_idx % 3;
305 let byte_pos = channel_idx * 4 + rgb_offset;
306
307 let pixel =
308 pixels
309 .get_mut(byte_pos)
310 .ok_or_else(|| StegoError::MalformedCoverData {
311 reason: "pixel index out of bounds".to_string(),
312 })?;
313 *pixel = (*pixel & 0xFE) | bit;
314 }
315 }
316
317 let payload_bytes = payload.as_bytes();
319 for (byte_idx, byte) in payload_bytes.iter().enumerate() {
320 for bit_idx in 0..8 {
321 let bit = (byte >> (7 - bit_idx)) & 1;
322 let pixel_idx = 32 + byte_idx * 8 + bit_idx;
323
324 let channel_idx = pixel_idx / 3;
326 let rgb_offset = pixel_idx % 3;
327 let byte_pos = channel_idx * 4 + rgb_offset;
328
329 let pixel =
330 pixels
331 .get_mut(byte_pos)
332 .ok_or_else(|| StegoError::MalformedCoverData {
333 reason: "pixel index out of bounds".to_string(),
334 })?;
335 *pixel = (*pixel & 0xFE) | bit;
336 }
337 }
338
339 cover.data = pixels.into();
340 Ok(cover)
341 }
342}
343
344impl ExtractTechnique for LsbImage {
345 fn technique(&self) -> StegoTechnique {
346 StegoTechnique::LsbImage
347 }
348
349 fn extract(&self, cover: &CoverMedia) -> Result<Payload, StegoError> {
350 match cover.kind {
352 CoverMediaKind::PngImage | CoverMediaKind::BmpImage => {}
353 _ => {
354 return Err(StegoError::UnsupportedCoverType {
355 reason: format!("LSB image requires PNG or BMP, got {:?}", cover.kind),
356 });
357 }
358 }
359
360 let pixels = cover.data.as_ref();
361
362 let mut len_bytes = [0u8; 4];
364 for (byte_idx, len_byte) in len_bytes.iter_mut().enumerate() {
365 for bit_idx in 0..8 {
366 let pixel_idx = byte_idx * 8 + bit_idx;
367
368 let channel_idx = pixel_idx / 3;
370 let rgb_offset = pixel_idx % 3;
371 let byte_pos = channel_idx * 4 + rgb_offset;
372
373 let bit = pixels
374 .get(byte_pos)
375 .ok_or_else(|| StegoError::MalformedCoverData {
376 reason: "pixel index out of bounds".to_string(),
377 })?
378 & 1;
379 *len_byte |= bit << (7 - bit_idx);
380 }
381 }
382
383 let payload_len = u32::from_be_bytes(len_bytes) as usize;
384
385 let mut payload_bytes = vec![0u8; payload_len];
387 for (byte_idx, payload_byte) in payload_bytes.iter_mut().enumerate() {
388 for bit_idx in 0..8 {
389 let pixel_idx = 32 + byte_idx * 8 + bit_idx;
390
391 let channel_idx = pixel_idx / 3;
393 let rgb_offset = pixel_idx % 3;
394 let byte_pos = channel_idx * 4 + rgb_offset;
395
396 let bit = pixels
397 .get(byte_pos)
398 .ok_or_else(|| StegoError::MalformedCoverData {
399 reason: "pixel index out of bounds".to_string(),
400 })?
401 & 1;
402 *payload_byte |= bit << (7 - bit_idx);
403 }
404 }
405
406 Ok(Payload::from_bytes(payload_bytes))
407 }
408}
409
410#[derive(Debug, Default)]
423pub struct DctJpeg;
424
425impl DctJpeg {
426 #[must_use]
428 pub const fn new() -> Self {
429 Self
430 }
431}
432
433impl EmbedTechnique for DctJpeg {
434 fn technique(&self) -> StegoTechnique {
435 StegoTechnique::DctJpeg
436 }
437
438 fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
439 Err(StegoError::UnsupportedCoverType {
440 reason: "DCT JPEG steganography not yet implemented (requires DCT coefficient access)"
441 .to_string(),
442 })
443 }
444
445 fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
446 Err(StegoError::UnsupportedCoverType {
447 reason: "DCT JPEG steganography not yet implemented (requires DCT coefficient access)"
448 .to_string(),
449 })
450 }
451}
452
453impl ExtractTechnique for DctJpeg {
454 fn technique(&self) -> StegoTechnique {
455 StegoTechnique::DctJpeg
456 }
457
458 fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
459 Err(StegoError::UnsupportedCoverType {
460 reason: "DCT JPEG steganography not yet implemented (requires DCT coefficient access)"
461 .to_string(),
462 })
463 }
464}
465
466#[derive(Debug, Default)]
479pub struct PaletteStego;
480
481impl PaletteStego {
482 #[must_use]
484 pub const fn new() -> Self {
485 Self
486 }
487}
488
489impl EmbedTechnique for PaletteStego {
490 fn technique(&self) -> StegoTechnique {
491 StegoTechnique::Palette
492 }
493
494 fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
495 Err(StegoError::UnsupportedCoverType {
496 reason: "Palette steganography not yet implemented (requires palette extraction)"
497 .to_string(),
498 })
499 }
500
501 fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
502 Err(StegoError::UnsupportedCoverType {
503 reason: "Palette steganography not yet implemented (requires palette extraction)"
504 .to_string(),
505 })
506 }
507}
508
509impl ExtractTechnique for PaletteStego {
510 fn technique(&self) -> StegoTechnique {
511 StegoTechnique::Palette
512 }
513
514 fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
515 Err(StegoError::UnsupportedCoverType {
516 reason: "Palette steganography not yet implemented (requires palette extraction)"
517 .to_string(),
518 })
519 }
520}
521
522#[derive(Debug, Default)]
527pub struct LsbAudio;
528
529impl LsbAudio {
530 #[must_use]
532 pub const fn new() -> Self {
533 Self
534 }
535}
536
537impl EmbedTechnique for LsbAudio {
538 fn technique(&self) -> StegoTechnique {
539 StegoTechnique::LsbAudio
540 }
541
542 fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
543 if cover.kind != CoverMediaKind::WavAudio {
545 return Err(StegoError::UnsupportedCoverType {
546 reason: format!("LSB audio requires WAV, got {:?}", cover.kind),
547 });
548 }
549
550 let sample_count = cover.data.len() / 2;
552
553 if sample_count < 32 {
555 return Err(StegoError::MalformedCoverData {
556 reason: "audio too short for LSB embedding (need at least 32 samples)".to_string(),
557 });
558 }
559
560 let capacity_bits =
562 sample_count
563 .checked_sub(32)
564 .ok_or_else(|| StegoError::MalformedCoverData {
565 reason: "capacity calculation underflow".to_string(),
566 })?;
567
568 let bytes = (capacity_bits / 8) as u64;
569
570 Ok(Capacity {
571 bytes,
572 technique: StegoTechnique::LsbAudio,
573 })
574 }
575
576 fn embed(&self, mut cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
577 if cover.kind != CoverMediaKind::WavAudio {
579 return Err(StegoError::UnsupportedCoverType {
580 reason: format!("LSB audio requires WAV, got {:?}", cover.kind),
581 });
582 }
583
584 let cap = self.capacity(&cover)?;
586 let payload_len = payload.as_bytes().len() as u64;
587 if payload_len > cap.bytes {
588 return Err(StegoError::PayloadTooLarge {
589 needed: payload_len,
590 available: cap.bytes,
591 });
592 }
593
594 if payload_len > u64::from(u32::MAX) {
596 return Err(StegoError::PayloadTooLarge {
597 needed: payload_len,
598 available: u64::from(u32::MAX),
599 });
600 }
601
602 let mut samples = cover.data.to_vec();
604
605 #[expect(
607 clippy::cast_possible_truncation,
608 reason = "checked above: payload_len <= u32::MAX"
609 )]
610 let len_bytes = (payload_len as u32).to_be_bytes();
611 for (byte_idx, byte) in len_bytes.iter().enumerate() {
612 for bit_idx in 0..8 {
613 let bit = (byte >> (7 - bit_idx)) & 1;
614 let sample_idx = byte_idx * 8 + bit_idx;
615
616 let byte_pos = sample_idx * 2; let sample =
619 samples
620 .get_mut(byte_pos)
621 .ok_or_else(|| StegoError::MalformedCoverData {
622 reason: "sample index out of bounds".to_string(),
623 })?;
624 *sample = (*sample & 0xFE) | bit;
625 }
626 }
627
628 let payload_bytes = payload.as_bytes();
630 for (byte_idx, byte) in payload_bytes.iter().enumerate() {
631 for bit_idx in 0..8 {
632 let bit = (byte >> (7 - bit_idx)) & 1;
633 let sample_idx = 32 + byte_idx * 8 + bit_idx;
634
635 let byte_pos = sample_idx * 2;
636 let sample =
637 samples
638 .get_mut(byte_pos)
639 .ok_or_else(|| StegoError::MalformedCoverData {
640 reason: "sample index out of bounds".to_string(),
641 })?;
642 *sample = (*sample & 0xFE) | bit;
643 }
644 }
645
646 cover.data = samples.into();
647 Ok(cover)
648 }
649}
650
651impl ExtractTechnique for LsbAudio {
652 fn technique(&self) -> StegoTechnique {
653 StegoTechnique::LsbAudio
654 }
655
656 fn extract(&self, cover: &CoverMedia) -> Result<Payload, StegoError> {
657 if cover.kind != CoverMediaKind::WavAudio {
659 return Err(StegoError::UnsupportedCoverType {
660 reason: format!("LSB audio requires WAV, got {:?}", cover.kind),
661 });
662 }
663
664 let samples = cover.data.as_ref();
665
666 if samples.len() < 64 {
668 return Err(StegoError::MalformedCoverData {
670 reason: "audio too short to extract payload".to_string(),
671 });
672 }
673
674 let mut len_bytes = [0u8; 4];
676 for (byte_idx, len_byte) in len_bytes.iter_mut().enumerate() {
677 for bit_idx in 0..8 {
678 let sample_idx = byte_idx * 8 + bit_idx;
679 let byte_pos = sample_idx * 2;
680
681 let bit = samples
682 .get(byte_pos)
683 .ok_or_else(|| StegoError::MalformedCoverData {
684 reason: "sample index out of bounds".to_string(),
685 })?
686 & 1;
687 *len_byte |= bit << (7 - bit_idx);
688 }
689 }
690
691 let payload_len = u32::from_be_bytes(len_bytes) as usize;
692
693 let max_samples = samples.len() / 2;
695 if payload_len > (max_samples.saturating_sub(32)) / 8 {
696 return Err(StegoError::MalformedCoverData {
697 reason: format!("invalid payload length: {payload_len}"),
698 });
699 }
700
701 let mut payload_bytes = vec![0u8; payload_len];
703 for (byte_idx, payload_byte) in payload_bytes.iter_mut().enumerate() {
704 for bit_idx in 0..8 {
705 let sample_idx = 32 + byte_idx * 8 + bit_idx;
706 let byte_pos = sample_idx * 2;
707
708 let bit = samples
709 .get(byte_pos)
710 .ok_or_else(|| StegoError::MalformedCoverData {
711 reason: "sample index out of bounds".to_string(),
712 })?
713 & 1;
714 *payload_byte |= bit << (7 - bit_idx);
715 }
716 }
717
718 Ok(Payload::from_bytes(payload_bytes))
719 }
720}
721
722#[derive(Debug, Default)]
734pub struct PhaseEncoding;
735
736impl PhaseEncoding {
737 #[must_use]
739 pub const fn new() -> Self {
740 Self
741 }
742}
743
744impl EmbedTechnique for PhaseEncoding {
745 fn technique(&self) -> StegoTechnique {
746 StegoTechnique::PhaseEncoding
747 }
748
749 fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
750 Err(StegoError::UnsupportedCoverType {
751 reason: "Phase encoding not yet implemented (requires FFT/phase manipulation)"
752 .to_string(),
753 })
754 }
755
756 fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
757 Err(StegoError::UnsupportedCoverType {
758 reason: "Phase encoding not yet implemented (requires FFT/phase manipulation)"
759 .to_string(),
760 })
761 }
762}
763
764impl ExtractTechnique for PhaseEncoding {
765 fn technique(&self) -> StegoTechnique {
766 StegoTechnique::PhaseEncoding
767 }
768
769 fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
770 Err(StegoError::UnsupportedCoverType {
771 reason: "Phase encoding not yet implemented (requires FFT/phase manipulation)"
772 .to_string(),
773 })
774 }
775}
776
777#[derive(Debug, Default)]
788pub struct EchoHiding;
789
790impl EchoHiding {
791 #[must_use]
793 pub const fn new() -> Self {
794 Self
795 }
796}
797
798impl EmbedTechnique for EchoHiding {
799 fn technique(&self) -> StegoTechnique {
800 StegoTechnique::EchoHiding
801 }
802
803 fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
804 Err(StegoError::UnsupportedCoverType {
805 reason: "Echo hiding not yet implemented (requires echo synthesis and autocorrelation)"
806 .to_string(),
807 })
808 }
809
810 fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
811 Err(StegoError::UnsupportedCoverType {
812 reason: "Echo hiding not yet implemented (requires echo synthesis and autocorrelation)"
813 .to_string(),
814 })
815 }
816}
817
818impl ExtractTechnique for EchoHiding {
819 fn technique(&self) -> StegoTechnique {
820 StegoTechnique::EchoHiding
821 }
822
823 fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
824 Err(StegoError::UnsupportedCoverType {
825 reason: "Echo hiding not yet implemented (requires echo synthesis and autocorrelation)"
826 .to_string(),
827 })
828 }
829}
830
831#[derive(Debug, Default)]
844pub struct ZeroWidthText;
845
846impl ZeroWidthText {
847 #[must_use]
849 pub const fn new() -> Self {
850 Self
851 }
852}
853
854impl EmbedTechnique for ZeroWidthText {
855 fn technique(&self) -> StegoTechnique {
856 StegoTechnique::ZeroWidthText
857 }
858
859 fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
860 Err(StegoError::UnsupportedCoverType {
861 reason: "Zero-width text steganography not yet implemented (Unicode grapheme segmentation complexity)".to_string(),
862 })
863 }
864
865 fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
866 Err(StegoError::UnsupportedCoverType {
867 reason: "Zero-width text steganography not yet implemented (Unicode grapheme segmentation complexity)".to_string(),
868 })
869 }
870}
871
872impl ExtractTechnique for ZeroWidthText {
873 fn technique(&self) -> StegoTechnique {
874 StegoTechnique::ZeroWidthText
875 }
876
877 fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
878 Err(StegoError::UnsupportedCoverType {
879 reason: "Zero-width text steganography not yet implemented (Unicode grapheme segmentation complexity)".to_string(),
880 })
881 }
882}
883
884pub struct DualPayloadEmbedder;
899
900impl Default for DualPayloadEmbedder {
901 fn default() -> Self {
902 Self
903 }
904}
905
906impl DualPayloadEmbedder {
907 #[must_use]
909 pub const fn new() -> Self {
910 Self
911 }
912
913 fn derive_seed_with_channel(key: &[u8], channel: u8) -> [u8; 32] {
918 let mut hasher = Sha256::new();
919 hasher.update(key);
920 hasher.update([channel]);
921 hasher.finalize().into()
922 }
923
924 fn generate_pattern(seed: [u8; 32], total: usize, count: usize) -> Vec<usize> {
930 let mut rng = ChaCha20Rng::from_seed(seed);
931 let mut indices: Vec<usize> = (0..total).collect();
932 indices.shuffle(&mut rng);
933 indices.truncate(count);
934 indices
935 }
936
937 fn embed_at_positions(
939 cover_data: &mut [u8],
940 payload: &[u8],
941 positions: &[usize],
942 ) -> Result<(), DeniableError> {
943 let payload_bits = payload.len() * 8;
944
945 if payload_bits > positions.len() {
946 return Err(DeniableError::InsufficientCapacity);
947 }
948
949 for (bit_idx, &pos) in positions.iter().enumerate().take(payload_bits) {
951 let payload_byte_idx = bit_idx / 8;
952 let payload_bit_idx = 7 - (bit_idx % 8); let payload_byte = payload
954 .get(payload_byte_idx)
955 .ok_or(DeniableError::InsufficientCapacity)?;
956 let payload_bit = (payload_byte >> payload_bit_idx) & 1;
957
958 let cover_byte_idx = pos / 8;
959 let cover_bit_idx = pos % 8;
960
961 let byte = cover_data
963 .get_mut(cover_byte_idx)
964 .ok_or(DeniableError::InsufficientCapacity)?;
965 if payload_bit == 1 {
966 *byte |= 1 << cover_bit_idx;
967 } else {
968 *byte &= !(1 << cover_bit_idx);
969 }
970 }
971
972 Ok(())
973 }
974
975 fn extract_from_positions(
977 cover_data: &[u8],
978 positions: &[usize],
979 payload_len: usize,
980 ) -> Result<Vec<u8>, DeniableError> {
981 let payload_bits = payload_len * 8;
982
983 if payload_bits > positions.len() {
984 return Err(DeniableError::ExtractionFailed {
985 reason: "insufficient embedding positions for expected payload length".to_string(),
986 });
987 }
988
989 let mut payload = vec![0u8; payload_len];
990
991 for (bit_idx, &pos) in positions.iter().enumerate().take(payload_bits) {
992 let cover_byte_idx = pos / 8;
993 let cover_bit_idx = pos % 8;
994 let cover_byte =
995 cover_data
996 .get(cover_byte_idx)
997 .ok_or_else(|| DeniableError::ExtractionFailed {
998 reason: "cover byte index out of bounds".to_string(),
999 })?;
1000 let cover_bit = (cover_byte >> cover_bit_idx) & 1;
1001
1002 let payload_byte_idx = bit_idx / 8;
1003 let payload_bit_idx = 7 - (bit_idx % 8); if cover_bit == 1 {
1006 let byte = payload.get_mut(payload_byte_idx).ok_or_else(|| {
1007 DeniableError::ExtractionFailed {
1008 reason: "payload byte index out of bounds".to_string(),
1009 }
1010 })?;
1011 *byte |= 1 << payload_bit_idx;
1012 }
1013 }
1014
1015 Ok(payload)
1016 }
1017}
1018
1019impl DeniableEmbedder for DualPayloadEmbedder {
1020 fn embed_dual(
1021 &self,
1022 mut cover: CoverMedia,
1023 pair: &DeniablePayloadPair,
1024 keys: &DeniableKeySet,
1025 _embedder: &dyn EmbedTechnique,
1026 ) -> Result<CoverMedia, DeniableError> {
1027 let cover_bytes = cover.data.len();
1028 let cover_bits = cover_bytes * 8;
1029
1030 let channel_capacity = cover_bits / 2;
1034
1035 let real_total_bits = (pair.real_payload.len() + 4) * 8;
1038 let decoy_total_bits = (pair.decoy_payload.len() + 4) * 8;
1039
1040 if real_total_bits > channel_capacity || decoy_total_bits > channel_capacity {
1041 return Err(DeniableError::InsufficientCapacity);
1042 }
1043
1044 let primary_seed = Self::derive_seed_with_channel(&keys.primary_key, 0);
1048 let decoy_seed = Self::derive_seed_with_channel(&keys.decoy_key, 1);
1049
1050 let primary_positions =
1053 Self::generate_pattern(primary_seed, channel_capacity, real_total_bits)
1054 .into_iter()
1055 .map(|i| i * 2) .collect::<Vec<_>>();
1057
1058 let decoy_positions =
1060 Self::generate_pattern(decoy_seed, channel_capacity, decoy_total_bits)
1061 .into_iter()
1062 .map(|i| i * 2 + 1) .collect::<Vec<_>>();
1064
1065 let real_len = pair.real_payload.len();
1067 let decoy_len = pair.decoy_payload.len();
1068
1069 #[expect(
1070 clippy::cast_possible_truncation,
1071 reason = "payload size checked against u32::MAX in capacity validation"
1072 )]
1073 let mut real_with_header = (real_len as u32).to_be_bytes().to_vec();
1074 real_with_header.extend_from_slice(&pair.real_payload);
1075
1076 #[expect(
1077 clippy::cast_possible_truncation,
1078 reason = "payload size checked against u32::MAX in capacity validation"
1079 )]
1080 let mut decoy_with_header = (decoy_len as u32).to_be_bytes().to_vec();
1081 decoy_with_header.extend_from_slice(&pair.decoy_payload);
1082
1083 let mut cover_data = cover.data.to_vec();
1085
1086 Self::embed_at_positions(&mut cover_data, &real_with_header, &primary_positions).map_err(
1088 |e| DeniableError::EmbedFailed {
1089 reason: format!("real payload embed failed: {e}"),
1090 },
1091 )?;
1092
1093 Self::embed_at_positions(&mut cover_data, &decoy_with_header, &decoy_positions).map_err(
1094 |e| DeniableError::EmbedFailed {
1095 reason: format!("decoy payload embed failed: {e}"),
1096 },
1097 )?;
1098
1099 cover.data = cover_data.into();
1100 Ok(cover)
1101 }
1102
1103 fn extract_with_key(
1104 &self,
1105 stego: &CoverMedia,
1106 key: &[u8],
1107 _extractor: &dyn ExtractTechnique,
1108 ) -> Result<Payload, DeniableError> {
1109 let cover_bytes = stego.data.len();
1110 let cover_bits = cover_bytes * 8;
1111 let channel_capacity = cover_bits / 2;
1112
1113 for channel in 0..2 {
1118 let seed = Self::derive_seed_with_channel(key, channel);
1119
1120 let header_bits = 32;
1122 let header_positions = Self::generate_pattern(seed, channel_capacity, header_bits)
1123 .into_iter()
1124 .map(|i| i * 2 + channel as usize) .collect::<Vec<_>>();
1126
1127 if header_positions.len() < header_bits {
1128 continue; }
1130
1131 let Ok(header_bytes) =
1133 Self::extract_from_positions(stego.data.as_ref(), &header_positions, 4)
1134 else {
1135 continue; };
1137
1138 let Ok(header_arr) = <[u8; 4]>::try_from(header_bytes.as_slice()) else {
1139 continue;
1140 };
1141 let payload_len = u32::from_be_bytes(header_arr) as usize;
1142
1143 if payload_len == 0 {
1146 continue; }
1148
1149 let max_payload_len = channel_capacity / 8;
1150 if payload_len > max_payload_len {
1151 continue; }
1153
1154 let total_bits = (payload_len + 4) * 8;
1156 if total_bits > channel_capacity {
1157 continue; }
1159
1160 let positions = Self::generate_pattern(seed, channel_capacity, total_bits)
1161 .into_iter()
1162 .map(|i| i * 2 + channel as usize)
1163 .collect::<Vec<_>>();
1164
1165 let Ok(with_header) =
1167 Self::extract_from_positions(stego.data.as_ref(), &positions, payload_len + 4)
1168 else {
1169 continue; };
1171
1172 let Ok(extracted_arr) = <[u8; 4]>::try_from(with_header.get(..4).unwrap_or_default())
1174 else {
1175 continue;
1176 };
1177 let extracted_header = u32::from_be_bytes(extracted_arr) as usize;
1178
1179 if extracted_header == payload_len {
1180 let payload_data = with_header.get(4..).unwrap_or_default();
1182 return Ok(Payload::from_bytes(payload_data.to_vec()));
1183 }
1184 }
1185
1186 Err(DeniableError::ExtractionFailed {
1188 reason: "failed to extract valid payload from either channel".to_string(),
1189 })
1190 }
1191}
1192
1193#[cfg(test)]
1201mod tests {
1202 use super::*;
1203 use crate::adapters::pdf::PdfProcessorImpl;
1204 use lopdf::{Document, Object, dictionary};
1205
1206 type TestResult = Result<(), Box<dyn std::error::Error>>;
1207
1208 fn create_test_pdf() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
1209 let mut doc = Document::with_version("1.7");
1210 let catalog_pages = doc.new_object_id();
1211 let first_page = doc.new_object_id();
1212
1213 doc.objects.insert(
1214 first_page,
1215 Object::Dictionary(dictionary! {
1216 "Type" => "Page",
1217 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1218 "Contents" => Object::Reference((first_page.0 + 1, 0)),
1219 }),
1220 );
1221
1222 let content =
1224 b"BT\n/F1 12 Tf\n100 700 Td\n(Test) Tj\n200 650 Td\n(PDF) Tj\nET\n1 0 0 1 0 0 cm\n";
1225 doc.add_object(lopdf::Stream::new(dictionary! {}, content.to_vec()));
1226
1227 doc.objects.insert(
1228 catalog_pages,
1229 Object::Dictionary(dictionary! {
1230 "Type" => "Pages",
1231 "Kids" => vec![Object::Reference(first_page)],
1232 "Count" => 1,
1233 }),
1234 );
1235
1236 let catalog_id = doc.add_object(dictionary! {
1237 "Type" => "Catalog",
1238 "Pages" => Object::Reference(catalog_pages),
1239 });
1240
1241 doc.trailer.set("Root", Object::Reference(catalog_id));
1242
1243 let mut bytes = Vec::new();
1244 doc.save_to(&mut bytes)?;
1245 Ok(bytes)
1246 }
1247
1248 #[test]
1249 fn test_pdf_content_stream_lsb_roundtrip() -> TestResult {
1250 let processor = Box::new(PdfProcessorImpl::default());
1251 let embedder = PdfContentStreamLsb::new(processor);
1252
1253 let pdf_bytes = create_test_pdf()?;
1254 let cover = CoverMedia {
1255 kind: CoverMediaKind::PdfDocument,
1256 data: pdf_bytes.into(),
1257 metadata: std::collections::HashMap::new(),
1258 };
1259
1260 let payload = Payload::from_bytes(vec![0xAB]); let stego = embedder.embed(cover, &payload)?;
1264
1265 let extracted = embedder.extract(&stego)?;
1267 assert_eq!(extracted.as_bytes(), payload.as_bytes());
1268 Ok(())
1269 }
1270
1271 #[test]
1272 fn test_pdf_metadata_embed_roundtrip() -> TestResult {
1273 let processor = Box::new(PdfProcessorImpl::default());
1274 let embedder = PdfMetadataEmbed::new(processor);
1275
1276 let pdf_bytes = create_test_pdf()?;
1277 let cover = CoverMedia {
1278 kind: CoverMediaKind::PdfDocument,
1279 data: pdf_bytes.into(),
1280 metadata: std::collections::HashMap::new(),
1281 };
1282
1283 let payload = Payload::from_bytes(vec![1, 2, 3, 4, 5]); let stego = embedder.embed(cover, &payload)?;
1287
1288 let extracted = embedder.extract(&stego)?;
1290 assert_eq!(extracted.as_bytes(), payload.as_bytes());
1291 Ok(())
1292 }
1293
1294 #[test]
1295 fn test_unsupported_cover_type() {
1296 let processor = Box::new(PdfProcessorImpl::default());
1297 let embedder = PdfContentStreamLsb::new(processor);
1298
1299 let cover = CoverMedia {
1300 kind: CoverMediaKind::PngImage,
1301 data: vec![].into(),
1302 metadata: std::collections::HashMap::new(),
1303 };
1304
1305 let payload = Payload::from_bytes(vec![1, 2, 3]);
1306
1307 let result = embedder.embed(cover, &payload);
1308 assert!(matches!(
1309 result,
1310 Err(StegoError::UnsupportedCoverType { .. })
1311 ));
1312 }
1313
1314 #[test]
1315 fn test_lsb_image_roundtrip_256x256() -> TestResult {
1316 let embedder = LsbImage::new();
1317
1318 let width = 256_u32;
1320 let height = 256_u32;
1321 let pixel_count = width * height;
1322 let data = vec![255u8; (pixel_count * 4) as usize]; let mut metadata = std::collections::HashMap::new();
1325 metadata.insert("width".to_string(), width.to_string());
1326 metadata.insert("height".to_string(), height.to_string());
1327
1328 let cover = CoverMedia {
1329 kind: CoverMediaKind::PngImage,
1330 data: data.into(),
1331 metadata,
1332 };
1333
1334 let payload = Payload::from_bytes(vec![0xAB; 64]);
1336
1337 let stego = embedder.embed(cover.clone(), &payload)?;
1339
1340 let orig_pixels = cover.data.as_ref();
1342 let stego_pixels = stego.data.as_ref();
1343 for (i, (orig, stego_val)) in orig_pixels.iter().zip(stego_pixels.iter()).enumerate() {
1344 let diff = orig.abs_diff(*stego_val);
1345 assert!(
1346 diff <= 1,
1347 "pixel at index {i} changed by more than 1: {orig} -> {stego_val}"
1348 );
1349 }
1350
1351 let extracted = embedder.extract(&stego)?;
1353 assert_eq!(extracted.as_bytes(), payload.as_bytes());
1354 Ok(())
1355 }
1356
1357 #[test]
1358 fn test_lsb_image_capacity_10x10() -> TestResult {
1359 let embedder = LsbImage::new();
1360
1361 let width = 10_u32;
1362 let height = 10_u32;
1363 let pixel_count = width * height;
1364 let data = vec![0u8; (pixel_count * 4) as usize];
1365
1366 let mut metadata = std::collections::HashMap::new();
1367 metadata.insert("width".to_string(), width.to_string());
1368 metadata.insert("height".to_string(), height.to_string());
1369
1370 let cover = CoverMedia {
1371 kind: CoverMediaKind::PngImage,
1372 data: data.into(),
1373 metadata,
1374 };
1375
1376 let cap = embedder.capacity(&cover)?;
1377
1378 assert_eq!(cap.bytes, 33);
1383 assert_eq!(cap.technique, StegoTechnique::LsbImage);
1384 Ok(())
1385 }
1386
1387 #[test]
1388 fn test_lsb_image_insufficient_capacity() {
1389 let embedder = LsbImage::new();
1390
1391 let width = 10_u32;
1392 let height = 10_u32;
1393 let pixel_count = width * height;
1394 let data = vec![0u8; (pixel_count * 4) as usize];
1395
1396 let mut metadata = std::collections::HashMap::new();
1397 metadata.insert("width".to_string(), width.to_string());
1398 metadata.insert("height".to_string(), height.to_string());
1399
1400 let cover = CoverMedia {
1401 kind: CoverMediaKind::PngImage,
1402 data: data.into(),
1403 metadata,
1404 };
1405
1406 let payload = Payload::from_bytes(vec![0xAB; 100]);
1408
1409 let result = embedder.embed(cover, &payload);
1410 assert!(matches!(result, Err(StegoError::PayloadTooLarge { .. })));
1411 }
1412
1413 #[test]
1414 fn test_lsb_image_bmp_support() -> TestResult {
1415 let embedder = LsbImage::new();
1416
1417 let width = 100_u32;
1418 let height = 100_u32;
1419 let pixel_count = width * height;
1420 let data = vec![128u8; (pixel_count * 4) as usize];
1421
1422 let mut metadata = std::collections::HashMap::new();
1423 metadata.insert("width".to_string(), width.to_string());
1424 metadata.insert("height".to_string(), height.to_string());
1425
1426 let cover = CoverMedia {
1427 kind: CoverMediaKind::BmpImage,
1428 data: data.into(),
1429 metadata,
1430 };
1431
1432 let payload = Payload::from_bytes(vec![1, 2, 3, 4, 5]);
1433
1434 let stego = embedder.embed(cover, &payload)?;
1436
1437 let extracted = embedder.extract(&stego)?;
1439 assert_eq!(extracted.as_bytes(), payload.as_bytes());
1440 Ok(())
1441 }
1442
1443 #[test]
1444 fn test_dct_jpeg_stub_returns_not_implemented() {
1445 let embedder = DctJpeg::new();
1446
1447 let cover = CoverMedia {
1448 kind: CoverMediaKind::JpegImage,
1449 data: vec![].into(),
1450 metadata: std::collections::HashMap::new(),
1451 };
1452
1453 let payload = Payload::from_bytes(vec![1, 2, 3]);
1454
1455 let result = embedder.embed(cover.clone(), &payload);
1457 assert!(matches!(
1458 result,
1459 Err(StegoError::UnsupportedCoverType { .. })
1460 ));
1461
1462 let result = embedder.extract(&cover);
1463 assert!(matches!(
1464 result,
1465 Err(StegoError::UnsupportedCoverType { .. })
1466 ));
1467
1468 let result = embedder.capacity(&cover);
1469 assert!(matches!(
1470 result,
1471 Err(StegoError::UnsupportedCoverType { .. })
1472 ));
1473 }
1474
1475 #[test]
1476 fn test_palette_stego_stub_returns_not_implemented() {
1477 let embedder = PaletteStego::new();
1478
1479 let cover = CoverMedia {
1480 kind: CoverMediaKind::GifImage,
1481 data: vec![].into(),
1482 metadata: std::collections::HashMap::new(),
1483 };
1484
1485 let payload = Payload::from_bytes(vec![1, 2, 3]);
1486
1487 let result = embedder.embed(cover.clone(), &payload);
1489 assert!(matches!(
1490 result,
1491 Err(StegoError::UnsupportedCoverType { .. })
1492 ));
1493
1494 let result = embedder.extract(&cover);
1495 assert!(matches!(
1496 result,
1497 Err(StegoError::UnsupportedCoverType { .. })
1498 ));
1499
1500 let result = embedder.capacity(&cover);
1501 assert!(matches!(
1502 result,
1503 Err(StegoError::UnsupportedCoverType { .. })
1504 ));
1505 }
1506
1507 #[test]
1508 fn test_lsb_audio_roundtrip() -> TestResult {
1509 let embedder = LsbAudio::new();
1510
1511 let sample_rate = 44100;
1513 let sample_count = sample_rate; let mut data = Vec::new();
1515 for _ in 0..sample_count {
1516 data.extend_from_slice(&0_i16.to_le_bytes());
1517 }
1518
1519 let mut metadata = std::collections::HashMap::new();
1520 metadata.insert("sample_rate".to_string(), sample_rate.to_string());
1521 metadata.insert("channels".to_string(), "1".to_string());
1522 metadata.insert("bits_per_sample".to_string(), "16".to_string());
1523
1524 let cover = CoverMedia {
1525 kind: CoverMediaKind::WavAudio,
1526 data: data.into(),
1527 metadata,
1528 };
1529
1530 let payload = Payload::from_bytes(vec![0xAB; 512]);
1532
1533 let stego = embedder.embed(cover, &payload)?;
1535
1536 let extracted = embedder.extract(&stego)?;
1538 assert_eq!(extracted.as_bytes(), payload.as_bytes());
1539 Ok(())
1540 }
1541
1542 #[test]
1543 fn test_lsb_audio_capacity() -> TestResult {
1544 let embedder = LsbAudio::new();
1545
1546 let sample_count = 1000;
1548 let mut data = Vec::new();
1549 for _ in 0..sample_count {
1550 data.extend_from_slice(&0_i16.to_le_bytes());
1551 }
1552
1553 let mut metadata = std::collections::HashMap::new();
1554 metadata.insert("sample_rate".to_string(), "44100".to_string());
1555 metadata.insert("channels".to_string(), "1".to_string());
1556 metadata.insert("bits_per_sample".to_string(), "16".to_string());
1557
1558 let cover = CoverMedia {
1559 kind: CoverMediaKind::WavAudio,
1560 data: data.into(),
1561 metadata,
1562 };
1563
1564 let cap = embedder.capacity(&cover)?;
1565
1566 assert_eq!(cap.bytes, 121);
1568 assert_eq!(cap.technique, StegoTechnique::LsbAudio);
1569 Ok(())
1570 }
1571
1572 #[test]
1573 fn test_lsb_audio_insufficient_capacity() {
1574 let embedder = LsbAudio::new();
1575
1576 let sample_count = 100;
1578 let mut data = Vec::new();
1579 for _ in 0..sample_count {
1580 data.extend_from_slice(&0_i16.to_le_bytes());
1581 }
1582
1583 let mut metadata = std::collections::HashMap::new();
1584 metadata.insert("sample_rate".to_string(), "44100".to_string());
1585 metadata.insert("channels".to_string(), "1".to_string());
1586 metadata.insert("bits_per_sample".to_string(), "16".to_string());
1587
1588 let cover = CoverMedia {
1589 kind: CoverMediaKind::WavAudio,
1590 data: data.into(),
1591 metadata,
1592 };
1593
1594 let payload = Payload::from_bytes(vec![0xAB; 100]);
1596
1597 let result = embedder.embed(cover, &payload);
1598 assert!(matches!(result, Err(StegoError::PayloadTooLarge { .. })));
1599 }
1600
1601 #[test]
1602 fn test_phase_encoding_stub_returns_not_implemented() {
1603 let embedder = PhaseEncoding::new();
1604
1605 let mut metadata = std::collections::HashMap::new();
1606 metadata.insert("sample_rate".to_string(), "44100".to_string());
1607 metadata.insert("channels".to_string(), "1".to_string());
1608 metadata.insert("bits_per_sample".to_string(), "16".to_string());
1609
1610 let cover = CoverMedia {
1611 kind: CoverMediaKind::WavAudio,
1612 data: vec![0; 1000].into(),
1613 metadata,
1614 };
1615
1616 let payload = Payload::from_bytes(vec![1, 2, 3]);
1617
1618 let result = embedder.embed(cover.clone(), &payload);
1619 assert!(matches!(
1620 result,
1621 Err(StegoError::UnsupportedCoverType { .. })
1622 ));
1623
1624 let result = embedder.extract(&cover);
1625 assert!(matches!(
1626 result,
1627 Err(StegoError::UnsupportedCoverType { .. })
1628 ));
1629
1630 let result = embedder.capacity(&cover);
1631 assert!(matches!(
1632 result,
1633 Err(StegoError::UnsupportedCoverType { .. })
1634 ));
1635 }
1636
1637 #[test]
1638 fn test_echo_hiding_stub_returns_not_implemented() {
1639 let embedder = EchoHiding::new();
1640
1641 let mut metadata = std::collections::HashMap::new();
1642 metadata.insert("sample_rate".to_string(), "44100".to_string());
1643 metadata.insert("channels".to_string(), "1".to_string());
1644 metadata.insert("bits_per_sample".to_string(), "16".to_string());
1645
1646 let cover = CoverMedia {
1647 kind: CoverMediaKind::WavAudio,
1648 data: vec![0; 1000].into(),
1649 metadata,
1650 };
1651
1652 let payload = Payload::from_bytes(vec![1, 2, 3]);
1653
1654 let result = embedder.embed(cover.clone(), &payload);
1655 assert!(matches!(
1656 result,
1657 Err(StegoError::UnsupportedCoverType { .. })
1658 ));
1659
1660 let result = embedder.extract(&cover);
1661 assert!(matches!(
1662 result,
1663 Err(StegoError::UnsupportedCoverType { .. })
1664 ));
1665
1666 let result = embedder.capacity(&cover);
1667 assert!(matches!(
1668 result,
1669 Err(StegoError::UnsupportedCoverType { .. })
1670 ));
1671 }
1672
1673 #[test]
1674 fn test_zero_width_text_stub_returns_not_implemented() {
1675 let embedder = ZeroWidthText::new();
1676
1677 let cover = CoverMedia {
1678 kind: CoverMediaKind::PlainText,
1679 data: b"Hello, world!".to_vec().into(),
1680 metadata: std::collections::HashMap::new(),
1681 };
1682
1683 let payload = Payload::from_bytes(vec![1, 2, 3]);
1684
1685 let result = embedder.embed(cover.clone(), &payload);
1686 assert!(matches!(
1687 result,
1688 Err(StegoError::UnsupportedCoverType { .. })
1689 ));
1690
1691 let result = embedder.extract(&cover);
1692 assert!(matches!(
1693 result,
1694 Err(StegoError::UnsupportedCoverType { .. })
1695 ));
1696
1697 let result = embedder.capacity(&cover);
1698 assert!(matches!(
1699 result,
1700 Err(StegoError::UnsupportedCoverType { .. })
1701 ));
1702 }
1703
1704 #[test]
1705 fn test_dual_payload_roundtrip() -> TestResult {
1706 let embedder = DualPayloadEmbedder::new();
1707 let lsb_image = LsbImage::new();
1708
1709 let width = 100u32;
1711 let height = 100u32;
1712 let pixel_count = (width * height) as usize;
1713 let data = vec![0u8; pixel_count * 3]; let mut metadata = std::collections::HashMap::new();
1716 metadata.insert("width".to_string(), width.to_string());
1717 metadata.insert("height".to_string(), height.to_string());
1718 metadata.insert("channels".to_string(), "3".to_string());
1719
1720 let cover = CoverMedia {
1721 kind: CoverMediaKind::PngImage,
1722 data: data.into(),
1723 metadata,
1724 };
1725
1726 let real_payload = b"This is the REAL secret message".to_vec();
1728 let decoy_payload = b"This is just a decoy".to_vec();
1729
1730 let pair = DeniablePayloadPair {
1731 real_payload: real_payload.clone(),
1732 decoy_payload: decoy_payload.clone(),
1733 };
1734
1735 let keys = DeniableKeySet {
1737 primary_key: b"primary_key_12345".to_vec(),
1738 decoy_key: b"decoy_key_67890".to_vec(),
1739 };
1740
1741 let stego = embedder.embed_dual(cover, &pair, &keys, &lsb_image)?;
1743
1744 let extracted_real = embedder.extract_with_key(&stego, &keys.primary_key, &lsb_image)?;
1746 assert_eq!(extracted_real.as_bytes(), &real_payload);
1747
1748 let extracted_decoy = embedder.extract_with_key(&stego, &keys.decoy_key, &lsb_image)?;
1750 assert_eq!(extracted_decoy.as_bytes(), &decoy_payload);
1751 Ok(())
1752 }
1753
1754 #[test]
1755 fn test_dual_payload_insufficient_capacity() {
1756 let embedder = DualPayloadEmbedder::new();
1757 let lsb_image = LsbImage::new();
1758
1759 let width = 10u32;
1761 let height = 10u32;
1762 let pixel_count = (width * height) as usize;
1763 let data = vec![0u8; pixel_count * 3];
1764
1765 let mut metadata = std::collections::HashMap::new();
1766 metadata.insert("width".to_string(), width.to_string());
1767 metadata.insert("height".to_string(), height.to_string());
1768 metadata.insert("channels".to_string(), "3".to_string());
1769
1770 let cover = CoverMedia {
1771 kind: CoverMediaKind::PngImage,
1772 data: data.into(),
1773 metadata,
1774 };
1775
1776 let real_payload = vec![0u8; 200];
1778 let decoy_payload = vec![0u8; 200];
1779
1780 let pair = DeniablePayloadPair {
1781 real_payload,
1782 decoy_payload,
1783 };
1784
1785 let keys = DeniableKeySet {
1786 primary_key: b"primary_key".to_vec(),
1787 decoy_key: b"decoy_key".to_vec(),
1788 };
1789
1790 let result = embedder.embed_dual(cover, &pair, &keys, &lsb_image);
1792 assert!(matches!(result, Err(DeniableError::InsufficientCapacity)));
1793 }
1794
1795 #[test]
1796 fn test_dual_payload_different_keys_produce_different_results() -> TestResult {
1797 let embedder = DualPayloadEmbedder::new();
1798 let lsb_image = LsbImage::new();
1799
1800 let width = 100u32;
1802 let height = 100u32;
1803 let pixel_count = (width * height) as usize;
1804 let data = vec![0u8; pixel_count * 3];
1805
1806 let mut metadata = std::collections::HashMap::new();
1807 metadata.insert("width".to_string(), width.to_string());
1808 metadata.insert("height".to_string(), height.to_string());
1809 metadata.insert("channels".to_string(), "3".to_string());
1810
1811 let cover = CoverMedia {
1812 kind: CoverMediaKind::PngImage,
1813 data: data.into(),
1814 metadata,
1815 };
1816
1817 let real_payload = b"Real secret".to_vec();
1818 let decoy_payload = b"Fake data".to_vec();
1819
1820 let pair = DeniablePayloadPair {
1821 real_payload: real_payload.clone(),
1822 decoy_payload: decoy_payload.clone(),
1823 };
1824
1825 let keys = DeniableKeySet {
1826 primary_key: b"key1".to_vec(),
1827 decoy_key: b"key2".to_vec(),
1828 };
1829
1830 let stego = embedder.embed_dual(cover, &pair, &keys, &lsb_image)?;
1831
1832 let extracted1 = embedder.extract_with_key(&stego, &keys.primary_key, &lsb_image)?;
1834
1835 let extracted2 = embedder.extract_with_key(&stego, &keys.decoy_key, &lsb_image)?;
1837
1838 assert_ne!(extracted1.as_bytes(), extracted2.as_bytes());
1840 assert_eq!(extracted1.as_bytes(), &real_payload);
1841 assert_eq!(extracted2.as_bytes(), &decoy_payload);
1842 Ok(())
1843 }
1844
1845 #[test]
1846 fn test_dual_payload_wrong_key_produces_garbage() -> TestResult {
1847 let embedder = DualPayloadEmbedder::new();
1848 let lsb_image = LsbImage::new();
1849
1850 let width = 100u32;
1852 let height = 100u32;
1853 let pixel_count = (width * height) as usize;
1854 let data = vec![0u8; pixel_count * 3];
1855
1856 let mut metadata = std::collections::HashMap::new();
1857 metadata.insert("width".to_string(), width.to_string());
1858 metadata.insert("height".to_string(), height.to_string());
1859 metadata.insert("channels".to_string(), "3".to_string());
1860
1861 let cover = CoverMedia {
1862 kind: CoverMediaKind::PngImage,
1863 data: data.into(),
1864 metadata,
1865 };
1866
1867 let real_payload = b"Real secret message".to_vec();
1868 let decoy_payload = b"Decoy message".to_vec();
1869
1870 let pair = DeniablePayloadPair {
1871 real_payload: real_payload.clone(),
1872 decoy_payload: decoy_payload.clone(),
1873 };
1874
1875 let keys = DeniableKeySet {
1876 primary_key: b"correct_primary".to_vec(),
1877 decoy_key: b"correct_decoy".to_vec(),
1878 };
1879
1880 let stego = embedder.embed_dual(cover, &pair, &keys, &lsb_image)?;
1881
1882 let wrong_key = b"wrong_key";
1884 let result = embedder.extract_with_key(&stego, wrong_key, &lsb_image);
1885
1886 match result {
1889 Ok(extracted) => {
1890 assert_ne!(extracted.as_bytes(), &real_payload);
1892 assert_ne!(extracted.as_bytes(), &decoy_payload);
1893 }
1894 Err(DeniableError::ExtractionFailed { .. }) => {
1895 }
1897 Err(e) => return Err(e.into()),
1898 }
1899 Ok(())
1900 }
1901
1902 #[test]
1905 fn test_lsb_image_wrong_cover_type_embed() {
1906 let embedder = LsbImage::new();
1907 let cover = CoverMedia {
1908 kind: CoverMediaKind::WavAudio,
1909 data: vec![0u8; 100].into(),
1910 metadata: std::collections::HashMap::new(),
1911 };
1912 let payload = Payload::from_bytes(vec![1, 2, 3]);
1913 let result = embedder.embed(cover, &payload);
1914 assert!(matches!(
1915 result,
1916 Err(StegoError::UnsupportedCoverType { .. })
1917 ));
1918 }
1919
1920 #[test]
1921 fn test_lsb_image_wrong_cover_type_extract() {
1922 let embedder = LsbImage::new();
1923 let cover = CoverMedia {
1924 kind: CoverMediaKind::JpegImage,
1925 data: vec![0u8; 100].into(),
1926 metadata: std::collections::HashMap::new(),
1927 };
1928 let result = embedder.extract(&cover);
1929 assert!(matches!(
1930 result,
1931 Err(StegoError::UnsupportedCoverType { .. })
1932 ));
1933 }
1934
1935 #[test]
1936 fn test_lsb_image_missing_width_metadata() {
1937 let embedder = LsbImage::new();
1938 let mut metadata = std::collections::HashMap::new();
1939 metadata.insert("height".to_string(), "100".to_string());
1940 metadata.insert("channels".to_string(), "3".to_string());
1941 let cover = CoverMedia {
1942 kind: CoverMediaKind::PngImage,
1943 data: vec![0u8; 30000].into(),
1944 metadata,
1945 };
1946 let result = embedder.capacity(&cover);
1947 assert!(matches!(result, Err(StegoError::MalformedCoverData { .. })));
1948 }
1949
1950 #[test]
1951 fn test_lsb_image_missing_height_metadata() {
1952 let embedder = LsbImage::new();
1953 let mut metadata = std::collections::HashMap::new();
1954 metadata.insert("width".to_string(), "100".to_string());
1955 metadata.insert("channels".to_string(), "3".to_string());
1956 let cover = CoverMedia {
1957 kind: CoverMediaKind::PngImage,
1958 data: vec![0u8; 30000].into(),
1959 metadata,
1960 };
1961 let result = embedder.capacity(&cover);
1962 assert!(matches!(result, Err(StegoError::MalformedCoverData { .. })));
1963 }
1964
1965 #[test]
1966 fn test_lsb_image_invalid_width_metadata() {
1967 let embedder = LsbImage::new();
1968 let mut metadata = std::collections::HashMap::new();
1969 metadata.insert("width".to_string(), "not_a_number".to_string());
1970 metadata.insert("height".to_string(), "100".to_string());
1971 metadata.insert("channels".to_string(), "3".to_string());
1972 let cover = CoverMedia {
1973 kind: CoverMediaKind::PngImage,
1974 data: vec![0u8; 30000].into(),
1975 metadata,
1976 };
1977 let result = embedder.capacity(&cover);
1978 assert!(matches!(result, Err(StegoError::MalformedCoverData { .. })));
1979 }
1980
1981 #[test]
1982 fn test_lsb_image_wrong_cover_type_capacity() {
1983 let embedder = LsbImage::new();
1984 let cover = CoverMedia {
1985 kind: CoverMediaKind::GifImage,
1986 data: vec![0u8; 100].into(),
1987 metadata: std::collections::HashMap::new(),
1988 };
1989 let result = embedder.capacity(&cover);
1990 assert!(matches!(
1991 result,
1992 Err(StegoError::UnsupportedCoverType { .. })
1993 ));
1994 }
1995
1996 #[test]
1997 fn test_lsb_audio_wrong_cover_type() {
1998 let embedder = LsbAudio::new();
1999 let cover = CoverMedia {
2000 kind: CoverMediaKind::PngImage,
2001 data: vec![0u8; 1000].into(),
2002 metadata: std::collections::HashMap::new(),
2003 };
2004 let payload = Payload::from_bytes(vec![1, 2, 3]);
2005 let result = embedder.embed(cover, &payload);
2006 assert!(matches!(
2007 result,
2008 Err(StegoError::UnsupportedCoverType { .. })
2009 ));
2010 }
2011
2012 #[test]
2013 fn test_lsb_audio_wrong_cover_type_extract() {
2014 let embedder = LsbAudio::new();
2015 let cover = CoverMedia {
2016 kind: CoverMediaKind::PngImage,
2017 data: vec![0u8; 1000].into(),
2018 metadata: std::collections::HashMap::new(),
2019 };
2020 let result = embedder.extract(&cover);
2021 assert!(matches!(
2022 result,
2023 Err(StegoError::UnsupportedCoverType { .. })
2024 ));
2025 }
2026
2027 #[test]
2028 fn test_lsb_audio_wrong_cover_type_capacity() {
2029 let embedder = LsbAudio::new();
2030 let cover = CoverMedia {
2031 kind: CoverMediaKind::PngImage,
2032 data: vec![0u8; 1000].into(),
2033 metadata: std::collections::HashMap::new(),
2034 };
2035 let result = embedder.capacity(&cover);
2036 assert!(matches!(
2037 result,
2038 Err(StegoError::UnsupportedCoverType { .. })
2039 ));
2040 }
2041
2042 #[test]
2043 fn test_pdf_content_stream_wrong_cover_type() {
2044 let processor = Box::new(PdfProcessorImpl::default());
2045 let embedder = PdfContentStreamLsb::new(processor);
2046 let cover = CoverMedia {
2047 kind: CoverMediaKind::PngImage,
2048 data: vec![0u8; 100].into(),
2049 metadata: std::collections::HashMap::new(),
2050 };
2051 let payload = Payload::from_bytes(vec![1, 2, 3]);
2052 let result = embedder.embed(cover, &payload);
2053 assert!(matches!(
2054 result,
2055 Err(StegoError::UnsupportedCoverType { .. })
2056 ));
2057 }
2058
2059 #[test]
2060 fn test_pdf_metadata_wrong_cover_type() {
2061 let processor = Box::new(PdfProcessorImpl::default());
2062 let embedder = PdfMetadataEmbed::new(processor);
2063 let cover = CoverMedia {
2064 kind: CoverMediaKind::PngImage,
2065 data: vec![0u8; 100].into(),
2066 metadata: std::collections::HashMap::new(),
2067 };
2068 let payload = Payload::from_bytes(vec![1, 2, 3]);
2069 let result = embedder.embed(cover, &payload);
2070 assert!(matches!(
2071 result,
2072 Err(StegoError::UnsupportedCoverType { .. })
2073 ));
2074 }
2075
2076 #[test]
2077 fn test_pdf_content_stream_extract_wrong_cover_type() {
2078 let processor = Box::new(PdfProcessorImpl::default());
2079 let embedder = PdfContentStreamLsb::new(processor);
2080 let cover = CoverMedia {
2081 kind: CoverMediaKind::WavAudio,
2082 data: vec![0u8; 100].into(),
2083 metadata: std::collections::HashMap::new(),
2084 };
2085 let result = embedder.extract(&cover);
2086 assert!(matches!(
2087 result,
2088 Err(StegoError::UnsupportedCoverType { .. })
2089 ));
2090 }
2091
2092 #[test]
2093 fn test_pdf_metadata_extract_wrong_cover_type() {
2094 let processor = Box::new(PdfProcessorImpl::default());
2095 let embedder = PdfMetadataEmbed::new(processor);
2096 let cover = CoverMedia {
2097 kind: CoverMediaKind::WavAudio,
2098 data: vec![0u8; 100].into(),
2099 metadata: std::collections::HashMap::new(),
2100 };
2101 let result = embedder.extract(&cover);
2102 assert!(matches!(
2103 result,
2104 Err(StegoError::UnsupportedCoverType { .. })
2105 ));
2106 }
2107
2108 fn dummy_cover() -> CoverMedia {
2111 CoverMedia {
2112 kind: CoverMediaKind::PngImage,
2113 data: vec![0u8; 64].into(),
2114 metadata: std::collections::HashMap::new(),
2115 }
2116 }
2117
2118 #[test]
2119 fn dct_jpeg_stub_capacity_returns_error() {
2120 let dct = DctJpeg::new();
2121 assert!(dct.capacity(&dummy_cover()).is_err());
2122 assert_eq!(EmbedTechnique::technique(&dct), StegoTechnique::DctJpeg);
2123 }
2124
2125 #[test]
2126 fn dct_jpeg_stub_embed_returns_error() {
2127 let dct = DctJpeg::new();
2128 let result = dct.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
2129 assert!(matches!(
2130 result,
2131 Err(StegoError::UnsupportedCoverType { .. })
2132 ));
2133 }
2134
2135 #[test]
2136 fn dct_jpeg_stub_extract_returns_error() {
2137 let dct = DctJpeg::new();
2138 let result = dct.extract(&dummy_cover());
2139 assert!(matches!(
2140 result,
2141 Err(StegoError::UnsupportedCoverType { .. })
2142 ));
2143 assert_eq!(ExtractTechnique::technique(&dct), StegoTechnique::DctJpeg);
2144 }
2145
2146 #[test]
2147 fn palette_stego_stub_capacity_returns_error() {
2148 let pal = PaletteStego::new();
2149 assert!(pal.capacity(&dummy_cover()).is_err());
2150 assert_eq!(EmbedTechnique::technique(&pal), StegoTechnique::Palette);
2151 }
2152
2153 #[test]
2154 fn palette_stego_stub_embed_returns_error() {
2155 let pal = PaletteStego::new();
2156 let result = pal.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
2157 assert!(matches!(
2158 result,
2159 Err(StegoError::UnsupportedCoverType { .. })
2160 ));
2161 }
2162
2163 #[test]
2164 fn palette_stego_stub_extract_returns_error() {
2165 let pal = PaletteStego::new();
2166 let result = pal.extract(&dummy_cover());
2167 assert!(matches!(
2168 result,
2169 Err(StegoError::UnsupportedCoverType { .. })
2170 ));
2171 assert_eq!(ExtractTechnique::technique(&pal), StegoTechnique::Palette);
2172 }
2173
2174 #[test]
2175 fn phase_encoding_stub_capacity_returns_error() {
2176 let pe = PhaseEncoding::new();
2177 assert!(pe.capacity(&dummy_cover()).is_err());
2178 assert_eq!(
2179 EmbedTechnique::technique(&pe),
2180 StegoTechnique::PhaseEncoding
2181 );
2182 }
2183
2184 #[test]
2185 fn phase_encoding_stub_embed_returns_error() {
2186 let pe = PhaseEncoding::new();
2187 let result = pe.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
2188 assert!(matches!(
2189 result,
2190 Err(StegoError::UnsupportedCoverType { .. })
2191 ));
2192 }
2193
2194 #[test]
2195 fn phase_encoding_stub_extract_returns_error() {
2196 let pe = PhaseEncoding::new();
2197 let result = pe.extract(&dummy_cover());
2198 assert!(matches!(
2199 result,
2200 Err(StegoError::UnsupportedCoverType { .. })
2201 ));
2202 assert_eq!(
2203 ExtractTechnique::technique(&pe),
2204 StegoTechnique::PhaseEncoding
2205 );
2206 }
2207
2208 #[test]
2209 fn echo_hiding_stub_capacity_returns_error() {
2210 let eh = EchoHiding::new();
2211 assert!(eh.capacity(&dummy_cover()).is_err());
2212 assert_eq!(EmbedTechnique::technique(&eh), StegoTechnique::EchoHiding);
2213 }
2214
2215 #[test]
2216 fn echo_hiding_stub_embed_returns_error() {
2217 let eh = EchoHiding::new();
2218 let result = eh.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
2219 assert!(matches!(
2220 result,
2221 Err(StegoError::UnsupportedCoverType { .. })
2222 ));
2223 }
2224
2225 #[test]
2226 fn echo_hiding_stub_extract_returns_error() {
2227 let eh = EchoHiding::new();
2228 let result = eh.extract(&dummy_cover());
2229 assert!(matches!(
2230 result,
2231 Err(StegoError::UnsupportedCoverType { .. })
2232 ));
2233 assert_eq!(ExtractTechnique::technique(&eh), StegoTechnique::EchoHiding);
2234 }
2235
2236 #[test]
2237 fn zero_width_text_stub_capacity_returns_error() {
2238 let zwt = ZeroWidthText::new();
2239 assert!(zwt.capacity(&dummy_cover()).is_err());
2240 assert_eq!(
2241 EmbedTechnique::technique(&zwt),
2242 StegoTechnique::ZeroWidthText
2243 );
2244 }
2245
2246 #[test]
2247 fn zero_width_text_stub_embed_returns_error() {
2248 let zwt = ZeroWidthText::new();
2249 let result = zwt.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
2250 assert!(matches!(
2251 result,
2252 Err(StegoError::UnsupportedCoverType { .. })
2253 ));
2254 }
2255
2256 #[test]
2257 fn zero_width_text_stub_extract_returns_error() {
2258 let zwt = ZeroWidthText::new();
2259 let result = zwt.extract(&dummy_cover());
2260 assert!(matches!(
2261 result,
2262 Err(StegoError::UnsupportedCoverType { .. })
2263 ));
2264 assert_eq!(
2265 ExtractTechnique::technique(&zwt),
2266 StegoTechnique::ZeroWidthText
2267 );
2268 }
2269
2270 #[test]
2271 fn lsb_image_insufficient_capacity() {
2272 let embedder = LsbImage::new();
2273 let mut metadata = std::collections::HashMap::new();
2274 metadata.insert("width".to_string(), "2".to_string());
2275 metadata.insert("height".to_string(), "2".to_string());
2276 metadata.insert("channels".to_string(), "3".to_string());
2277 let cover = CoverMedia {
2278 kind: CoverMediaKind::PngImage,
2279 data: vec![0u8; 12].into(),
2281 metadata,
2282 };
2283 let payload = Payload::from_bytes(vec![0xAB; 100]);
2285 let result = embedder.embed(cover, &payload);
2286 assert!(result.is_err());
2287 }
2288
2289 #[test]
2290 fn lsb_audio_insufficient_capacity() {
2291 let embedder = LsbAudio::new();
2292 let mut metadata = std::collections::HashMap::new();
2293 metadata.insert("bits_per_sample".to_string(), "16".to_string());
2294 let cover = CoverMedia {
2295 kind: CoverMediaKind::WavAudio,
2296 data: vec![0u8; 10].into(),
2298 metadata,
2299 };
2300 let payload = Payload::from_bytes(vec![0xAB; 100]);
2301 let result = embedder.embed(cover, &payload);
2302 assert!(result.is_err());
2303 }
2304}