use crate::codec::jpeg::JpegImage;
use crate::stego::cost::uniward::compute_positions_streaming;
use crate::stego::crypto;
use crate::stego::error::StegoError;
use crate::stego::frame::{self, MAX_FRAME_BITS};
use crate::stego::payload::{self, FileEntry, PayloadData};
use crate::stego::permute;
use crate::stego::progress;
use crate::stego::side_info::{self, SideInfo};
use crate::stego::shadow;
use crate::stego::quality::{self, EncodeQuality, GhostMetrics};
use crate::stego::stc::{embed, extract, hhat};
const STC_H: usize = 7;
const STC_POSITION_LIMIT: usize = 500_000_000;
const PARSE_STEPS: u32 = 5;
pub const GHOST_DECODE_STEPS: u32 = PARSE_STEPS + crate::stego::cost::uniward::UNIWARD_PROGRESS_STEPS + 2;
pub const GHOST_ENCODE_STEPS: u32 =
PARSE_STEPS
+ crate::stego::cost::uniward::UNIWARD_PROGRESS_STEPS
+ crate::stego::stc::embed::STC_PROGRESS_STEPS
+ 2
+ crate::codec::jpeg::scan::JPEG_WRITE_STEPS;
fn compute_stc_params(n: usize) -> Result<(usize, usize, usize), StegoError> {
let m_max = MAX_FRAME_BITS.min(n);
if m_max == 0 {
return Err(StegoError::ImageTooSmall);
}
let w = n / m_max; let n_used = m_max * w;
if n_used > STC_POSITION_LIMIT {
return Err(StegoError::ImageTooLarge);
}
Ok((w, n_used, m_max))
}
pub fn ghost_encode(
image_bytes: &[u8],
message: &str,
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
ghost_encode_impl(image_bytes, message, &[], passphrase, None, None)
.map(|(bytes, _)| bytes)
}
pub fn ghost_encode_with_quality(
image_bytes: &[u8],
message: &str,
passphrase: &str,
) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
ghost_encode_impl(image_bytes, message, &[], passphrase, None, None)
}
pub fn ghost_encode_with_files(
image_bytes: &[u8],
message: &str,
files: &[FileEntry],
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
ghost_encode_impl(image_bytes, message, files, passphrase, None, None)
.map(|(bytes, _)| bytes)
}
pub fn ghost_encode_with_files_quality(
image_bytes: &[u8],
message: &str,
files: &[FileEntry],
passphrase: &str,
) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
ghost_encode_impl(image_bytes, message, files, passphrase, None, None)
}
pub fn ghost_encode_si(
image_bytes: &[u8],
raw_pixels_rgb: &[u8],
pixel_width: u32,
pixel_height: u32,
message: &str,
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
ghost_encode_si_with_files(
image_bytes, raw_pixels_rgb, pixel_width, pixel_height,
message, &[], passphrase,
)
}
pub fn ghost_encode_si_with_quality(
image_bytes: &[u8],
raw_pixels_rgb: &[u8],
pixel_width: u32,
pixel_height: u32,
message: &str,
passphrase: &str,
) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
ghost_encode_si_with_files_quality(
image_bytes, raw_pixels_rgb, pixel_width, pixel_height,
message, &[], passphrase,
)
}
pub fn ghost_encode_si_with_files(
image_bytes: &[u8],
raw_pixels_rgb: &[u8],
pixel_width: u32,
pixel_height: u32,
message: &str,
files: &[FileEntry],
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
ghost_encode_si_with_files_quality(
image_bytes, raw_pixels_rgb, pixel_width, pixel_height,
message, files, passphrase,
).map(|(bytes, _)| bytes)
}
pub fn ghost_encode_si_with_files_quality(
image_bytes: &[u8],
raw_pixels_rgb: &[u8],
pixel_width: u32,
pixel_height: u32,
message: &str,
files: &[FileEntry],
passphrase: &str,
) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
let img = JpegImage::from_bytes(image_bytes)?;
let fi = img.frame_info();
crate::stego::validate_encode_dimensions(fi.width as u32, fi.height as u32)?;
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
let qt_id = fi.components[0].quant_table_id as usize;
let qt = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
let si = SideInfo::compute(
raw_pixels_rgb,
pixel_width,
pixel_height,
img.dct_grid(0),
&qt.values,
);
ghost_encode_impl(image_bytes, message, files, passphrase, Some(si), Some(img))
}
pub struct ShadowLayer {
pub message: String,
pub passphrase: String,
pub files: Vec<FileEntry>,
}
pub const GHOST_ENCODE_WITH_SHADOWS_STEPS: u32 =
PARSE_STEPS
+ crate::stego::cost::uniward::UNIWARD_PROGRESS_STEPS
+ 2 + crate::stego::stc::embed::STC_PROGRESS_STEPS
+ crate::stego::cost::uniward::UNIWARD_PROGRESS_STEPS + crate::codec::jpeg::scan::JPEG_WRITE_STEPS;
const CASCADE: &[(usize, usize)] = &[
(1, 4),
(1, 8),
(1, 16),
(1, 32),
(1, 64),
(1, 128),
(2, 16), (5, 16), (10, 16),
(2, 32), (5, 32),
(2, 64),
];
pub fn ghost_encode_with_shadows(
image_bytes: &[u8],
message: &str,
files: &[FileEntry],
passphrase: &str,
shadows: &[ShadowLayer],
si: Option<SideInfo>,
) -> Result<Vec<u8>, StegoError> {
ghost_encode_with_shadows_impl(image_bytes, message, files, passphrase, shadows, si, None)
.map(|(bytes, _)| bytes)
}
pub fn ghost_encode_with_shadows_quality(
image_bytes: &[u8],
message: &str,
files: &[FileEntry],
passphrase: &str,
shadows: &[ShadowLayer],
si: Option<SideInfo>,
) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
ghost_encode_with_shadows_impl(image_bytes, message, files, passphrase, shadows, si, None)
}
pub fn ghost_encode_si_with_shadows(
image_bytes: &[u8],
raw_pixels_rgb: &[u8],
pixel_width: u32,
pixel_height: u32,
message: &str,
files: &[FileEntry],
passphrase: &str,
shadows: &[ShadowLayer],
) -> Result<Vec<u8>, StegoError> {
ghost_encode_si_with_shadows_quality(
image_bytes, raw_pixels_rgb, pixel_width, pixel_height,
message, files, passphrase, shadows,
).map(|(bytes, _)| bytes)
}
pub fn ghost_encode_si_with_shadows_quality(
image_bytes: &[u8],
raw_pixels_rgb: &[u8],
pixel_width: u32,
pixel_height: u32,
message: &str,
files: &[FileEntry],
passphrase: &str,
shadows: &[ShadowLayer],
) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
let img = JpegImage::from_bytes(image_bytes)?;
let fi = img.frame_info();
crate::stego::validate_encode_dimensions(fi.width as u32, fi.height as u32)?;
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
let qt_id = fi.components[0].quant_table_id as usize;
let qt = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
let si = SideInfo::compute(
raw_pixels_rgb,
pixel_width,
pixel_height,
img.dct_grid(0),
&qt.values,
);
ghost_encode_with_shadows_impl(image_bytes, message, files, passphrase, shadows, Some(si), Some(img))
}
fn ghost_encode_with_shadows_impl(
image_bytes: &[u8],
message: &str,
files: &[FileEntry],
passphrase: &str,
shadows: &[ShadowLayer],
si: Option<SideInfo>,
pre_parsed: Option<JpegImage>,
) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
let cascade_budget = CASCADE.len() as u32 * (
crate::stego::stc::embed::STC_PROGRESS_STEPS
+ crate::stego::cost::uniward::UNIWARD_PROGRESS_STEPS
);
progress::init(GHOST_ENCODE_WITH_SHADOWS_STEPS + cascade_budget);
{
let mut all_passes: Vec<&str> = vec![passphrase];
for s in shadows {
all_passes.push(&s.passphrase);
}
for i in 0..all_passes.len() {
for j in (i + 1)..all_passes.len() {
if all_passes[i] == all_passes[j] {
return Err(StegoError::DuplicatePassphrase);
}
}
}
}
let primary_payload_size = payload::compressed_payload_size(message, files);
let mut swap_idx: Option<usize> = None;
for (i, s) in shadows.iter().enumerate() {
let shadow_size = payload::compressed_payload_size(&s.message, &s.files);
if shadow_size > primary_payload_size {
if let Some(prev) = swap_idx {
let prev_size = payload::compressed_payload_size(&shadows[prev].message, &shadows[prev].files);
if shadow_size > prev_size {
swap_idx = Some(i);
}
} else {
swap_idx = Some(i);
}
}
}
let primary_as_shadow;
let (eff_message, eff_files, eff_passphrase, eff_shadows);
if let Some(idx) = swap_idx {
eff_message = shadows[idx].message.as_str();
eff_files = &shadows[idx].files[..];
eff_passphrase = shadows[idx].passphrase.as_str();
primary_as_shadow = ShadowLayer {
message: message.to_string(),
passphrase: passphrase.to_string(),
files: files.to_vec(),
};
let mut new_shadows: Vec<&ShadowLayer> = Vec::with_capacity(shadows.len());
new_shadows.push(&primary_as_shadow);
for (i, s) in shadows.iter().enumerate() {
if i != idx {
new_shadows.push(s);
}
}
eff_shadows = new_shadows;
} else {
eff_message = message;
eff_files = files;
eff_passphrase = passphrase;
eff_shadows = shadows.iter().collect();
};
let payload_bytes = payload::encode_payload(eff_message, eff_files)?;
let mut img = match pre_parsed {
Some(img) => img,
None => JpegImage::from_bytes(image_bytes)?,
};
progress::advance_by(PARSE_STEPS);
let fi = img.frame_info();
crate::stego::validate_encode_dimensions(fi.width as u32, fi.height as u32)?;
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
#[cfg(feature = "parallel")]
let key_thread = {
let pass = eff_passphrase.to_string();
std::thread::spawn(move || crypto::derive_structural_key(&pass))
};
let qt_id = img.frame_info().components[0].quant_table_id as usize;
let qt = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
let si_ref = si.as_ref().map(|s| (s, img.dct_grid(0)));
let mut positions = compute_positions_streaming(img.dct_grid(0), qt, si_ref)?;
positions.sort_by(|a, b| a.cost.total_cmp(&b.cost));
let mut shadow_states: Vec<shadow::ShadowState> = Vec::new();
if !eff_shadows.is_empty() {
let initial_parity = 4;
for s in eff_shadows.iter() {
let state = shadow::prepare_shadow(
&positions,
&s.passphrase,
&s.message,
&s.files,
initial_parity,
)?;
shadow_states.push(state);
}
}
progress::advance();
positions.sort_by_key(|p| p.flat_idx);
let original_y = img.dct_grid(0).clone();
#[cfg(feature = "parallel")]
let structural_key = key_thread.join().expect("key derivation thread")?;
#[cfg(not(feature = "parallel"))]
let structural_key = crypto::derive_structural_key(eff_passphrase)?;
let perm_seed: [u8; 32] = structural_key[..32].try_into().unwrap();
let hhat_seed: [u8; 32] = structural_key[32..].try_into().unwrap();
permute::permute_positions(&mut positions, &perm_seed);
let n = positions.len();
progress::advance();
let (ciphertext, nonce, salt) = crypto::encrypt(&payload_bytes, eff_passphrase)?;
let frame_bytes = frame::build_frame(payload_bytes.len(), &salt, &nonce, &ciphertext);
let frame_bits = frame::bytes_to_bits(&frame_bytes);
let m = frame_bits.len();
let w = (n / m).clamp(1, 10);
let m_max = n / w;
let n_used = m_max * w;
if m > m_max {
return Err(StegoError::MessageTooLarge);
}
if w < 2 && !shadow_states.is_empty() {
return Err(StegoError::MessageTooLarge);
}
if n_used > STC_POSITION_LIMIT {
return Err(StegoError::ImageTooLarge);
}
positions.truncate(n_used);
let hhat_matrix = hhat::generate_hhat(STC_H, w, &hhat_seed);
let shadow_inf_costs = build_inf_cost_set(w, &shadow_states);
let median_cost = {
let mut finite_costs: Vec<f32> = positions.iter()
.map(|p| p.cost)
.filter(|c| c.is_finite())
.collect();
if finite_costs.is_empty() {
0.0f32
} else {
let mid = finite_costs.len() / 2;
finite_costs.select_nth_unstable_by(mid, f32::total_cmp);
finite_costs[mid]
}
};
let is_si = si.is_some();
let grid_ref = img.dct_grid(0);
let total_coefficients = grid_ref.blocks_wide() * grid_ref.blocks_tall() * 64;
let shadow_modifications: usize = shadow_states.iter()
.map(|s| s.n_total)
.sum();
let mut stc_total_cost: f64 = 0.0;
let mut stc_num_modifications: usize = 0;
if shadow_states.is_empty() {
let (tc, nm) = run_stc_pass(&mut img, &original_y, &positions, &[],
&frame_bits, &hhat_matrix, w, &si, &None)?;
stc_total_cost = tc;
stc_num_modifications = nm;
} else {
let (tc, nm) = run_stc_pass(&mut img, &original_y, &positions, &shadow_states,
&frame_bits, &hhat_matrix, w, &si, &shadow_inf_costs)?;
stc_total_cost = tc;
stc_num_modifications = nm;
let all_fraction_1 = shadow_states.iter().all(|s| s.cost_fraction == 1);
if !all_fraction_1 {
let qt_verify = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
let mut stego_y_positions = compute_positions_streaming(img.dct_grid(0), qt_verify, None)?;
stego_y_positions.sort_by(|a, b| a.cost.total_cmp(&b.cost));
if !verify_all_shadows_decoder_side(&img, &shadow_states, &eff_shadows, &stego_y_positions) {
let mut cascade_positions = positions.clone();
cascade_positions.sort_by(|a, b| a.cost.total_cmp(&b.cost));
let mut verified = false;
#[cfg(feature = "parallel")]
{
use std::sync::atomic::{AtomicUsize, Ordering};
use rayon::prelude::*;
let best_fraction = AtomicUsize::new(0);
for &parity in &[4, 8, 16, 32, 64, 128] {
let fractions_for_parity: Vec<usize> = CASCADE.iter()
.filter(|&&(_, p)| p == parity)
.map(|&(f, _)| f)
.collect();
if fractions_for_parity.is_empty() { continue; }
let has_fraction_1 = fractions_for_parity.contains(&1);
let successes: Vec<(usize, crate::codec::jpeg::JpegImage, Vec<shadow::ShadowState>, f64, usize)> =
fractions_for_parity.par_iter().filter_map(|&fraction| {
if best_fraction.load(Ordering::Relaxed) > fraction { return None; }
let mut local_states = shadow_states.clone();
let local_cascade_positions = cascade_positions.clone();
for state in local_states.iter_mut() {
if shadow::rebuild_shadow(state, &local_cascade_positions, parity, fraction).is_err() {
return None;
}
}
if best_fraction.load(Ordering::Relaxed) > fraction { return None; }
let mut local_img = img.clone();
let new_inf_costs = build_inf_cost_set(w, &local_states);
let (local_tc, local_nm) = match run_stc_pass(&mut local_img, &original_y, &positions, &local_states,
&frame_bits, &hhat_matrix, w, &si, &new_inf_costs) {
Ok(v) => v,
Err(_) => return None,
};
if best_fraction.load(Ordering::Relaxed) > fraction { return None; }
let qt_re = match local_img.quant_table(qt_id) {
Some(qt) => qt,
None => return None,
};
let mut local_stego_positions = match compute_positions_streaming(local_img.dct_grid(0), qt_re, None) {
Ok(p) => p,
Err(_) => return None,
};
local_stego_positions.sort_by(|a, b| a.cost.total_cmp(&b.cost));
if verify_all_shadows_decoder_side(&local_img, &local_states, &eff_shadows, &local_stego_positions) {
best_fraction.fetch_max(fraction, Ordering::Relaxed);
Some((fraction, local_img, local_states, local_tc, local_nm))
} else {
None
}
}).collect();
if !successes.is_empty() {
let best = successes.into_iter().max_by_key(|(f, _, _, _, _)| *f).unwrap();
img = best.1;
shadow_states = best.2;
stc_total_cost = best.3;
stc_num_modifications = best.4;
verified = true;
break;
}
if has_fraction_1 && parity == 128 {
break;
}
}
}
#[cfg(not(feature = "parallel"))]
{
let mut last_fraction_1_failed_parity: Option<usize> = None;
for &(frac, par) in CASCADE {
if frac > 1 {
if let Some(failed_par) = last_fraction_1_failed_parity {
if par <= failed_par {
continue;
}
}
}
for state in shadow_states.iter_mut() {
shadow::rebuild_shadow(state, &cascade_positions, par, frac)?;
}
let new_inf_costs = build_inf_cost_set(w, &shadow_states);
let (tc, nm) = run_stc_pass(&mut img, &original_y, &positions, &shadow_states,
&frame_bits, &hhat_matrix, w, &si, &new_inf_costs)?;
stc_total_cost = tc;
stc_num_modifications = nm;
let qt_re = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
stego_y_positions = compute_positions_streaming(img.dct_grid(0), qt_re, None)?;
stego_y_positions.sort_by(|a, b| a.cost.total_cmp(&b.cost));
if verify_all_shadows_decoder_side(&img, &shadow_states, &eff_shadows, &stego_y_positions) {
verified = true;
break;
}
if frac == 1 {
last_fraction_1_failed_parity = Some(par);
}
if frac == 1 && par == 128 {
break;
}
}
}
if !verified {
return Err(StegoError::MessageTooLarge);
}
}
} }
let encode_quality = quality::ghost_stealth_score(&GhostMetrics {
num_modifications: stc_num_modifications,
n_used,
w,
total_cost: stc_total_cost,
median_cost,
is_si,
shadow_modifications,
total_coefficients,
});
drop(positions);
drop(original_y);
drop(shadow_states);
drop(shadow_inf_costs);
drop(si);
let progress_cb = || progress::advance();
let stego_bytes = if let Ok(bytes) = img.to_bytes_with_progress(Some(&progress_cb)) { bytes } else {
img.rebuild_huffman_tables();
img.to_bytes_with_progress(Some(&progress_cb)).map_err(StegoError::InvalidJpeg)?
};
Ok((stego_bytes, encode_quality))
}
fn run_stc_pass(
img: &mut JpegImage,
original_y: &crate::codec::jpeg::dct::DctGrid,
positions: &[permute::CoeffPos],
shadow_states: &[shadow::ShadowState],
message_bits: &[u8],
hhat_matrix: &[Vec<u32>],
w: usize,
si: &Option<SideInfo>,
shadow_inf_costs: &Option<std::collections::HashSet<u32>>,
) -> Result<(f64, usize), StegoError> {
*img.dct_grid_mut(0) = original_y.clone();
for state in shadow_states {
shadow::embed_shadow_lsb(img, state);
}
let grid = img.dct_grid(0);
let cover_bits: Vec<u8> = positions.iter().map(|p| {
let coeff = flat_get(grid, p.flat_idx as usize);
(coeff.unsigned_abs() & 1) as u8
}).collect();
let costs: Vec<f32> = if let Some(inf_set) = shadow_inf_costs {
positions.iter().map(|p| {
if inf_set.contains(&p.flat_idx) {
f32::INFINITY
} else {
p.cost
}
}).collect()
} else {
positions.iter().map(|p| p.cost).collect()
};
let result = embed::stc_embed(&cover_bits, &costs, message_bits, hhat_matrix, STC_H, w);
progress::check_cancelled()?;
let result = result.ok_or(StegoError::MessageTooLarge)?;
let total_cost = result.total_cost;
let num_modifications = result.num_modifications;
apply_stc_changes(img, positions, &cover_bits, &result.stego_bits, si);
Ok((total_cost, num_modifications))
}
fn apply_stc_changes(
img: &mut JpegImage,
positions: &[permute::CoeffPos],
cover_bits: &[u8],
stego_bits: &[u8],
si: &Option<SideInfo>,
) {
let grid_mut = img.dct_grid_mut(0);
for (idx, pos) in positions.iter().enumerate() {
if cover_bits[idx] != stego_bits[idx] {
let fi = pos.flat_idx as usize;
let coeff = flat_get(grid_mut, fi);
let modified = if let Some(side_info) = si {
side_info::si_modify_coefficient(coeff, side_info.error_at(fi))
} else {
side_info::nsf5_modify_coefficient(coeff)
};
flat_set(grid_mut, fi, modified);
}
}
}
fn verify_all_shadows_decoder_side(
img: &JpegImage,
shadow_states: &[shadow::ShadowState],
shadows: &[&ShadowLayer],
stego_y_positions_sorted: &[permute::CoeffPos],
) -> bool {
for (i, state) in shadow_states.iter().enumerate() {
if shadow::verify_shadow_decoder_side(
img, state, &shadows[i].passphrase, stego_y_positions_sorted,
).is_err() {
return false;
}
}
true
}
fn build_inf_cost_set(w: usize, shadow_states: &[shadow::ShadowState]) -> Option<std::collections::HashSet<u32>> {
if w >= 2 && !shadow_states.is_empty() {
let mut set = std::collections::HashSet::new();
for state in shadow_states {
for pos in &state.positions {
set.insert(pos.flat_idx);
}
}
Some(set)
} else {
None
}
}
fn ghost_encode_impl(
image_bytes: &[u8],
message: &str,
files: &[FileEntry],
passphrase: &str,
si: Option<SideInfo>,
pre_parsed: Option<JpegImage>,
) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
progress::init(GHOST_ENCODE_STEPS);
let payload_bytes = payload::encode_payload(message, files)?;
let mut img = match pre_parsed {
Some(img) => img,
None => JpegImage::from_bytes(image_bytes)?,
};
progress::advance_by(PARSE_STEPS);
let fi = img.frame_info();
crate::stego::validate_encode_dimensions(fi.width as u32, fi.height as u32)?;
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
#[cfg(feature = "parallel")]
let key_thread = {
let pass = passphrase.to_string();
std::thread::spawn(move || crypto::derive_structural_key(&pass))
};
let qt_id = img.frame_info().components[0].quant_table_id as usize;
let qt = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
let si_ref = si.as_ref().map(|s| (s, img.dct_grid(0)));
let mut positions = compute_positions_streaming(img.dct_grid(0), qt, si_ref)?;
#[cfg(feature = "parallel")]
let structural_key = key_thread.join().expect("key derivation thread")?;
#[cfg(not(feature = "parallel"))]
let structural_key = crypto::derive_structural_key(passphrase)?;
let perm_seed: [u8; 32] = structural_key[..32].try_into().unwrap();
let hhat_seed: [u8; 32] = structural_key[32..].try_into().unwrap();
permute::permute_positions(&mut positions, &perm_seed);
let n = positions.len();
progress::advance();
let (ciphertext, nonce, salt) = crypto::encrypt(&payload_bytes, passphrase)?;
let frame_bytes = frame::build_frame(payload_bytes.len(), &salt, &nonce, &ciphertext);
let frame_bits = frame::bytes_to_bits(&frame_bytes);
let m = frame_bits.len();
let w = (n / m).clamp(1, 10);
let m_max = n / w;
let n_used = m_max * w;
if m > m_max {
return Err(StegoError::MessageTooLarge);
}
if n_used > STC_POSITION_LIMIT {
return Err(StegoError::ImageTooLarge);
}
positions.truncate(n_used);
let grid = img.dct_grid(0);
let cover_bits: Vec<u8> = positions.iter().map(|p| {
let coeff = flat_get(grid, p.flat_idx as usize);
(coeff.unsigned_abs() & 1) as u8
}).collect();
let costs: Vec<f32> = positions.iter().map(|p| p.cost).collect();
let median_cost = {
let mut finite_costs: Vec<f32> = costs.iter().copied().filter(|c| c.is_finite()).collect();
if finite_costs.is_empty() {
0.0f32
} else {
let mid = finite_costs.len() / 2;
finite_costs.select_nth_unstable_by(mid, f32::total_cmp);
finite_costs[mid]
}
};
let is_si = si.is_some();
let grid_ref = img.dct_grid(0);
let total_coefficients = grid_ref.blocks_wide() * grid_ref.blocks_tall() * 64;
let hhat_matrix = hhat::generate_hhat(STC_H, w, &hhat_seed);
let result = embed::stc_embed(&cover_bits, &costs, &frame_bits, &hhat_matrix, STC_H, w);
progress::check_cancelled()?;
let result = result.ok_or(StegoError::MessageTooLarge)?;
let encode_quality = quality::ghost_stealth_score(&GhostMetrics {
num_modifications: result.num_modifications,
n_used,
w,
total_cost: result.total_cost,
median_cost,
is_si,
shadow_modifications: 0,
total_coefficients,
});
let grid_mut = img.dct_grid_mut(0);
for (idx, pos) in positions.iter().enumerate() {
let old_bit = cover_bits[idx];
let new_bit = result.stego_bits[idx];
if old_bit != new_bit {
let fi = pos.flat_idx as usize;
let coeff = flat_get(grid_mut, fi);
let modified = if let Some(ref side_info) = si {
side_info::si_modify_coefficient(coeff, side_info.error_at(fi))
} else {
side_info::nsf5_modify_coefficient(coeff)
};
flat_set(grid_mut, fi, modified);
}
}
drop(positions);
drop(cover_bits);
drop(costs);
drop(result);
drop(si);
let progress_cb = || progress::advance();
let stego_bytes = if let Ok(bytes) = img.to_bytes_with_progress(Some(&progress_cb)) { bytes } else {
img.rebuild_huffman_tables();
img.to_bytes_with_progress(Some(&progress_cb)).map_err(StegoError::InvalidJpeg)?
};
Ok((stego_bytes, encode_quality))
}
pub fn ghost_decode(
stego_bytes: &[u8],
passphrase: &str,
) -> Result<PayloadData, StegoError> {
let img = JpegImage::from_bytes(stego_bytes)?;
progress::advance_by(PARSE_STEPS);
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
#[cfg(feature = "parallel")]
let key_thread = {
let pass = passphrase.to_string();
std::thread::spawn(move || crypto::derive_structural_key(&pass))
};
let qt_id = img.frame_info().components[0].quant_table_id as usize;
let qt = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
let mut positions = compute_positions_streaming(img.dct_grid(0), qt, None)?;
progress::check_cancelled()?;
#[cfg(feature = "parallel")]
let structural_key = key_thread.join().expect("key derivation thread")?;
#[cfg(not(feature = "parallel"))]
let structural_key = crypto::derive_structural_key(passphrase)?;
let perm_seed: [u8; 32] = structural_key[..32].try_into().unwrap();
let hhat_seed: [u8; 32] = structural_key[32..].try_into().unwrap();
permute::permute_positions(&mut positions, &perm_seed);
let n = positions.len();
progress::advance();
let all_stego_bits: Vec<u8> = {
let grid = img.dct_grid(0);
positions.iter().map(|p| {
let coeff = flat_get(grid, p.flat_idx as usize);
(coeff.unsigned_abs() & 1) as u8
}).collect()
};
drop(positions);
drop(img);
let w_natural = compute_stc_params(n).map(|(w, _, _)| w).unwrap_or(1);
let w_candidates_raw: &[usize] = &[w_natural, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut deduped_w: Vec<usize> = Vec::with_capacity(w_candidates_raw.len());
{
let mut tried_w = 0u16;
for &w in w_candidates_raw {
if w == 0 || n / w == 0 {
continue;
}
if w <= 15 && (tried_w & (1 << w)) != 0 {
continue;
}
if w <= 15 {
tried_w |= 1 << w;
}
let n_used = (n / w) * w;
if n_used > all_stego_bits.len() {
continue;
}
deduped_w.push(w);
}
}
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
let result = deduped_w.par_iter().find_map_first(|&w| {
let m_max = n / w;
let n_used = m_max * w;
let stego_bits = &all_stego_bits[..n_used];
let hhat_matrix = hhat::generate_hhat(STC_H, w, &hhat_seed);
let extracted_bits = extract::stc_extract(stego_bits, &hhat_matrix, w);
let frame_bytes = frame::bits_to_bytes(&extracted_bits[..m_max]);
match try_parse_and_decrypt(&frame_bytes, passphrase) {
Ok(payload) => Some(Ok(payload)),
Err(StegoError::DecryptionFailed) => Some(Err(StegoError::DecryptionFailed)),
Err(_) => None,
}
});
match result {
Some(Ok(payload)) => {
progress::advance();
return Ok(payload);
}
Some(Err(e)) => {
progress::advance();
return Err(e);
}
None => {
}
}
}
#[cfg(not(feature = "parallel"))]
{
let mut saw_decrypt_fail = false;
for &w in &deduped_w {
let m_max = n / w;
let n_used = m_max * w;
let stego_bits = &all_stego_bits[..n_used];
let hhat_matrix = hhat::generate_hhat(STC_H, w, &hhat_seed);
let extracted_bits = extract::stc_extract(stego_bits, &hhat_matrix, w);
let frame_bytes = frame::bits_to_bytes(&extracted_bits[..m_max]);
match try_parse_and_decrypt(&frame_bytes, passphrase) {
Ok(payload) => {
progress::advance();
return Ok(payload);
}
Err(StegoError::DecryptionFailed) => {
saw_decrypt_fail = true;
}
Err(_) => {}
}
}
progress::advance();
if saw_decrypt_fail {
return Err(StegoError::DecryptionFailed);
}
}
#[cfg(feature = "parallel")]
progress::advance();
Err(StegoError::FrameCorrupted)
}
fn try_parse_and_decrypt(
frame_bytes: &[u8],
passphrase: &str,
) -> Result<PayloadData, StegoError> {
let parsed = frame::parse_frame(frame_bytes)?;
let plaintext = crypto::decrypt(
&parsed.ciphertext,
passphrase,
&parsed.salt,
&parsed.nonce,
)?;
let len = parsed.plaintext_len as usize;
if len > plaintext.len() {
return Err(StegoError::FrameCorrupted);
}
payload::decode_payload(&plaintext[..len])
}
pub fn ghost_shadow_decode(
stego_bytes: &[u8],
passphrase: &str,
) -> Result<PayloadData, StegoError> {
let img = JpegImage::from_bytes(stego_bytes)?;
ghost_shadow_decode_from_image(&img, passphrase)
}
pub fn ghost_shadow_decode_from_image(
img: &JpegImage,
passphrase: &str,
) -> Result<PayloadData, StegoError> {
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
let qt_id = img.frame_info().components[0].quant_table_id as usize;
let qt = img.quant_table(qt_id).ok_or(StegoError::NoLuminanceChannel)?;
let mut positions = compute_positions_streaming(img.dct_grid(0), qt, None)?;
positions.sort_by(|a, b| a.cost.total_cmp(&b.cost));
shadow::shadow_extract(img, &positions, passphrase)
}
use crate::codec::jpeg::dct::DctGrid;
pub(super) fn flat_get(grid: &DctGrid, flat_idx: usize) -> i16 {
let bw = grid.blocks_wide();
let block_idx = flat_idx / 64;
let pos = flat_idx % 64;
let br = block_idx / bw;
let bc = block_idx % bw;
let i = pos / 8;
let j = pos % 8;
grid.get(br, bc, i, j)
}
pub(super) fn flat_set(grid: &mut DctGrid, flat_idx: usize, val: i16) {
let bw = grid.blocks_wide();
let block_idx = flat_idx / 64;
let pos = flat_idx % 64;
let br = block_idx / bw;
let bc = block_idx % bw;
let i = pos / 8;
let j = pos % 8;
grid.set(br, bc, i, j, val);
}