1use crate::codec::jpeg::JpegImage;
15use crate::stego::cost::uniward::compute_positions_streaming;
16use crate::stego::crypto;
17use crate::stego::error::StegoError;
18use crate::stego::frame::{self, MAX_FRAME_BITS};
19use crate::stego::payload::{self, FileEntry, PayloadData};
20use crate::stego::permute;
21use crate::stego::progress;
22use crate::stego::side_info::{self, SideInfo};
23use crate::stego::shadow;
24use crate::stego::quality::{self, EncodeQuality, GhostMetrics};
25use crate::stego::stc::{embed, extract, hhat};
26
27const STC_H: usize = 7;
29
30const STC_POSITION_LIMIT: usize = 500_000_000;
34
35const PARSE_STEPS: u32 = 5;
38
39pub const GHOST_DECODE_STEPS: u32 = PARSE_STEPS + crate::stego::cost::uniward::UNIWARD_PROGRESS_STEPS + 2;
43
44pub const GHOST_ENCODE_STEPS: u32 =
48 PARSE_STEPS
49 + crate::stego::cost::uniward::UNIWARD_PROGRESS_STEPS
50 + crate::stego::stc::embed::STC_PROGRESS_STEPS
51 + 2
52 + crate::codec::jpeg::scan::JPEG_WRITE_STEPS;
53
54fn compute_stc_params(n: usize) -> Result<(usize, usize, usize), StegoError> {
75 let m_max = MAX_FRAME_BITS.min(n);
76 if m_max == 0 {
77 return Err(StegoError::ImageTooSmall);
78 }
79 let w = n / m_max; let n_used = m_max * w;
81
82 if n_used > STC_POSITION_LIMIT {
83 return Err(StegoError::ImageTooLarge);
84 }
85
86 Ok((w, n_used, m_max))
87}
88
89pub fn ghost_encode(
106 image_bytes: &[u8],
107 message: &str,
108 passphrase: &str,
109) -> Result<Vec<u8>, StegoError> {
110 ghost_encode_impl(image_bytes, message, &[], passphrase, None, None)
111 .map(|(bytes, _)| bytes)
112}
113
114pub fn ghost_encode_with_quality(
116 image_bytes: &[u8],
117 message: &str,
118 passphrase: &str,
119) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
120 ghost_encode_impl(image_bytes, message, &[], passphrase, None, None)
121}
122
123pub fn ghost_encode_with_files(
128 image_bytes: &[u8],
129 message: &str,
130 files: &[FileEntry],
131 passphrase: &str,
132) -> Result<Vec<u8>, StegoError> {
133 ghost_encode_impl(image_bytes, message, files, passphrase, None, None)
134 .map(|(bytes, _)| bytes)
135}
136
137pub fn ghost_encode_with_files_quality(
139 image_bytes: &[u8],
140 message: &str,
141 files: &[FileEntry],
142 passphrase: &str,
143) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
144 ghost_encode_impl(image_bytes, message, files, passphrase, None, None)
145}
146
147pub fn ghost_encode_si(
163 image_bytes: &[u8],
164 raw_pixels_rgb: &[u8],
165 pixel_width: u32,
166 pixel_height: u32,
167 message: &str,
168 passphrase: &str,
169) -> Result<Vec<u8>, StegoError> {
170 ghost_encode_si_with_files(
171 image_bytes, raw_pixels_rgb, pixel_width, pixel_height,
172 message, &[], passphrase,
173 )
174}
175
176pub fn ghost_encode_si_with_quality(
178 image_bytes: &[u8],
179 raw_pixels_rgb: &[u8],
180 pixel_width: u32,
181 pixel_height: u32,
182 message: &str,
183 passphrase: &str,
184) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
185 ghost_encode_si_with_files_quality(
186 image_bytes, raw_pixels_rgb, pixel_width, pixel_height,
187 message, &[], passphrase,
188 )
189}
190
191pub fn ghost_encode_si_with_files(
193 image_bytes: &[u8],
194 raw_pixels_rgb: &[u8],
195 pixel_width: u32,
196 pixel_height: u32,
197 message: &str,
198 files: &[FileEntry],
199 passphrase: &str,
200) -> Result<Vec<u8>, StegoError> {
201 ghost_encode_si_with_files_quality(
202 image_bytes, raw_pixels_rgb, pixel_width, pixel_height,
203 message, files, passphrase,
204 ).map(|(bytes, _)| bytes)
205}
206
207pub fn ghost_encode_si_with_files_quality(
209 image_bytes: &[u8],
210 raw_pixels_rgb: &[u8],
211 pixel_width: u32,
212 pixel_height: u32,
213 message: &str,
214 files: &[FileEntry],
215 passphrase: &str,
216) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
217 let img = JpegImage::from_bytes(image_bytes)?;
219 let fi = img.frame_info();
220 crate::stego::validate_encode_dimensions(fi.width as u32, fi.height as u32)?;
221
222 if img.num_components() == 0 {
223 return Err(StegoError::NoLuminanceChannel);
224 }
225
226 let qt_id = fi.components[0].quant_table_id as usize;
227 let qt = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
228
229 let si = SideInfo::compute(
230 raw_pixels_rgb,
231 pixel_width,
232 pixel_height,
233 img.dct_grid(0),
234 &qt.values,
235 );
236
237 ghost_encode_impl(image_bytes, message, files, passphrase, Some(si), Some(img))
239}
240
241pub struct ShadowLayer {
243 pub message: String,
245 pub passphrase: String,
247 pub files: Vec<FileEntry>,
249}
250
251pub const GHOST_ENCODE_WITH_SHADOWS_STEPS: u32 =
258 PARSE_STEPS
259 + crate::stego::cost::uniward::UNIWARD_PROGRESS_STEPS
260 + 2 + crate::stego::stc::embed::STC_PROGRESS_STEPS
262 + crate::stego::cost::uniward::UNIWARD_PROGRESS_STEPS + crate::codec::jpeg::scan::JPEG_WRITE_STEPS;
264
265const CASCADE: &[(usize, usize)] = &[
271 (1, 4),
274 (1, 8),
275 (1, 16),
276 (1, 32),
277 (1, 64),
278 (1, 128),
279 (2, 16), (5, 16), (10, 16),
281 (2, 32), (5, 32),
282 (2, 64),
283];
284
285pub fn ghost_encode_with_shadows(
291 image_bytes: &[u8],
292 message: &str,
293 files: &[FileEntry],
294 passphrase: &str,
295 shadows: &[ShadowLayer],
296 si: Option<SideInfo>,
297) -> Result<Vec<u8>, StegoError> {
298 ghost_encode_with_shadows_impl(image_bytes, message, files, passphrase, shadows, si, None)
299 .map(|(bytes, _)| bytes)
300}
301
302pub fn ghost_encode_with_shadows_quality(
304 image_bytes: &[u8],
305 message: &str,
306 files: &[FileEntry],
307 passphrase: &str,
308 shadows: &[ShadowLayer],
309 si: Option<SideInfo>,
310) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
311 ghost_encode_with_shadows_impl(image_bytes, message, files, passphrase, shadows, si, None)
312}
313
314pub fn ghost_encode_si_with_shadows(
316 image_bytes: &[u8],
317 raw_pixels_rgb: &[u8],
318 pixel_width: u32,
319 pixel_height: u32,
320 message: &str,
321 files: &[FileEntry],
322 passphrase: &str,
323 shadows: &[ShadowLayer],
324) -> Result<Vec<u8>, StegoError> {
325 ghost_encode_si_with_shadows_quality(
326 image_bytes, raw_pixels_rgb, pixel_width, pixel_height,
327 message, files, passphrase, shadows,
328 ).map(|(bytes, _)| bytes)
329}
330
331pub fn ghost_encode_si_with_shadows_quality(
333 image_bytes: &[u8],
334 raw_pixels_rgb: &[u8],
335 pixel_width: u32,
336 pixel_height: u32,
337 message: &str,
338 files: &[FileEntry],
339 passphrase: &str,
340 shadows: &[ShadowLayer],
341) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
342 let img = JpegImage::from_bytes(image_bytes)?;
343 let fi = img.frame_info();
344 crate::stego::validate_encode_dimensions(fi.width as u32, fi.height as u32)?;
345
346 if img.num_components() == 0 {
347 return Err(StegoError::NoLuminanceChannel);
348 }
349
350 let qt_id = fi.components[0].quant_table_id as usize;
351 let qt = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
352
353 let si = SideInfo::compute(
354 raw_pixels_rgb,
355 pixel_width,
356 pixel_height,
357 img.dct_grid(0),
358 &qt.values,
359 );
360
361 ghost_encode_with_shadows_impl(image_bytes, message, files, passphrase, shadows, Some(si), Some(img))
362}
363
364fn ghost_encode_with_shadows_impl(
365 image_bytes: &[u8],
366 message: &str,
367 files: &[FileEntry],
368 passphrase: &str,
369 shadows: &[ShadowLayer],
370 si: Option<SideInfo>,
371 pre_parsed: Option<JpegImage>,
372) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
373 let cascade_budget = CASCADE.len() as u32 * (
376 crate::stego::stc::embed::STC_PROGRESS_STEPS
377 + crate::stego::cost::uniward::UNIWARD_PROGRESS_STEPS
378 );
379 progress::init(GHOST_ENCODE_WITH_SHADOWS_STEPS + cascade_budget);
380
381 {
383 let mut all_passes: Vec<&str> = vec![passphrase];
384 for s in shadows {
385 all_passes.push(&s.passphrase);
386 }
387 for i in 0..all_passes.len() {
388 for j in (i + 1)..all_passes.len() {
389 if all_passes[i] == all_passes[j] {
390 return Err(StegoError::DuplicatePassphrase);
391 }
392 }
393 }
394 }
395
396 let primary_payload_size = payload::compressed_payload_size(message, files);
399 let mut swap_idx: Option<usize> = None;
400 for (i, s) in shadows.iter().enumerate() {
401 let shadow_size = payload::compressed_payload_size(&s.message, &s.files);
402 if shadow_size > primary_payload_size {
403 if let Some(prev) = swap_idx {
404 let prev_size = payload::compressed_payload_size(&shadows[prev].message, &shadows[prev].files);
405 if shadow_size > prev_size {
406 swap_idx = Some(i);
407 }
408 } else {
409 swap_idx = Some(i);
410 }
411 }
412 }
413
414 let primary_as_shadow;
417 let (eff_message, eff_files, eff_passphrase, eff_shadows);
418 if let Some(idx) = swap_idx {
419 eff_message = shadows[idx].message.as_str();
420 eff_files = &shadows[idx].files[..];
421 eff_passphrase = shadows[idx].passphrase.as_str();
422 primary_as_shadow = ShadowLayer {
424 message: message.to_string(),
425 passphrase: passphrase.to_string(),
426 files: files.to_vec(),
427 };
428 let mut new_shadows: Vec<&ShadowLayer> = Vec::with_capacity(shadows.len());
429 new_shadows.push(&primary_as_shadow);
430 for (i, s) in shadows.iter().enumerate() {
431 if i != idx {
432 new_shadows.push(s);
433 }
434 }
435 eff_shadows = new_shadows;
436 } else {
437 eff_message = message;
438 eff_files = files;
439 eff_passphrase = passphrase;
440 eff_shadows = shadows.iter().collect();
441 };
442
443 let payload_bytes = payload::encode_payload(eff_message, eff_files)?;
445
446 let mut img = match pre_parsed {
447 Some(img) => img,
448 None => JpegImage::from_bytes(image_bytes)?,
449 };
450 progress::advance_by(PARSE_STEPS);
451 let fi = img.frame_info();
452 crate::stego::validate_encode_dimensions(fi.width as u32, fi.height as u32)?;
453
454 if img.num_components() == 0 {
455 return Err(StegoError::NoLuminanceChannel);
456 }
457
458 #[cfg(feature = "parallel")]
461 let key_thread = {
462 let pass = eff_passphrase.to_string();
463 std::thread::spawn(move || crypto::derive_structural_key(&pass))
464 };
465
466 let qt_id = img.frame_info().components[0].quant_table_id as usize;
467 let qt = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
468 let si_ref = si.as_ref().map(|s| (s, img.dct_grid(0)));
469 let mut positions = compute_positions_streaming(img.dct_grid(0), qt, si_ref)?;
470
471 positions.sort_by(|a, b| a.cost.total_cmp(&b.cost));
475
476 let mut shadow_states: Vec<shadow::ShadowState> = Vec::new();
477 if !eff_shadows.is_empty() {
478 let initial_parity = 4;
479 for s in eff_shadows.iter() {
480 let state = shadow::prepare_shadow(
481 &positions,
482 &s.passphrase,
483 &s.message,
484 &s.files,
485 initial_parity,
486 )?;
487 shadow_states.push(state);
488 }
489 }
490 progress::advance(); positions.sort_by_key(|p| p.flat_idx);
495
496 let original_y = img.dct_grid(0).clone();
498
499 #[cfg(feature = "parallel")]
501 let structural_key = key_thread.join().expect("key derivation thread")?;
502 #[cfg(not(feature = "parallel"))]
503 let structural_key = crypto::derive_structural_key(eff_passphrase)?;
504 let perm_seed: [u8; 32] = structural_key[..32].try_into().unwrap();
505 let hhat_seed: [u8; 32] = structural_key[32..].try_into().unwrap();
506
507 permute::permute_positions(&mut positions, &perm_seed);
508 let n = positions.len();
509
510 progress::advance();
512
513 let (ciphertext, nonce, salt) = crypto::encrypt(&payload_bytes, eff_passphrase)?;
514 let frame_bytes = frame::build_frame(payload_bytes.len(), &salt, &nonce, &ciphertext);
515 let frame_bits = frame::bytes_to_bits(&frame_bytes);
516 let m = frame_bits.len();
517
518 let w = (n / m).clamp(1, 10);
520 let m_max = n / w;
521 let n_used = m_max * w;
522
523 if m > m_max {
524 return Err(StegoError::MessageTooLarge);
525 }
526
527 if w < 2 && !shadow_states.is_empty() {
530 return Err(StegoError::MessageTooLarge);
531 }
532
533 if n_used > STC_POSITION_LIMIT {
534 return Err(StegoError::ImageTooLarge);
535 }
536
537 positions.truncate(n_used);
538 let hhat_matrix = hhat::generate_hhat(STC_H, w, &hhat_seed);
539
540 let shadow_inf_costs = build_inf_cost_set(w, &shadow_states);
544
545 let median_cost = {
547 let mut finite_costs: Vec<f32> = positions.iter()
548 .map(|p| p.cost)
549 .filter(|c| c.is_finite())
550 .collect();
551 if finite_costs.is_empty() {
552 0.0f32
553 } else {
554 let mid = finite_costs.len() / 2;
555 finite_costs.select_nth_unstable_by(mid, f32::total_cmp);
556 finite_costs[mid]
557 }
558 };
559 let is_si = si.is_some();
560 let grid_ref = img.dct_grid(0);
561 let total_coefficients = grid_ref.blocks_wide() * grid_ref.blocks_tall() * 64;
562
563 let shadow_modifications: usize = shadow_states.iter()
567 .map(|s| s.n_total)
568 .sum();
569
570 let mut stc_total_cost: f64 = 0.0;
572 let mut stc_num_modifications: usize = 0;
573
574 if shadow_states.is_empty() {
575 let (tc, nm) = run_stc_pass(&mut img, &original_y, &positions, &[],
577 &frame_bits, &hhat_matrix, w, &si, &None)?;
578 stc_total_cost = tc;
579 stc_num_modifications = nm;
580 } else {
581 let (tc, nm) = run_stc_pass(&mut img, &original_y, &positions, &shadow_states,
582 &frame_bits, &hhat_matrix, w, &si, &shadow_inf_costs)?;
583 stc_total_cost = tc;
584 stc_num_modifications = nm;
585
586 let all_fraction_1 = shadow_states.iter().all(|s| s.cost_fraction == 1);
591
592 if !all_fraction_1 {
597 let qt_verify = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
598 let mut stego_y_positions = compute_positions_streaming(img.dct_grid(0), qt_verify, None)?;
599 stego_y_positions.sort_by(|a, b| a.cost.total_cmp(&b.cost));
600
601 if !verify_all_shadows_decoder_side(&img, &shadow_states, &eff_shadows, &stego_y_positions) {
602 let mut cascade_positions = positions.clone();
610 cascade_positions.sort_by(|a, b| a.cost.total_cmp(&b.cost));
611
612 let mut verified = false;
613
614 #[cfg(feature = "parallel")]
615 {
616 use std::sync::atomic::{AtomicUsize, Ordering};
617 use rayon::prelude::*;
618
619 let best_fraction = AtomicUsize::new(0);
623
624 for &parity in &[4, 8, 16, 32, 64, 128] {
625 let fractions_for_parity: Vec<usize> = CASCADE.iter()
626 .filter(|&&(_, p)| p == parity)
627 .map(|&(f, _)| f)
628 .collect();
629 if fractions_for_parity.is_empty() { continue; }
630
631 let has_fraction_1 = fractions_for_parity.contains(&1);
632
633 let successes: Vec<(usize, crate::codec::jpeg::JpegImage, Vec<shadow::ShadowState>, f64, usize)> =
634 fractions_for_parity.par_iter().filter_map(|&fraction| {
635 if best_fraction.load(Ordering::Relaxed) > fraction { return None; }
637
638 let mut local_states = shadow_states.clone();
640 let local_cascade_positions = cascade_positions.clone();
641 for state in local_states.iter_mut() {
642 if shadow::rebuild_shadow(state, &local_cascade_positions, parity, fraction).is_err() {
643 return None;
644 }
645 }
646
647 if best_fraction.load(Ordering::Relaxed) > fraction { return None; }
649
650 let mut local_img = img.clone();
651 let new_inf_costs = build_inf_cost_set(w, &local_states);
652 let (local_tc, local_nm) = match run_stc_pass(&mut local_img, &original_y, &positions, &local_states,
653 &frame_bits, &hhat_matrix, w, &si, &new_inf_costs) {
654 Ok(v) => v,
655 Err(_) => return None,
656 };
657
658 if best_fraction.load(Ordering::Relaxed) > fraction { return None; }
660
661 let qt_re = match local_img.quant_table(qt_id) {
662 Some(qt) => qt,
663 None => return None,
664 };
665 let mut local_stego_positions = match compute_positions_streaming(local_img.dct_grid(0), qt_re, None) {
666 Ok(p) => p,
667 Err(_) => return None,
668 };
669 local_stego_positions.sort_by(|a, b| a.cost.total_cmp(&b.cost));
670
671 if verify_all_shadows_decoder_side(&local_img, &local_states, &eff_shadows, &local_stego_positions) {
672 best_fraction.fetch_max(fraction, Ordering::Relaxed);
674 Some((fraction, local_img, local_states, local_tc, local_nm))
675 } else {
676 None
677 }
678 }).collect();
679
680 if !successes.is_empty() {
681 let best = successes.into_iter().max_by_key(|(f, _, _, _, _)| *f).unwrap();
683 img = best.1;
684 shadow_states = best.2;
685 stc_total_cost = best.3;
686 stc_num_modifications = best.4;
687 verified = true;
688 break;
689 }
690
691 if has_fraction_1 && parity == 128 {
694 break;
695 }
696 }
697 }
698
699 #[cfg(not(feature = "parallel"))]
700 {
701 let mut last_fraction_1_failed_parity: Option<usize> = None;
702 for &(frac, par) in CASCADE {
703 if frac > 1 {
706 if let Some(failed_par) = last_fraction_1_failed_parity {
707 if par <= failed_par {
708 continue;
709 }
710 }
711 }
712
713 for state in shadow_states.iter_mut() {
714 shadow::rebuild_shadow(state, &cascade_positions, par, frac)?;
715 }
716 let new_inf_costs = build_inf_cost_set(w, &shadow_states);
717 let (tc, nm) = run_stc_pass(&mut img, &original_y, &positions, &shadow_states,
718 &frame_bits, &hhat_matrix, w, &si, &new_inf_costs)?;
719 stc_total_cost = tc;
720 stc_num_modifications = nm;
721
722 let qt_re = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
724 stego_y_positions = compute_positions_streaming(img.dct_grid(0), qt_re, None)?;
725 stego_y_positions.sort_by(|a, b| a.cost.total_cmp(&b.cost));
726
727 if verify_all_shadows_decoder_side(&img, &shadow_states, &eff_shadows, &stego_y_positions) {
728 verified = true;
729 break;
730 }
731
732 if frac == 1 {
734 last_fraction_1_failed_parity = Some(par);
735 }
736
737 if frac == 1 && par == 128 {
739 break;
740 }
741 }
742 }
743
744 if !verified {
745 return Err(StegoError::MessageTooLarge);
746 }
747 }
748 } }
750
751 let encode_quality = quality::ghost_stealth_score(&GhostMetrics {
753 num_modifications: stc_num_modifications,
754 n_used,
755 w,
756 total_cost: stc_total_cost,
757 median_cost,
758 is_si,
759 shadow_modifications,
760 total_coefficients,
761 });
762
763 drop(positions);
765 drop(original_y);
766 drop(shadow_states);
767 drop(shadow_inf_costs);
768 drop(si);
769
770 let progress_cb = || progress::advance();
772 let stego_bytes = if let Ok(bytes) = img.to_bytes_with_progress(Some(&progress_cb)) { bytes } else {
773 img.rebuild_huffman_tables();
774 img.to_bytes_with_progress(Some(&progress_cb)).map_err(StegoError::InvalidJpeg)?
775 };
776
777 Ok((stego_bytes, encode_quality))
778}
779
780fn run_stc_pass(
784 img: &mut JpegImage,
785 original_y: &crate::codec::jpeg::dct::DctGrid,
786 positions: &[permute::CoeffPos],
787 shadow_states: &[shadow::ShadowState],
788 message_bits: &[u8],
789 hhat_matrix: &[Vec<u32>],
790 w: usize,
791 si: &Option<SideInfo>,
792 shadow_inf_costs: &Option<std::collections::HashSet<u32>>,
793) -> Result<(f64, usize), StegoError> {
794 *img.dct_grid_mut(0) = original_y.clone();
795
796 for state in shadow_states {
797 shadow::embed_shadow_lsb(img, state);
798 }
799
800 let grid = img.dct_grid(0);
801 let cover_bits: Vec<u8> = positions.iter().map(|p| {
802 let coeff = flat_get(grid, p.flat_idx as usize);
803 (coeff.unsigned_abs() & 1) as u8
804 }).collect();
805
806 let costs: Vec<f32> = if let Some(inf_set) = shadow_inf_costs {
808 positions.iter().map(|p| {
809 if inf_set.contains(&p.flat_idx) {
810 f32::INFINITY
811 } else {
812 p.cost
813 }
814 }).collect()
815 } else {
816 positions.iter().map(|p| p.cost).collect()
817 };
818
819 let result = embed::stc_embed(&cover_bits, &costs, message_bits, hhat_matrix, STC_H, w);
820 progress::check_cancelled()?;
821 let result = result.ok_or(StegoError::MessageTooLarge)?;
822
823 let total_cost = result.total_cost;
824 let num_modifications = result.num_modifications;
825
826 apply_stc_changes(img, positions, &cover_bits, &result.stego_bits, si);
827
828 Ok((total_cost, num_modifications))
829}
830
831fn apply_stc_changes(
833 img: &mut JpegImage,
834 positions: &[permute::CoeffPos],
835 cover_bits: &[u8],
836 stego_bits: &[u8],
837 si: &Option<SideInfo>,
838) {
839 let grid_mut = img.dct_grid_mut(0);
840 for (idx, pos) in positions.iter().enumerate() {
841 if cover_bits[idx] != stego_bits[idx] {
842 let fi = pos.flat_idx as usize;
843 let coeff = flat_get(grid_mut, fi);
844 let modified = if let Some(side_info) = si {
845 side_info::si_modify_coefficient(coeff, side_info.error_at(fi))
846 } else {
847 side_info::nsf5_modify_coefficient(coeff)
848 };
849 flat_set(grid_mut, fi, modified);
850 }
851 }
852}
853
854fn verify_all_shadows_decoder_side(
859 img: &JpegImage,
860 shadow_states: &[shadow::ShadowState],
861 shadows: &[&ShadowLayer],
862 stego_y_positions_sorted: &[permute::CoeffPos],
863) -> bool {
864 for (i, state) in shadow_states.iter().enumerate() {
865 if shadow::verify_shadow_decoder_side(
866 img, state, &shadows[i].passphrase, stego_y_positions_sorted,
867 ).is_err() {
868 return false;
869 }
870 }
871 true
872}
873
874fn build_inf_cost_set(w: usize, shadow_states: &[shadow::ShadowState]) -> Option<std::collections::HashSet<u32>> {
876 if w >= 2 && !shadow_states.is_empty() {
877 let mut set = std::collections::HashSet::new();
878 for state in shadow_states {
879 for pos in &state.positions {
880 set.insert(pos.flat_idx);
881 }
882 }
883 Some(set)
884 } else {
885 None
886 }
887}
888
889fn ghost_encode_impl(
890 image_bytes: &[u8],
891 message: &str,
892 files: &[FileEntry],
893 passphrase: &str,
894 si: Option<SideInfo>,
895 pre_parsed: Option<JpegImage>,
896) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
897 progress::init(GHOST_ENCODE_STEPS);
899
900 let payload_bytes = payload::encode_payload(message, files)?;
902
903 let mut img = match pre_parsed {
904 Some(img) => img,
905 None => JpegImage::from_bytes(image_bytes)?,
906 };
907 progress::advance_by(PARSE_STEPS);
908
909 let fi = img.frame_info();
911 crate::stego::validate_encode_dimensions(fi.width as u32, fi.height as u32)?;
912
913 if img.num_components() == 0 {
914 return Err(StegoError::NoLuminanceChannel);
915 }
916
917 #[cfg(feature = "parallel")]
920 let key_thread = {
921 let pass = passphrase.to_string();
922 std::thread::spawn(move || crypto::derive_structural_key(&pass))
923 };
924
925 let qt_id = img.frame_info().components[0].quant_table_id as usize;
926 let qt = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
927 let si_ref = si.as_ref().map(|s| (s, img.dct_grid(0)));
928 let mut positions = compute_positions_streaming(img.dct_grid(0), qt, si_ref)?;
929
930 #[cfg(feature = "parallel")]
932 let structural_key = key_thread.join().expect("key derivation thread")?;
933 #[cfg(not(feature = "parallel"))]
934 let structural_key = crypto::derive_structural_key(passphrase)?;
935 let perm_seed: [u8; 32] = structural_key[..32].try_into().unwrap();
936 let hhat_seed: [u8; 32] = structural_key[32..].try_into().unwrap();
937
938 permute::permute_positions(&mut positions, &perm_seed);
940 let n = positions.len();
941
942 progress::advance();
944
945 let (ciphertext, nonce, salt) = crypto::encrypt(&payload_bytes, passphrase)?;
947
948 let frame_bytes = frame::build_frame(payload_bytes.len(), &salt, &nonce, &ciphertext);
950 let frame_bits = frame::bytes_to_bits(&frame_bytes);
951 let m = frame_bits.len();
952
953 let w = (n / m).clamp(1, 10);
955 let m_max = n / w;
956 let n_used = m_max * w;
957
958 if m > m_max {
959 return Err(StegoError::MessageTooLarge);
960 }
961
962 if n_used > STC_POSITION_LIMIT {
964 return Err(StegoError::ImageTooLarge);
965 }
966
967 positions.truncate(n_used);
968
969 let grid = img.dct_grid(0);
971 let cover_bits: Vec<u8> = positions.iter().map(|p| {
972 let coeff = flat_get(grid, p.flat_idx as usize);
973 (coeff.unsigned_abs() & 1) as u8
974 }).collect();
975 let costs: Vec<f32> = positions.iter().map(|p| p.cost).collect();
976
977 let median_cost = {
979 let mut finite_costs: Vec<f32> = costs.iter().copied().filter(|c| c.is_finite()).collect();
980 if finite_costs.is_empty() {
981 0.0f32
982 } else {
983 let mid = finite_costs.len() / 2;
984 finite_costs.select_nth_unstable_by(mid, f32::total_cmp);
985 finite_costs[mid]
986 }
987 };
988 let is_si = si.is_some();
989
990 let grid_ref = img.dct_grid(0);
992 let total_coefficients = grid_ref.blocks_wide() * grid_ref.blocks_tall() * 64;
993
994 let hhat_matrix = hhat::generate_hhat(STC_H, w, &hhat_seed);
996 let result = embed::stc_embed(&cover_bits, &costs, &frame_bits, &hhat_matrix, STC_H, w);
997 progress::check_cancelled()?;
998 let result = result.ok_or(StegoError::MessageTooLarge)?;
999
1000 let encode_quality = quality::ghost_stealth_score(&GhostMetrics {
1002 num_modifications: result.num_modifications,
1003 n_used,
1004 w,
1005 total_cost: result.total_cost,
1006 median_cost,
1007 is_si,
1008 shadow_modifications: 0,
1009 total_coefficients,
1010 });
1011
1012 let grid_mut = img.dct_grid_mut(0);
1014 for (idx, pos) in positions.iter().enumerate() {
1015 let old_bit = cover_bits[idx];
1016 let new_bit = result.stego_bits[idx];
1017 if old_bit != new_bit {
1018 let fi = pos.flat_idx as usize;
1019 let coeff = flat_get(grid_mut, fi);
1020 let modified = if let Some(ref side_info) = si {
1021 side_info::si_modify_coefficient(coeff, side_info.error_at(fi))
1022 } else {
1023 side_info::nsf5_modify_coefficient(coeff)
1024 };
1025 flat_set(grid_mut, fi, modified);
1026 }
1027 }
1028
1029 drop(positions);
1031 drop(cover_bits);
1032 drop(costs);
1033 drop(result);
1034 drop(si);
1035
1036 let progress_cb = || progress::advance();
1038 let stego_bytes = if let Ok(bytes) = img.to_bytes_with_progress(Some(&progress_cb)) { bytes } else {
1039 img.rebuild_huffman_tables();
1040 img.to_bytes_with_progress(Some(&progress_cb)).map_err(StegoError::InvalidJpeg)?
1041 };
1042
1043 Ok((stego_bytes, encode_quality))
1044}
1045
1046pub fn ghost_decode(
1055 stego_bytes: &[u8],
1056 passphrase: &str,
1057) -> Result<PayloadData, StegoError> {
1058 let img = JpegImage::from_bytes(stego_bytes)?;
1059 progress::advance_by(PARSE_STEPS);
1060
1061 if img.num_components() == 0 {
1062 return Err(StegoError::NoLuminanceChannel);
1063 }
1064
1065 #[cfg(feature = "parallel")]
1068 let key_thread = {
1069 let pass = passphrase.to_string();
1070 std::thread::spawn(move || crypto::derive_structural_key(&pass))
1071 };
1072
1073 let qt_id = img.frame_info().components[0].quant_table_id as usize;
1074 let qt = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
1075 let mut positions = compute_positions_streaming(img.dct_grid(0), qt, None)?;
1076
1077 progress::check_cancelled()?;
1078
1079 #[cfg(feature = "parallel")]
1081 let structural_key = key_thread.join().expect("key derivation thread")?;
1082 #[cfg(not(feature = "parallel"))]
1083 let structural_key = crypto::derive_structural_key(passphrase)?;
1084 let perm_seed: [u8; 32] = structural_key[..32].try_into().unwrap();
1085 let hhat_seed: [u8; 32] = structural_key[32..].try_into().unwrap();
1086
1087 permute::permute_positions(&mut positions, &perm_seed);
1089 let n = positions.len();
1090
1091 progress::advance();
1093
1094 let all_stego_bits: Vec<u8> = {
1096 let grid = img.dct_grid(0);
1097 positions.iter().map(|p| {
1098 let coeff = flat_get(grid, p.flat_idx as usize);
1099 (coeff.unsigned_abs() & 1) as u8
1100 }).collect()
1101 };
1102 drop(positions);
1104 drop(img);
1105
1106 let w_natural = compute_stc_params(n).map(|(w, _, _)| w).unwrap_or(1);
1108 let w_candidates_raw: &[usize] = &[w_natural, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
1109
1110 let mut deduped_w: Vec<usize> = Vec::with_capacity(w_candidates_raw.len());
1112 {
1113 let mut tried_w = 0u16;
1114 for &w in w_candidates_raw {
1115 if w == 0 || n / w == 0 {
1116 continue;
1117 }
1118 if w <= 15 && (tried_w & (1 << w)) != 0 {
1119 continue;
1120 }
1121 if w <= 15 {
1122 tried_w |= 1 << w;
1123 }
1124 let n_used = (n / w) * w;
1125 if n_used > all_stego_bits.len() {
1126 continue;
1127 }
1128 deduped_w.push(w);
1129 }
1130 }
1131
1132 #[cfg(feature = "parallel")]
1133 {
1134 use rayon::prelude::*;
1135 let result = deduped_w.par_iter().find_map_first(|&w| {
1136 let m_max = n / w;
1137 let n_used = m_max * w;
1138
1139 let stego_bits = &all_stego_bits[..n_used];
1140 let hhat_matrix = hhat::generate_hhat(STC_H, w, &hhat_seed);
1141 let extracted_bits = extract::stc_extract(stego_bits, &hhat_matrix, w);
1142
1143 let frame_bytes = frame::bits_to_bytes(&extracted_bits[..m_max]);
1144 match try_parse_and_decrypt(&frame_bytes, passphrase) {
1145 Ok(payload) => Some(Ok(payload)),
1146 Err(StegoError::DecryptionFailed) => Some(Err(StegoError::DecryptionFailed)),
1147 Err(_) => None,
1148 }
1149 });
1150 match result {
1151 Some(Ok(payload)) => {
1152 progress::advance();
1153 return Ok(payload);
1154 }
1155 Some(Err(e)) => {
1156 progress::advance();
1157 return Err(e);
1158 }
1159 None => {
1160 }
1162 }
1163 }
1164 #[cfg(not(feature = "parallel"))]
1165 {
1166 let mut saw_decrypt_fail = false;
1167 for &w in &deduped_w {
1168 let m_max = n / w;
1169 let n_used = m_max * w;
1170
1171 let stego_bits = &all_stego_bits[..n_used];
1172 let hhat_matrix = hhat::generate_hhat(STC_H, w, &hhat_seed);
1173 let extracted_bits = extract::stc_extract(stego_bits, &hhat_matrix, w);
1174
1175 let frame_bytes = frame::bits_to_bytes(&extracted_bits[..m_max]);
1176 match try_parse_and_decrypt(&frame_bytes, passphrase) {
1177 Ok(payload) => {
1178 progress::advance();
1179 return Ok(payload);
1180 }
1181 Err(StegoError::DecryptionFailed) => {
1182 saw_decrypt_fail = true;
1183 }
1184 Err(_) => {}
1185 }
1186 }
1187
1188 progress::advance();
1190
1191 if saw_decrypt_fail {
1192 return Err(StegoError::DecryptionFailed);
1193 }
1194 }
1195
1196 #[cfg(feature = "parallel")]
1198 progress::advance();
1199
1200 Err(StegoError::FrameCorrupted)
1201}
1202
1203fn try_parse_and_decrypt(
1205 frame_bytes: &[u8],
1206 passphrase: &str,
1207) -> Result<PayloadData, StegoError> {
1208 let parsed = frame::parse_frame(frame_bytes)?;
1209 let plaintext = crypto::decrypt(
1210 &parsed.ciphertext,
1211 passphrase,
1212 &parsed.salt,
1213 &parsed.nonce,
1214 )?;
1215 let len = parsed.plaintext_len as usize;
1216 if len > plaintext.len() {
1217 return Err(StegoError::FrameCorrupted);
1218 }
1219 payload::decode_payload(&plaintext[..len])
1220}
1221
1222pub fn ghost_shadow_decode(
1227 stego_bytes: &[u8],
1228 passphrase: &str,
1229) -> Result<PayloadData, StegoError> {
1230 let img = JpegImage::from_bytes(stego_bytes)?;
1231 ghost_shadow_decode_from_image(&img, passphrase)
1232}
1233
1234pub fn ghost_shadow_decode_from_image(
1239 img: &JpegImage,
1240 passphrase: &str,
1241) -> Result<PayloadData, StegoError> {
1242 if img.num_components() == 0 {
1243 return Err(StegoError::NoLuminanceChannel);
1244 }
1245
1246 let qt_id = img.frame_info().components[0].quant_table_id as usize;
1248 let qt = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
1249 let mut positions = compute_positions_streaming(img.dct_grid(0), qt, None)?;
1250
1251 positions.sort_by(|a, b| a.cost.total_cmp(&b.cost));
1253
1254 shadow::shadow_extract(img, &positions, passphrase)
1255}
1256
1257use crate::codec::jpeg::dct::DctGrid;
1260
1261pub(super) fn flat_get(grid: &DctGrid, flat_idx: usize) -> i16 {
1266 let bw = grid.blocks_wide();
1267 let block_idx = flat_idx / 64;
1268 let pos = flat_idx % 64;
1269 let br = block_idx / bw;
1270 let bc = block_idx % bw;
1271 let i = pos / 8;
1272 let j = pos % 8;
1273 grid.get(br, bc, i, j)
1274}
1275
1276pub(super) fn flat_set(grid: &mut DctGrid, flat_idx: usize, val: i16) {
1278 let bw = grid.blocks_wide();
1279 let block_idx = flat_idx / 64;
1280 let pos = flat_idx % 64;
1281 let br = block_idx / bw;
1282 let bc = block_idx % bw;
1283 let i = pos / 8;
1284 let j = pos % 8;
1285 grid.set(br, bc, i, j, val);
1286}