use super::{DomainCover, EmbedDomain, PositionKey};
use crate::stego::armor::ecc;
use crate::stego::crypto::{self, NONCE_LEN, SALT_LEN};
use crate::stego::error::StegoError;
use crate::stego::frame;
use crate::stego::payload::{self, FileEntry, PayloadData};
use crate::stego::shadow_layer::{
build_shadow_frame_wide as build_shadow_frame,
compute_max_shadow_fdl,
parse_shadow_frame_wide as parse_shadow_frame,
MAX_SHADOW_FRAME_BYTES_WIDE as MAX_SHADOW_FRAME_BYTES,
SHADOW_FRAME_OVERHEAD_WIDE as SHADOW_FRAME_OVERHEAD,
SHADOW_PARITY_TIERS,
};
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha20Rng;
#[derive(Debug, Clone, Copy)]
pub struct ShadowSlot {
pub domain: EmbedDomain,
pub intra_index: usize,
pub priority: u32,
}
#[derive(Debug)]
pub struct ShadowState {
pub positions: Vec<ShadowSlot>,
pub bits: Vec<u8>,
pub n_total: usize,
pub parity_len: usize,
pub frame_data_len: usize,
}
fn priority_slots(cover: &DomainCover, perm_seed: &[u8; 32]) -> Vec<ShadowSlot> {
let mut rng = ChaCha20Rng::from_seed(*perm_seed);
let mut slots = Vec::with_capacity(cover.coeff_sign_bypass.len());
for (intra_index, key) in cover.coeff_sign_bypass.positions.iter().enumerate() {
rng.set_word_pos((key.raw() as u128).wrapping_mul(2));
let priority = rng.next_u32();
slots.push(ShadowSlot {
domain: EmbedDomain::CoeffSignBypass,
intra_index,
priority,
});
}
slots.sort_by_key(|s| s.priority);
slots
}
pub fn prepare_shadow(
cover: &DomainCover,
shadow_pass: &str,
message: &str,
files: &[FileEntry],
parity_len: usize,
) -> Result<ShadowState, StegoError> {
let payload_bytes = payload::encode_payload(message, files)?;
let (ciphertext, nonce, salt) = crypto::encrypt(&payload_bytes, shadow_pass)?;
let frame_bytes = build_shadow_frame(payload_bytes.len(), &salt, &nonce, &ciphertext);
let frame_data_len = frame_bytes.len();
let rs_bytes = ecc::rs_encode_blocks_with_parity(&frame_bytes, parity_len);
let rs_bits = frame::bytes_to_bits(&rs_bytes);
let n_total = rs_bits.len();
let perm_seed = crypto::derive_shadow_structural_key(shadow_pass)?;
let slots = priority_slots(cover, &perm_seed);
if slots.len() < n_total {
return Err(StegoError::MessageTooLarge);
}
let positions = slots.into_iter().take(n_total).collect();
Ok(ShadowState {
positions,
bits: rs_bits,
n_total,
parity_len,
frame_data_len,
})
}
pub fn embed_shadow_lsb_residual(
coeff_sign_bypass_bits: &mut [u8],
coeff_suffix_lsb_bits: &mut [u8],
state: &ShadowState,
) {
for (i, slot) in state.positions.iter().enumerate().take(state.n_total) {
let bit = state.bits[i];
match slot.domain {
EmbedDomain::CoeffSignBypass => {
coeff_sign_bypass_bits[slot.intra_index] = bit;
}
EmbedDomain::CoeffSuffixLsb => {
coeff_suffix_lsb_bits[slot.intra_index] = bit;
}
EmbedDomain::MvdSignBypass | EmbedDomain::MvdSuffixLsb => {
unreachable!("priority_slots restricted to residual domains")
}
}
}
}
pub fn overlay_infinity_costs_residual(
coeff_sign_bypass_cost: &mut [f32],
coeff_suffix_lsb_cost: &mut [f32],
state: &ShadowState,
) {
for slot in state.positions.iter().take(state.n_total) {
match slot.domain {
EmbedDomain::CoeffSignBypass => {
coeff_sign_bypass_cost[slot.intra_index] = f32::INFINITY;
}
EmbedDomain::CoeffSuffixLsb => {
coeff_suffix_lsb_cost[slot.intra_index] = f32::INFINITY;
}
EmbedDomain::MvdSignBypass | EmbedDomain::MvdSuffixLsb => {
}
}
}
}
pub fn apply_shadow_to_plan_residual(
coeff_sign_bypass: &mut [u8],
coeff_suffix_lsb: &mut [u8],
state: &ShadowState,
) {
for (i, slot) in state.positions.iter().enumerate().take(state.n_total) {
let bit = state.bits[i];
match slot.domain {
EmbedDomain::CoeffSignBypass => coeff_sign_bypass[slot.intra_index] = bit,
EmbedDomain::CoeffSuffixLsb => coeff_suffix_lsb[slot.intra_index] = bit,
EmbedDomain::MvdSignBypass | EmbedDomain::MvdSuffixLsb => {
unreachable!("priority_slots restricted to residual domains")
}
}
}
}
fn try_single_fdl(
lsbs: &[u8],
fdl: usize,
parity_len: usize,
passphrase: &str,
) -> Option<Result<PayloadData, StegoError>> {
let rs_encoded_len = ecc::rs_encoded_len_with_parity(fdl, parity_len);
let rs_bits_needed = rs_encoded_len * 8;
if rs_bits_needed > lsbs.len() {
return None;
}
let rs_bytes = frame::bits_to_bytes(&lsbs[..rs_bits_needed]);
let decoded = match ecc::rs_decode_blocks_with_parity(&rs_bytes, fdl, parity_len) {
Ok((data, _)) => data,
Err(_) => return None,
};
let fr = parse_shadow_frame(&decoded).ok()?;
match crypto::decrypt(&fr.ciphertext, passphrase, &fr.salt, &fr.nonce) {
Ok(plaintext) => {
let len = fr.plaintext_len as usize;
if len > plaintext.len() {
return None;
}
Some(payload::decode_payload(&plaintext[..len]))
}
Err(_) => None,
}
}
fn peek_fdl_from_first_block(
lsbs: &[u8],
parity_len: usize,
max_fdl: usize,
) -> Option<usize> {
let k = 255usize.saturating_sub(parity_len);
if k < 2 || lsbs.len() < 255 * 8 {
return None;
}
let first_block_bytes = frame::bits_to_bytes(&lsbs[..255 * 8]);
let (data, _) =
ecc::rs_decode_blocks_with_parity(&first_block_bytes, k, parity_len).ok()?;
if data.len() < 2 {
return None;
}
let plaintext_len = u16::from_be_bytes([data[0], data[1]]) as usize;
let fdl = SHADOW_FRAME_OVERHEAD + plaintext_len;
if fdl >= k && fdl <= max_fdl {
Some(fdl)
} else {
None
}
}
pub(super) fn priority_slots_all4_safe(
cover: &DomainCover,
perm_seed: &[u8; 32],
safe_csl: Option<&[bool]>,
safe_msl: Option<&[bool]>,
) -> Vec<ShadowSlot> {
let mut rng = ChaCha20Rng::from_seed(*perm_seed);
let msl_count = safe_msl
.map(|m| m.iter().filter(|&&b| b).count())
.unwrap_or(0);
let csl_count = match safe_csl {
Some(m) => m.iter().filter(|&&b| b).count(),
None => cover.coeff_suffix_lsb.len(),
};
let mut slots = Vec::with_capacity(
cover.coeff_sign_bypass.len() + csl_count + cover.mvd_sign_bypass.len() + msl_count,
);
for (intra_index, key) in cover.coeff_sign_bypass.positions.iter().enumerate() {
rng.set_word_pos((key.raw() as u128).wrapping_mul(2));
slots.push(ShadowSlot {
domain: EmbedDomain::CoeffSignBypass,
intra_index,
priority: rng.next_u32(),
});
}
for (intra_index, key) in cover.coeff_suffix_lsb.positions.iter().enumerate() {
if let Some(mask) = safe_csl
&& (intra_index >= mask.len() || !mask[intra_index])
{
continue;
}
rng.set_word_pos((key.raw() as u128).wrapping_mul(2));
slots.push(ShadowSlot {
domain: EmbedDomain::CoeffSuffixLsb,
intra_index,
priority: rng.next_u32(),
});
}
for (intra_index, key) in cover.mvd_sign_bypass.positions.iter().enumerate() {
rng.set_word_pos((key.raw() as u128).wrapping_mul(2));
slots.push(ShadowSlot {
domain: EmbedDomain::MvdSignBypass,
intra_index,
priority: rng.next_u32(),
});
}
if let Some(mask) = safe_msl {
for (intra_index, key) in cover.mvd_suffix_lsb.positions.iter().enumerate() {
if intra_index >= mask.len() || !mask[intra_index] {
continue;
}
rng.set_word_pos((key.raw() as u128).wrapping_mul(2));
slots.push(ShadowSlot {
domain: EmbedDomain::MvdSuffixLsb,
intra_index,
priority: rng.next_u32(),
});
}
}
slots.sort_by_key(|s| s.priority);
slots
}
fn priority_slots_all4(
cover: &DomainCover,
perm_seed: &[u8; 32],
) -> Vec<ShadowSlot> {
priority_slots_all4_safe(cover, perm_seed, None, None)
}
pub fn prepare_shadow_all4(
cover: &DomainCover,
shadow_pass: &str,
message: &str,
files: &[FileEntry],
parity_len: usize,
) -> Result<ShadowState, StegoError> {
prepare_shadow_all4_safe(cover, shadow_pass, message, files, parity_len, None, None)
}
pub fn prepare_shadow_all4_safe(
cover: &DomainCover,
shadow_pass: &str,
message: &str,
files: &[FileEntry],
parity_len: usize,
safe_csl: Option<&[bool]>,
safe_msl: Option<&[bool]>,
) -> Result<ShadowState, StegoError> {
let payload_bytes = payload::encode_payload(message, files)?;
let (ciphertext, nonce, salt) = crypto::encrypt(&payload_bytes, shadow_pass)?;
let frame_bytes = build_shadow_frame(payload_bytes.len(), &salt, &nonce, &ciphertext);
let frame_data_len = frame_bytes.len();
let rs_bytes = ecc::rs_encode_blocks_with_parity(&frame_bytes, parity_len);
let rs_bits = frame::bytes_to_bits(&rs_bytes);
let n_total = rs_bits.len();
let perm_seed = crypto::derive_shadow_structural_key(shadow_pass)?;
let slots = priority_slots_all4_safe(cover, &perm_seed, safe_csl, safe_msl);
if slots.len() < n_total {
return Err(StegoError::MessageTooLarge);
}
let positions = slots.into_iter().take(n_total).collect();
Ok(ShadowState {
positions,
bits: rs_bits,
n_total,
parity_len,
frame_data_len,
})
}
pub fn embed_shadow_lsb_all4(
coeff_sign_bypass_bits: &mut [u8],
coeff_suffix_lsb_bits: &mut [u8],
mvd_sign_bypass_bits: &mut [u8],
mvd_suffix_lsb_bits: &mut [u8],
state: &ShadowState,
) {
for (i, slot) in state.positions.iter().enumerate().take(state.n_total) {
let bit = state.bits[i];
match slot.domain {
EmbedDomain::CoeffSignBypass => {
coeff_sign_bypass_bits[slot.intra_index] = bit;
}
EmbedDomain::CoeffSuffixLsb => {
coeff_suffix_lsb_bits[slot.intra_index] = bit;
}
EmbedDomain::MvdSignBypass => {
mvd_sign_bypass_bits[slot.intra_index] = bit;
}
EmbedDomain::MvdSuffixLsb => {
mvd_suffix_lsb_bits[slot.intra_index] = bit;
}
}
}
}
pub fn overlay_infinity_costs_all4(
coeff_sign_bypass_cost: &mut [f32],
coeff_suffix_lsb_cost: &mut [f32],
mvd_sign_bypass_cost: &mut [f32],
mvd_suffix_lsb_cost: &mut [f32],
state: &ShadowState,
) {
for slot in state.positions.iter().take(state.n_total) {
match slot.domain {
EmbedDomain::CoeffSignBypass => {
coeff_sign_bypass_cost[slot.intra_index] = f32::INFINITY;
}
EmbedDomain::CoeffSuffixLsb => {
coeff_suffix_lsb_cost[slot.intra_index] = f32::INFINITY;
}
EmbedDomain::MvdSignBypass => {
mvd_sign_bypass_cost[slot.intra_index] = f32::INFINITY;
}
EmbedDomain::MvdSuffixLsb => {
mvd_suffix_lsb_cost[slot.intra_index] = f32::INFINITY;
}
}
}
}
pub fn apply_shadow_to_plan_all4(
coeff_sign_bypass: &mut [u8],
coeff_suffix_lsb: &mut [u8],
mvd_sign_bypass: &mut [u8],
mvd_suffix_lsb: &mut [u8],
state: &ShadowState,
) {
for (i, slot) in state.positions.iter().enumerate().take(state.n_total) {
let bit = state.bits[i];
match slot.domain {
EmbedDomain::CoeffSignBypass => coeff_sign_bypass[slot.intra_index] = bit,
EmbedDomain::CoeffSuffixLsb => coeff_suffix_lsb[slot.intra_index] = bit,
EmbedDomain::MvdSignBypass => mvd_sign_bypass[slot.intra_index] = bit,
EmbedDomain::MvdSuffixLsb => mvd_suffix_lsb[slot.intra_index] = bit,
}
}
}
pub fn priority_slots_4domain_over_cover(
primary_emit_cover: &DomainCover,
perm_seed: &[u8; 32],
) -> Vec<ShadowSlot> {
priority_slots_all4(primary_emit_cover, perm_seed)
}
pub fn prepare_shadow_over_emit_cover(
primary_emit_cover: &DomainCover,
shadow_pass: &str,
message: &str,
files: &[FileEntry],
parity_len: usize,
) -> Result<ShadowState, StegoError> {
prepare_shadow_all4(
primary_emit_cover,
shadow_pass,
message,
files,
parity_len,
)
}
pub fn prepare_shadow_over_emit_cover_safe(
primary_emit_cover: &DomainCover,
shadow_pass: &str,
message: &str,
files: &[FileEntry],
parity_len: usize,
safe_csl: Option<&[bool]>,
safe_msl: Option<&[bool]>,
) -> Result<ShadowState, StegoError> {
prepare_shadow_all4_safe(
primary_emit_cover,
shadow_pass,
message,
files,
parity_len,
safe_csl,
safe_msl,
)
}
pub fn shadow_extract_all4(
cover: &DomainCover,
passphrase: &str,
) -> Result<PayloadData, StegoError> {
shadow_extract_all4_safe(cover, passphrase, None, None)
}
pub fn shadow_extract_all4_safe(
cover: &DomainCover,
passphrase: &str,
safe_csl: Option<&[bool]>,
safe_msl: Option<&[bool]>,
) -> Result<PayloadData, StegoError> {
if cover.total_len() == 0 {
return Err(StegoError::FrameCorrupted);
}
let perm_seed = crypto::derive_shadow_structural_key(passphrase)?;
let slots = priority_slots_all4_safe(cover, &perm_seed, safe_csl, safe_msl);
let all_lsbs: Vec<u8> = slots
.iter()
.map(|slot| match slot.domain {
EmbedDomain::CoeffSignBypass => cover.coeff_sign_bypass.bits[slot.intra_index],
EmbedDomain::CoeffSuffixLsb => cover.coeff_suffix_lsb.bits[slot.intra_index],
EmbedDomain::MvdSignBypass => cover.mvd_sign_bypass.bits[slot.intra_index],
EmbedDomain::MvdSuffixLsb => cover.mvd_suffix_lsb.bits[slot.intra_index],
})
.collect();
for &parity_len in &SHADOW_PARITY_TIERS {
let k = 255usize.saturating_sub(parity_len);
if k < 2 {
continue;
}
let max_rs_bytes = all_lsbs.len() / 8;
let max_fdl = compute_max_shadow_fdl(max_rs_bytes, parity_len)
.min(MAX_SHADOW_FRAME_BYTES);
if SHADOW_FRAME_OVERHEAD > max_fdl {
continue;
}
if let Some(fdl) = peek_fdl_from_first_block(&all_lsbs, parity_len, max_fdl)
&& let Some(result) = try_single_fdl(&all_lsbs, fdl, parity_len, passphrase)
{
return result;
}
let small_max = (k - 1).min(max_fdl);
if SHADOW_FRAME_OVERHEAD > small_max {
continue;
}
for fdl in SHADOW_FRAME_OVERHEAD..=small_max {
if let Some(result) = try_single_fdl(&all_lsbs, fdl, parity_len, passphrase) {
return result;
}
}
}
Err(StegoError::FrameCorrupted)
}
pub fn shadow_extract(
cover: &DomainCover,
passphrase: &str,
) -> Result<PayloadData, StegoError> {
if cover.total_len() == 0 {
return Err(StegoError::FrameCorrupted);
}
let perm_seed = crypto::derive_shadow_structural_key(passphrase)?;
let slots = priority_slots(cover, &perm_seed);
let all_lsbs: Vec<u8> = slots
.iter()
.map(|slot| match slot.domain {
EmbedDomain::CoeffSignBypass => cover.coeff_sign_bypass.bits[slot.intra_index],
EmbedDomain::CoeffSuffixLsb => cover.coeff_suffix_lsb.bits[slot.intra_index],
EmbedDomain::MvdSignBypass | EmbedDomain::MvdSuffixLsb => {
unreachable!("priority_slots restricted to residual domains")
}
})
.collect();
for &parity_len in &SHADOW_PARITY_TIERS {
let k = 255usize.saturating_sub(parity_len);
if k < 2 {
continue;
}
let max_rs_bytes = all_lsbs.len() / 8;
let max_fdl = compute_max_shadow_fdl(max_rs_bytes, parity_len)
.min(MAX_SHADOW_FRAME_BYTES);
if SHADOW_FRAME_OVERHEAD > max_fdl {
continue;
}
if let Some(fdl) = peek_fdl_from_first_block(&all_lsbs, parity_len, max_fdl)
&& let Some(result) = try_single_fdl(&all_lsbs, fdl, parity_len, passphrase)
{
return result;
}
let small_max = (k - 1).min(max_fdl);
if SHADOW_FRAME_OVERHEAD > small_max {
continue;
}
for fdl in SHADOW_FRAME_OVERHEAD..=small_max {
if let Some(result) = try_single_fdl(&all_lsbs, fdl, parity_len, passphrase) {
return result;
}
}
}
Err(StegoError::FrameCorrupted)
}
#[allow(dead_code)]
fn _unused_imports_guard() -> usize {
NONCE_LEN + SALT_LEN
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::DomainBits;
fn synth_cover(n_per_domain: usize) -> DomainCover {
let mut cover = DomainCover::default();
let mut s: u32 = 0xDEAD_BEEF;
let mut next_key = || {
s = s.wrapping_mul(1103515245).wrapping_add(12345);
s
};
let mut push_bits = |bits: &mut DomainBits, domain: EmbedDomain| {
for _ in 0..n_per_domain {
let raw = next_key();
let bit = (raw & 1) as u8;
let path = super::super::SyntaxPath::Mvd {
list: 0, partition: 0,
axis: super::super::Axis::X,
kind: super::super::BinKind::Sign,
};
let key = PositionKey::new(
(raw >> 16) & 0xFF,
(raw >> 8) & 0xFFFF,
domain,
path,
);
bits.bits.push(bit);
bits.positions.push(key);
}
};
push_bits(&mut cover.coeff_sign_bypass, EmbedDomain::CoeffSignBypass);
push_bits(&mut cover.coeff_suffix_lsb, EmbedDomain::CoeffSuffixLsb);
push_bits(&mut cover.mvd_sign_bypass, EmbedDomain::MvdSignBypass);
push_bits(&mut cover.mvd_suffix_lsb, EmbedDomain::MvdSuffixLsb);
cover
}
#[test]
fn shadow_all4_prepare_inject_extract_roundtrip() {
let mut cover = synth_cover(2000);
let state = prepare_shadow_all4(
&cover, "test-pass", "hello shadow", &[], 4,
).expect("prepare 4-domain shadow");
let (csb, csl, msb, msl) = (
std::mem::take(&mut cover.coeff_sign_bypass.bits),
std::mem::take(&mut cover.coeff_suffix_lsb.bits),
std::mem::take(&mut cover.mvd_sign_bypass.bits),
std::mem::take(&mut cover.mvd_suffix_lsb.bits),
);
let mut csb = csb;
let mut csl = csl;
let mut msb = msb;
let mut msl = msl;
embed_shadow_lsb_all4(&mut csb, &mut csl, &mut msb, &mut msl, &state);
cover.coeff_sign_bypass.bits = csb;
cover.coeff_suffix_lsb.bits = csl;
cover.mvd_sign_bypass.bits = msb;
cover.mvd_suffix_lsb.bits = msl;
let recovered = shadow_extract_all4(&cover, "test-pass")
.expect("extract 4-domain shadow");
assert_eq!(recovered.text, "hello shadow");
}
#[test]
fn shadow_all4_positions_span_all_domains() {
let cover = synth_cover(2000);
let state = prepare_shadow_all4(
&cover, "test-pass", "x", &[], 4,
).expect("prepare");
let mut domain_count = std::collections::HashMap::new();
for slot in state.positions.iter().take(state.n_total) {
*domain_count.entry(slot.domain as u8).or_insert(0usize) += 1;
}
assert!(domain_count.len() >= 3,
"expected positions in 3+ domains, got {:?}", domain_count);
}
#[test]
fn priority_slots_4domain_over_cover_matches_priority_slots_all4() {
let cover = synth_cover(500);
let seed = [42u8; 32];
let a = priority_slots_4domain_over_cover(&cover, &seed);
let b = priority_slots_all4(&cover, &seed);
assert_eq!(a.len(), b.len());
for (x, y) in a.iter().zip(b.iter()) {
assert_eq!(x.domain as u8, y.domain as u8);
assert_eq!(x.intra_index, y.intra_index);
assert_eq!(x.priority, y.priority);
}
}
#[test]
fn prepare_shadow_over_emit_cover_matches_prepare_shadow_all4() {
let cover = synth_cover(2000);
let pass = "polish-test-pass";
let msg = "polish hello";
let parity = 4;
let polish = prepare_shadow_over_emit_cover(
&cover, pass, msg, &[], parity,
).expect("polish prepare");
let legacy = prepare_shadow_all4(
&cover, pass, msg, &[], parity,
).expect("legacy prepare");
assert_eq!(polish.n_total, legacy.n_total);
assert_eq!(polish.parity_len, legacy.parity_len);
assert_eq!(polish.frame_data_len, legacy.frame_data_len);
assert_eq!(polish.bits.len(), legacy.bits.len());
assert_eq!(polish.positions.len(), legacy.positions.len());
for (a, b) in polish.positions.iter().zip(legacy.positions.iter()) {
assert_eq!(a.domain as u8, b.domain as u8);
assert_eq!(a.intra_index, b.intra_index);
assert_eq!(a.priority, b.priority);
}
}
#[test]
fn shadow_over_emit_cover_roundtrip_self_consistent() {
let mut cover = synth_cover(2000);
let state = prepare_shadow_over_emit_cover(
&cover, "polish-roundtrip", "single-cover msg", &[], 4,
).expect("polish prepare");
let (csb, csl, msb, msl) = (
std::mem::take(&mut cover.coeff_sign_bypass.bits),
std::mem::take(&mut cover.coeff_suffix_lsb.bits),
std::mem::take(&mut cover.mvd_sign_bypass.bits),
std::mem::take(&mut cover.mvd_suffix_lsb.bits),
);
let mut csb = csb;
let mut csl = csl;
let mut msb = msb;
let mut msl = msl;
embed_shadow_lsb_all4(&mut csb, &mut csl, &mut msb, &mut msl, &state);
cover.coeff_sign_bypass.bits = csb;
cover.coeff_suffix_lsb.bits = csl;
cover.mvd_sign_bypass.bits = msb;
cover.mvd_suffix_lsb.bits = msl;
let recovered = shadow_extract_all4(&cover, "polish-roundtrip")
.expect("extract polish-prepared shadow");
assert_eq!(recovered.text, "single-cover msg");
}
}