use crate::stego::error::StegoError;
use crate::stego::frame::{self, FRAME_OVERHEAD};
use crate::stego::stc::extract::stc_extract;
use crate::stego::stc::hhat::generate_hhat;
use crate::stego::{crypto, payload};
use crate::stego::payload::PayloadData;
use crate::codec::h264::cabac::bin_decoder::{
walk_annex_b_for_cover, walk_annex_b_for_cover_with_options,
walk_nalus_for_cover, WalkOptions,
};
use crate::codec::h264::NalUnit;
use super::hook::EmbedDomain;
use super::keys::CabacStegoMasterKeys;
use super::orchestrate::{split_message_per_domain, DomainMessages};
use super::DomainCover;
pub fn h264_stego_decode_yuv_string(
annex_b: &[u8],
passphrase: &str,
) -> Result<String, StegoError> {
let walk = walk_annex_b_for_cover(annex_b)
.map_err(|e| StegoError::InvalidVideo(format!("walk: {e}")))?;
decode_from_cover(walk.cover, passphrase)
}
pub fn h264_stego_decode_nalus_string(
nalus: &[NalUnit],
passphrase: &str,
) -> Result<String, StegoError> {
let walk = walk_nalus_for_cover(nalus)
.map_err(|e| StegoError::InvalidVideo(format!("walk nalus: {e}")))?;
decode_from_cover(walk.cover, passphrase)
}
fn decode_from_cover(
cover: DomainCover,
passphrase: &str,
) -> Result<String, StegoError> {
let keys = CabacStegoMasterKeys::derive(passphrase)?;
let seeds = [
(EmbedDomain::CoeffSignBypass,
keys.per_gop_seeds(EmbedDomain::CoeffSignBypass, 0).hhat_seed),
(EmbedDomain::CoeffSuffixLsb,
keys.per_gop_seeds(EmbedDomain::CoeffSuffixLsb, 0).hhat_seed),
(EmbedDomain::MvdSignBypass,
keys.per_gop_seeds(EmbedDomain::MvdSignBypass, 0).hhat_seed),
(EmbedDomain::MvdSuffixLsb,
keys.per_gop_seeds(EmbedDomain::MvdSuffixLsb, 0).hhat_seed),
];
const STC_H: usize = 4;
let capacities = cover.capacity();
let total_n_bits: usize = capacities.total();
if total_n_bits == 0 {
return Err(StegoError::InvalidVideo("empty cover".into()));
}
let max_m_total_bits = (frame::MAX_FRAME_BITS).min(total_n_bits);
let min_m_total_bits = FRAME_OVERHEAD * 8;
let mut m_total = min_m_total_bits;
while m_total <= max_m_total_bits {
if let Some(plaintext) = try_decode_at(
&cover, &seeds, STC_H, m_total, passphrase,
) {
return Ok(plaintext);
}
m_total += 8;
}
Err(StegoError::FrameCorrupted)
}
pub fn h264_stego_decode_yuv_string_4domain(
annex_b: &[u8],
passphrase: &str,
) -> Result<String, StegoError> {
let opts = WalkOptions { record_mvd: true };
let walk = walk_annex_b_for_cover_with_options(annex_b, opts)
.map_err(|e| StegoError::InvalidVideo(format!("walk: {e}")))?;
decode_from_cover_4domain(walk.cover, passphrase)
}
pub fn h264_stego_shadow_decode(
annex_b: &[u8],
passphrase: &str,
) -> Result<String, StegoError> {
let opts = WalkOptions { record_mvd: true };
let walk = walk_annex_b_for_cover_with_options(annex_b, opts)
.map_err(|e| StegoError::InvalidVideo(format!("walk: {e}")))?;
let safe_msb = super::cascade_safety::analyze_safe_mvd_subset(
&walk.mvd_meta, walk.mb_w, walk.mb_h,
);
let safe_msl = super::cascade_safety::derive_msl_safe_from_msb(
&walk.cover.mvd_sign_bypass.positions,
&safe_msb,
&walk.cover.mvd_suffix_lsb.positions,
);
let payload_data = super::shadow::shadow_extract_all4_safe(
&walk.cover, passphrase, None, Some(&safe_msl),
)?;
Ok(payload_data.text)
}
pub fn h264_stego_smart_decode_video(
annex_b: &[u8],
passphrase: &str,
) -> Result<String, StegoError> {
h264_stego_smart_decode_video_with_payload(annex_b, passphrase).map(|p| p.text)
}
pub fn h264_stego_smart_decode_video_with_payload(
annex_b: &[u8],
passphrase: &str,
) -> Result<PayloadData, StegoError> {
let opts = WalkOptions { record_mvd: true };
let walk = walk_annex_b_for_cover_with_options(annex_b, opts)
.map_err(|e| StegoError::InvalidVideo(format!("walk: {e}")))?;
let safe_msb = super::cascade_safety::analyze_safe_mvd_subset(
&walk.mvd_meta, walk.mb_w, walk.mb_h,
);
let safe_msl = super::cascade_safety::derive_msl_safe_from_msb(
&walk.cover.mvd_sign_bypass.positions,
&safe_msb,
&walk.cover.mvd_suffix_lsb.positions,
);
if let Ok(payload_data) = super::shadow::shadow_extract_all4_safe(
&walk.cover, passphrase, None, Some(&safe_msl),
) {
return Ok(payload_data);
}
decode_from_cover_4domain_with_payload(walk.cover, passphrase)
}
fn decode_from_cover_4domain(
cover: DomainCover,
passphrase: &str,
) -> Result<String, StegoError> {
decode_from_cover_4domain_with_payload(cover, passphrase).map(|p| p.text)
}
fn decode_from_cover_4domain_with_payload(
cover: DomainCover,
passphrase: &str,
) -> Result<PayloadData, StegoError> {
let keys = CabacStegoMasterKeys::derive(passphrase)?;
let seeds = [
(EmbedDomain::CoeffSignBypass,
keys.per_gop_seeds(EmbedDomain::CoeffSignBypass, 0).hhat_seed),
(EmbedDomain::CoeffSuffixLsb,
keys.per_gop_seeds(EmbedDomain::CoeffSuffixLsb, 0).hhat_seed),
(EmbedDomain::MvdSignBypass,
keys.per_gop_seeds(EmbedDomain::MvdSignBypass, 0).hhat_seed),
(EmbedDomain::MvdSuffixLsb,
keys.per_gop_seeds(EmbedDomain::MvdSuffixLsb, 0).hhat_seed),
];
const STC_H: usize = 4;
let total_n_bits = cover.coeff_sign_bypass.len()
+ cover.coeff_suffix_lsb.len()
+ cover.mvd_sign_bypass.len()
+ cover.mvd_suffix_lsb.len();
if total_n_bits == 0 {
return Err(StegoError::InvalidVideo("empty cover".into()));
}
let max_m_total_bits = (frame::MAX_FRAME_BITS).min(total_n_bits);
let min_m_total_bits = FRAME_OVERHEAD * 8;
let mut m_total = min_m_total_bits;
while m_total <= max_m_total_bits {
if let Some(payload_data) = try_decode_at_4domain(
&cover, &seeds, STC_H, m_total, passphrase,
) {
return Ok(payload_data);
}
m_total += 8;
}
Err(StegoError::FrameCorrupted)
}
fn try_decode_at_4domain(
cover: &DomainCover,
seeds: &[(EmbedDomain, [u8; 32]); 4],
h: usize,
m_total_bits: usize,
passphrase: &str,
) -> Option<PayloadData> {
use super::hook::GopCapacity;
let cap_for_alloc = GopCapacity {
coeff_sign_bypass: cover.coeff_sign_bypass.len(),
coeff_suffix_lsb: cover.coeff_suffix_lsb.len(),
mvd_sign_bypass: cover.mvd_sign_bypass.len(),
mvd_suffix_lsb: 0,
};
let allocator = super::orchestrate::StealthAllocator::v1_default();
let (m_cs, m_cl, m_ms, _m_ml) =
super::orchestrate::stealth_weighted_allocation(
m_total_bits, &cap_for_alloc, &allocator,
)?;
let m_mvd = m_ms; let m_residual = m_cs + m_cl;
debug_assert_eq!(m_total_bits, m_mvd + m_residual,
"decoder stealth-weighted alloc must conserve m_total");
let cap_mvd = GopCapacity {
coeff_sign_bypass: 0,
coeff_suffix_lsb: 0,
mvd_sign_bypass: cover.mvd_sign_bypass.len(),
mvd_suffix_lsb: 0,
};
let stub_mvd = vec![0u8; m_mvd];
let split_a = split_message_per_domain(&stub_mvd, &cap_mvd)?;
let cap_coeff = GopCapacity {
coeff_sign_bypass: cover.coeff_sign_bypass.len(),
coeff_suffix_lsb: cover.coeff_suffix_lsb.len(),
mvd_sign_bypass: 0,
mvd_suffix_lsb: 0,
};
let stub_residual = vec![0u8; m_residual];
let split_b = split_message_per_domain(&stub_residual, &cap_coeff)?;
let mvd_sign = extract_one_domain(
&cover.mvd_sign_bypass.bits,
split_a.mvd_sign_bypass.len(),
h, &seeds[2].1,
)?;
let mvd_suffix = extract_one_domain(
&cover.mvd_suffix_lsb.bits,
split_a.mvd_suffix_lsb.len(),
h, &seeds[3].1,
)?;
let coeff_sign = extract_one_domain(
&cover.coeff_sign_bypass.bits,
split_b.coeff_sign_bypass.len(),
h, &seeds[0].1,
)?;
let coeff_suffix = extract_one_domain(
&cover.coeff_suffix_lsb.bits,
split_b.coeff_suffix_lsb.len(),
h, &seeds[1].1,
)?;
let mut all_bits = Vec::with_capacity(m_total_bits);
all_bits.extend_from_slice(&mvd_sign);
all_bits.extend_from_slice(&mvd_suffix);
all_bits.extend_from_slice(&coeff_sign);
all_bits.extend_from_slice(&coeff_suffix);
if all_bits.len() != m_total_bits {
return None;
}
let frame_bytes = bits_to_bytes_msb_first(&all_bits);
let parsed = frame::parse_frame(&frame_bytes).ok()?;
let plaintext = crypto::decrypt(
&parsed.ciphertext, passphrase, &parsed.salt, &parsed.nonce,
).ok()?;
let payload_data = payload::decode_payload(&plaintext).ok()?;
Some(payload_data)
}
fn try_decode_at(
cover: &DomainCover,
seeds: &[(EmbedDomain, [u8; 32]); 4],
h: usize,
m_total_bits: usize,
passphrase: &str,
) -> Option<String> {
let stub: Vec<u8> = vec![0u8; m_total_bits];
let capacities = cover.capacity();
let split: DomainMessages = split_message_per_domain(&stub, &capacities)?;
let coeff_sign = extract_one_domain(
&cover.coeff_sign_bypass.bits,
split.coeff_sign_bypass.len(),
h, &seeds[0].1,
)?;
let coeff_suffix = extract_one_domain(
&cover.coeff_suffix_lsb.bits,
split.coeff_suffix_lsb.len(),
h, &seeds[1].1,
)?;
let mvd_sign = extract_one_domain(
&cover.mvd_sign_bypass.bits,
split.mvd_sign_bypass.len(),
h, &seeds[2].1,
)?;
let mvd_suffix = extract_one_domain(
&cover.mvd_suffix_lsb.bits,
split.mvd_suffix_lsb.len(),
h, &seeds[3].1,
)?;
let mut all_bits = Vec::with_capacity(m_total_bits);
all_bits.extend_from_slice(&coeff_sign);
all_bits.extend_from_slice(&coeff_suffix);
all_bits.extend_from_slice(&mvd_sign);
all_bits.extend_from_slice(&mvd_suffix);
if all_bits.len() != m_total_bits {
return None;
}
let frame_bytes = bits_to_bytes_msb_first(&all_bits);
let parsed = frame::parse_frame(&frame_bytes).ok()?;
let plaintext = crypto::decrypt(
&parsed.ciphertext, passphrase, &parsed.salt, &parsed.nonce,
).ok()?;
let payload_data = payload::decode_payload(&plaintext).ok()?;
Some(payload_data.text)
}
fn extract_one_domain(
cover_bits: &[u8],
m_d: usize,
h: usize,
seed: &[u8; 32],
) -> Option<Vec<u8>> {
if m_d == 0 {
return Some(Vec::new());
}
let n_d = cover_bits.len();
if m_d > n_d {
return None;
}
let w_d = n_d / m_d;
if w_d == 0 {
return None;
}
let hhat = generate_hhat(h, w_d, seed);
let cover_slice = &cover_bits[..m_d * w_d];
Some(stc_extract(cover_slice, &hhat, w_d))
}
fn bits_to_bytes_msb_first(bits: &[u8]) -> Vec<u8> {
let n_bytes = bits.len() / 8;
let mut out = Vec::with_capacity(n_bytes);
for byte_idx in 0..n_bytes {
let mut byte = 0u8;
for i in 0..8 {
byte |= (bits[byte_idx * 8 + i] & 1) << (7 - i);
}
out.push(byte);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::encode_pixels::h264_stego_encode_yuv_string;
fn deterministic_yuv(w: u32, h: u32, n_frames: usize) -> Vec<u8> {
let frame_size = (w * h * 3 / 2) as usize;
let mut out = Vec::with_capacity(frame_size * n_frames);
let mut s: u32 = 0xCAFE_F00D;
for _ in 0..n_frames {
for _ in 0..frame_size {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
out.push((s >> 16) as u8);
}
}
out
}
#[test]
fn roundtrip_short_string_32x32() {
let yuv = deterministic_yuv(32, 32, 1);
let pass = "test-pass";
let msg = "hi";
let bytes = h264_stego_encode_yuv_string(
&yuv, 32, 32, 1, msg, pass,
).expect("encode");
let recovered = h264_stego_decode_yuv_string(&bytes, pass)
.expect("decode");
assert_eq!(recovered, msg);
}
#[test]
fn roundtrip_wrong_passphrase_fails() {
let yuv = deterministic_yuv(32, 32, 1);
let bytes = h264_stego_encode_yuv_string(
&yuv, 32, 32, 1, "secret", "right",
).expect("encode");
let r = h264_stego_decode_yuv_string(&bytes, "wrong");
assert!(r.is_err(), "wrong passphrase must fail");
}
#[test]
fn roundtrip_multi_row_64x64() {
let yuv = deterministic_yuv(64, 64, 1);
let pass = "multi-row";
let msg = "test message across multi-row frame";
let bytes = h264_stego_encode_yuv_string(
&yuv, 64, 64, 1, msg, pass,
).expect("encode");
let recovered = h264_stego_decode_yuv_string(&bytes, pass)
.expect("decode");
assert_eq!(recovered, msg);
}
#[test]
fn roundtrip_i_then_p_frames_4domain_32x32() {
use crate::codec::h264::stego::encode_pixels::
h264_stego_encode_yuv_string_4domain;
let yuv = deterministic_yuv(32, 32, 3);
let pass = "p-4domain";
let msg = "hello via 4-domain stego";
let bytes = h264_stego_encode_yuv_string_4domain(
&yuv, 32, 32, 3, msg, pass,
).expect("encode 4-domain");
let recovered = h264_stego_decode_yuv_string_4domain(&bytes, pass)
.expect("decode 4-domain");
assert_eq!(recovered, msg);
}
#[test]
fn roundtrip_i_then_p_frames_32x32() {
use crate::codec::h264::stego::encode_pixels::
h264_stego_encode_yuv_string_i_then_p;
let yuv = deterministic_yuv(32, 32, 3);
let pass = "p-roundtrip";
let msg = "hello via P-slices";
let bytes = h264_stego_encode_yuv_string_i_then_p(
&yuv, 32, 32, 3, msg, pass,
).expect("encode I+P");
let recovered = h264_stego_decode_yuv_string(&bytes, pass)
.expect("decode");
assert_eq!(recovered, msg);
}
#[test]
fn roundtrip_multi_frame_32x32_3_idrs() {
let yuv = deterministic_yuv(32, 32, 3);
let pass = "multi-frame";
let msg = "spread me across 3 IDR frames";
let bytes = h264_stego_encode_yuv_string(
&yuv, 32, 32, 3, msg, pass,
).expect("encode");
let recovered = h264_stego_decode_yuv_string(&bytes, pass)
.expect("decode");
assert_eq!(recovered, msg);
}
#[test]
fn bits_to_bytes_msb_first_roundtrip() {
let original = [0xAB, 0xCD, 0x12];
let bits: Vec<u8> = original.iter()
.flat_map(|&b| (0..8).rev().map(move |i| (b >> i) & 1))
.collect();
let recovered = bits_to_bytes_msb_first(&bits);
assert_eq!(recovered, original);
}
}