use crate::stego::crypto::{NONCE_LEN, SALT_LEN};
use crate::stego::error::StegoError;
pub const MODE_GHOST: u8 = 0x01;
pub const MODE_ARMOR: u8 = 0x02;
pub const FRAME_OVERHEAD: usize = 2 + SALT_LEN + NONCE_LEN + 16 + 4;
pub const FRAME_OVERHEAD_EXT: usize = 2 + 4 + SALT_LEN + NONCE_LEN + 16 + 4;
pub const MAX_FRAME_BYTES: usize = 256 * 1024;
pub const MAX_FRAME_BITS: usize = MAX_FRAME_BYTES * 8;
pub fn build_frame(
plaintext_len: usize,
salt: &[u8; SALT_LEN],
nonce: &[u8; NONCE_LEN],
ciphertext: &[u8],
) -> Vec<u8> {
debug_assert_eq!(ciphertext.len(), plaintext_len + 16, "ciphertext length mismatch");
assert!(plaintext_len <= u32::MAX as usize, "plaintext exceeds u32::MAX");
let is_v2 = plaintext_len > u16::MAX as usize;
let header_len = if is_v2 { 6 } else { 2 };
let mut frame = Vec::with_capacity(header_len + SALT_LEN + NONCE_LEN + ciphertext.len() + 4);
if is_v2 {
frame.extend_from_slice(&0u16.to_be_bytes()); frame.extend_from_slice(&(plaintext_len as u32).to_be_bytes());
} else {
frame.extend_from_slice(&(plaintext_len as u16).to_be_bytes());
}
frame.extend_from_slice(salt);
frame.extend_from_slice(nonce);
frame.extend_from_slice(ciphertext);
let crc = crc32fast::hash(&frame);
frame.extend_from_slice(&crc.to_be_bytes());
frame
}
pub struct ParsedFrame {
pub plaintext_len: u32,
pub salt: [u8; SALT_LEN],
pub nonce: [u8; NONCE_LEN],
pub ciphertext: Vec<u8>,
}
pub fn parse_frame(data: &[u8]) -> Result<ParsedFrame, StegoError> {
if data.len() < 2 {
return Err(StegoError::FrameCorrupted);
}
let header_u16 = u16::from_be_bytes([data[0], data[1]]);
let (plaintext_len, header_len): (usize, usize) = if header_u16 == 0 && data.len() >= 6 {
let v2_len = u32::from_be_bytes([data[2], data[3], data[4], data[5]]) as usize;
if v2_len > u16::MAX as usize {
(v2_len, 6)
} else {
(0, 2)
}
} else {
(header_u16 as usize, 2)
};
let ciphertext_len = plaintext_len + 16; let total_frame_len = header_len + SALT_LEN + NONCE_LEN + ciphertext_len + 4;
if total_frame_len > MAX_FRAME_BYTES || data.len() < total_frame_len {
return Err(StegoError::FrameCorrupted);
}
let payload = &data[..total_frame_len - 4];
let crc_bytes = &data[total_frame_len - 4..total_frame_len];
let stored_crc = u32::from_be_bytes([crc_bytes[0], crc_bytes[1], crc_bytes[2], crc_bytes[3]]);
let computed_crc = crc32fast::hash(payload);
if stored_crc != computed_crc {
return Err(StegoError::FrameCorrupted);
}
let mut salt = [0u8; SALT_LEN];
salt.copy_from_slice(&payload[header_len..header_len + SALT_LEN]);
let mut nonce = [0u8; NONCE_LEN];
nonce.copy_from_slice(&payload[header_len + SALT_LEN..header_len + SALT_LEN + NONCE_LEN]);
let ciphertext = payload[header_len + SALT_LEN + NONCE_LEN..].to_vec();
Ok(ParsedFrame {
plaintext_len: plaintext_len as u32,
salt,
nonce,
ciphertext,
})
}
pub const FORTRESS_COMPACT_FRAME_OVERHEAD: usize = 2 + 16 + 4;
pub const FORTRESS_COMPACT_FRAME_OVERHEAD_EXT: usize = 2 + 4 + 16 + 4;
pub fn build_fortress_compact_frame(
plaintext_len: usize,
ciphertext: &[u8],
) -> Vec<u8> {
assert!(plaintext_len <= u32::MAX as usize, "plaintext exceeds u32::MAX");
let is_v2 = plaintext_len > u16::MAX as usize;
let header_len = if is_v2 { 6 } else { 2 };
let mut frame = Vec::with_capacity(header_len + ciphertext.len() + 4);
if is_v2 {
frame.extend_from_slice(&0u16.to_be_bytes());
frame.extend_from_slice(&(plaintext_len as u32).to_be_bytes());
} else {
frame.extend_from_slice(&(plaintext_len as u16).to_be_bytes());
}
frame.extend_from_slice(ciphertext);
let crc = crc32fast::hash(&frame);
frame.extend_from_slice(&crc.to_be_bytes());
frame
}
pub fn parse_fortress_compact_frame(data: &[u8]) -> Result<ParsedFrame, StegoError> {
use crate::stego::crypto::{FORTRESS_EMPTY_SALT, FORTRESS_EMPTY_NONCE};
if data.len() < 2 {
return Err(StegoError::FrameCorrupted);
}
let header_u16 = u16::from_be_bytes([data[0], data[1]]);
let (plaintext_len, header_len): (usize, usize) = if header_u16 == 0 && data.len() >= 6 {
let v2_len = u32::from_be_bytes([data[2], data[3], data[4], data[5]]) as usize;
if v2_len > u16::MAX as usize {
(v2_len, 6)
} else {
(0, 2)
}
} else {
(header_u16 as usize, 2)
};
let ciphertext_len = plaintext_len + 16;
let total_frame_len = header_len + ciphertext_len + 4;
if total_frame_len > MAX_FRAME_BYTES || data.len() < total_frame_len {
return Err(StegoError::FrameCorrupted);
}
let payload = &data[..total_frame_len - 4];
let crc_bytes = &data[total_frame_len - 4..total_frame_len];
let stored_crc = u32::from_be_bytes([crc_bytes[0], crc_bytes[1], crc_bytes[2], crc_bytes[3]]);
let computed_crc = crc32fast::hash(payload);
if stored_crc != computed_crc {
return Err(StegoError::FrameCorrupted);
}
let ciphertext = payload[header_len..].to_vec();
Ok(ParsedFrame {
plaintext_len: plaintext_len as u32,
salt: FORTRESS_EMPTY_SALT,
nonce: FORTRESS_EMPTY_NONCE,
ciphertext,
})
}
pub fn bytes_to_bits(bytes: &[u8]) -> Vec<u8> {
let mut bits = Vec::with_capacity(bytes.len() * 8);
for &byte in bytes {
for bit_pos in (0..8).rev() {
bits.push((byte >> bit_pos) & 1);
}
}
bits
}
pub fn bits_to_bytes(bits: &[u8]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(bits.len().div_ceil(8));
for chunk in bits.chunks(8) {
let mut byte = 0u8;
for (i, &bit) in chunk.iter().enumerate() {
byte |= (bit & 1) << (7 - i);
}
bytes.push(byte);
}
bytes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_parse_roundtrip() {
let salt = [1u8; SALT_LEN];
let nonce = [2u8; NONCE_LEN];
let ciphertext = vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
0x77, 0x88, 0x99, 0x00, 0xAA, 0xBB];
let frame = build_frame(2, &salt, &nonce, &ciphertext);
let parsed = parse_frame(&frame).unwrap();
assert_eq!(parsed.plaintext_len, 2);
assert_eq!(parsed.salt, salt);
assert_eq!(parsed.nonce, nonce);
assert_eq!(parsed.ciphertext, ciphertext);
}
#[test]
fn corrupted_crc_detected() {
let salt = [0u8; SALT_LEN];
let nonce = [0u8; NONCE_LEN];
let ciphertext = vec![0u8; 20];
let mut frame = build_frame(4, &salt, &nonce, &ciphertext);
let len = frame.len();
frame[len - 1] ^= 0xFF;
assert!(matches!(parse_frame(&frame), Err(StegoError::FrameCorrupted)));
}
#[test]
fn corrupted_length_detected() {
let salt = [0u8; SALT_LEN];
let nonce = [0u8; NONCE_LEN];
let ciphertext = vec![0u8; 20];
let mut frame = build_frame(4, &salt, &nonce, &ciphertext);
frame[0] = 0xFF;
assert!(matches!(parse_frame(&frame), Err(StegoError::FrameCorrupted)));
}
#[test]
fn bytes_bits_roundtrip() {
let original = vec![0xDE, 0xAD, 0xBE, 0xEF];
let bits = bytes_to_bits(&original);
assert_eq!(bits.len(), 32);
let recovered = bits_to_bytes(&bits);
assert_eq!(recovered, original);
}
#[test]
fn truncated_data_rejected() {
assert!(matches!(parse_frame(&[0x00]), Err(StegoError::FrameCorrupted)));
assert!(matches!(parse_frame(&[]), Err(StegoError::FrameCorrupted)));
}
#[test]
fn frame_no_mode_byte() {
let salt = [3u8; SALT_LEN];
let nonce = [4u8; NONCE_LEN];
let ciphertext = vec![0x55u8; 20]; let frame = build_frame(4, &salt, &nonce, &ciphertext);
assert_eq!(frame[0], 0x00);
assert_eq!(frame[1], 0x04);
assert_eq!(frame.len(), 2 + SALT_LEN + NONCE_LEN + 20 + 4);
let parsed = parse_frame(&frame).unwrap();
assert_eq!(parsed.plaintext_len, 4);
assert_eq!(parsed.salt, salt);
assert_eq!(parsed.nonce, nonce);
assert_eq!(parsed.ciphertext, ciphertext);
}
#[test]
fn frame_with_zero_length_data() {
let salt = [0u8; SALT_LEN];
let nonce = [0u8; NONCE_LEN];
let ciphertext = vec![0u8; 16];
let frame = build_frame(0, &salt, &nonce, &ciphertext);
let parsed = parse_frame(&frame).unwrap();
assert_eq!(parsed.plaintext_len, 0);
assert_eq!(parsed.ciphertext.len(), 16);
}
#[test]
fn bits_to_bytes_partial_byte() {
let bits = vec![1u8, 0, 1, 1, 0];
let bytes = bits_to_bytes(&bits);
assert_eq!(bytes.len(), 1);
assert_eq!(bytes[0], 0xB0);
}
#[test]
fn compact_frame_build_parse_roundtrip() {
let ciphertext = vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
0x77, 0x88, 0x99, 0x00, 0xAA, 0xBB,
0xCC, 0xDD];
let frame = build_fortress_compact_frame(4, &ciphertext);
assert_eq!(frame.len(), 2 + 20 + 4);
let parsed = parse_fortress_compact_frame(&frame).unwrap();
assert_eq!(parsed.plaintext_len, 4);
assert_eq!(parsed.ciphertext, ciphertext);
assert_eq!(parsed.salt, crate::stego::crypto::FORTRESS_EMPTY_SALT);
assert_eq!(parsed.nonce, crate::stego::crypto::FORTRESS_EMPTY_NONCE);
}
#[test]
fn compact_frame_smaller_than_full() {
let salt = [1u8; SALT_LEN];
let nonce = [2u8; NONCE_LEN];
let ciphertext = vec![0u8; 20];
let full_frame = build_frame(4, &salt, &nonce, &ciphertext);
let compact_frame = build_fortress_compact_frame(4, &ciphertext);
assert_eq!(full_frame.len() - compact_frame.len(), SALT_LEN + NONCE_LEN);
assert_eq!(full_frame.len() - compact_frame.len(), 28);
}
#[test]
fn compact_frame_corrupted_crc_detected() {
let ciphertext = vec![0u8; 20];
let mut frame = build_fortress_compact_frame(4, &ciphertext);
let len = frame.len();
frame[len - 1] ^= 0xFF;
assert!(matches!(parse_fortress_compact_frame(&frame), Err(StegoError::FrameCorrupted)));
}
#[test]
fn compact_frame_truncated_rejected() {
assert!(matches!(parse_fortress_compact_frame(&[0x00]), Err(StegoError::FrameCorrupted)));
assert!(matches!(parse_fortress_compact_frame(&[]), Err(StegoError::FrameCorrupted)));
}
#[test]
fn compact_frame_zero_length() {
let ciphertext = vec![0u8; 16];
let frame = build_fortress_compact_frame(0, &ciphertext);
let parsed = parse_fortress_compact_frame(&frame).unwrap();
assert_eq!(parsed.plaintext_len, 0);
assert_eq!(parsed.ciphertext.len(), 16);
}
#[test]
fn compact_frame_overhead_is_28_less() {
assert_eq!(
FRAME_OVERHEAD - FORTRESS_COMPACT_FRAME_OVERHEAD,
28,
"Compact frame saves exactly 28 bytes (salt + nonce)"
);
}
#[test]
fn v2_ext_overhead_is_4_more() {
assert_eq!(FRAME_OVERHEAD_EXT - FRAME_OVERHEAD, 4);
assert_eq!(FORTRESS_COMPACT_FRAME_OVERHEAD_EXT - FORTRESS_COMPACT_FRAME_OVERHEAD, 4);
}
#[test]
fn v2_frame_build_parse_roundtrip() {
let salt = [5u8; SALT_LEN];
let nonce = [6u8; NONCE_LEN];
let plaintext_len = 70_000usize; let ciphertext = vec![0xAB; plaintext_len + 16];
let frame = build_frame(plaintext_len, &salt, &nonce, &ciphertext);
assert_eq!(frame.len(), FRAME_OVERHEAD_EXT + plaintext_len);
assert_eq!(frame[0], 0x00);
assert_eq!(frame[1], 0x00);
assert_eq!(u32::from_be_bytes([frame[2], frame[3], frame[4], frame[5]]), 70_000);
let parsed = parse_frame(&frame).unwrap();
assert_eq!(parsed.plaintext_len, 70_000);
assert_eq!(parsed.salt, salt);
assert_eq!(parsed.nonce, nonce);
assert_eq!(parsed.ciphertext, ciphertext);
}
#[test]
fn v1_frame_still_uses_u16_header() {
let salt = [7u8; SALT_LEN];
let nonce = [8u8; NONCE_LEN];
let plaintext_len = 1000usize; let ciphertext = vec![0xCD; plaintext_len + 16];
let frame = build_frame(plaintext_len, &salt, &nonce, &ciphertext);
assert_eq!(frame.len(), FRAME_OVERHEAD + plaintext_len);
assert_eq!(u16::from_be_bytes([frame[0], frame[1]]), 1000);
let parsed = parse_frame(&frame).unwrap();
assert_eq!(parsed.plaintext_len, 1000);
}
#[test]
fn v2_compact_frame_roundtrip() {
let plaintext_len = 70_000usize;
let ciphertext = vec![0xEF; plaintext_len + 16];
let frame = build_fortress_compact_frame(plaintext_len, &ciphertext);
assert_eq!(frame.len(), FORTRESS_COMPACT_FRAME_OVERHEAD_EXT + plaintext_len);
let parsed = parse_fortress_compact_frame(&frame).unwrap();
assert_eq!(parsed.plaintext_len, 70_000);
assert_eq!(parsed.ciphertext, ciphertext);
}
}