use crate::error::{Result, StegoError};
pub const CHUNK_HEADER_SIZE: usize = 88;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChunkHeader {
pub message_id: [u8; 16],
pub chunk_index: u8,
pub total_chunks: u8,
pub payload_length: u64,
pub argon2_salt: [u8; 16],
pub aes_nonce: [u8; 12],
pub payload_hash: [u8; 32],
pub bits_per_channel: u8,
pub reserved: u8,
}
impl ChunkHeader {
pub const SIZE: usize = CHUNK_HEADER_SIZE;
pub fn to_bytes(&self) -> [u8; CHUNK_HEADER_SIZE] {
let mut buf = [0u8; CHUNK_HEADER_SIZE];
let mut pos = 0;
buf[pos..pos + 16].copy_from_slice(&self.message_id);
pos += 16;
buf[pos] = self.chunk_index;
pos += 1;
buf[pos] = self.total_chunks;
pos += 1;
buf[pos..pos + 8].copy_from_slice(&self.payload_length.to_be_bytes());
pos += 8;
buf[pos..pos + 16].copy_from_slice(&self.argon2_salt);
pos += 16;
buf[pos..pos + 12].copy_from_slice(&self.aes_nonce);
pos += 12;
buf[pos..pos + 32].copy_from_slice(&self.payload_hash);
pos += 32;
buf[pos] = self.bits_per_channel;
pos += 1;
buf[pos] = self.reserved;
buf
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < CHUNK_HEADER_SIZE {
return Err(StegoError::InvalidHeader);
}
let mut pos = 0;
let mut message_id = [0u8; 16];
message_id.copy_from_slice(&data[pos..pos + 16]);
pos += 16;
let chunk_index = data[pos];
pos += 1;
let total_chunks = data[pos];
pos += 1;
let payload_length = u64::from_be_bytes(
data[pos..pos + 8]
.try_into()
.map_err(|_| StegoError::InvalidHeader)?,
);
pos += 8;
let mut argon2_salt = [0u8; 16];
argon2_salt.copy_from_slice(&data[pos..pos + 16]);
pos += 16;
let mut aes_nonce = [0u8; 12];
aes_nonce.copy_from_slice(&data[pos..pos + 12]);
pos += 12;
let mut payload_hash = [0u8; 32];
payload_hash.copy_from_slice(&data[pos..pos + 32]);
pos += 32;
let bits_per_channel = data[pos];
pos += 1;
let reserved = data[pos];
if total_chunks == 0 {
return Err(StegoError::InvalidHeader);
}
if chunk_index >= total_chunks {
return Err(StegoError::InvalidHeader);
}
if bits_per_channel == 0 || bits_per_channel > 8 {
return Err(StegoError::InvalidHeader);
}
if reserved != 0 {
return Err(StegoError::InvalidHeader);
}
Ok(Self {
message_id,
chunk_index,
total_chunks,
payload_length,
argon2_salt,
aes_nonce,
payload_hash,
bits_per_channel,
reserved,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_header() -> ChunkHeader {
ChunkHeader {
message_id: [1u8; 16],
chunk_index: 0,
total_chunks: 1,
payload_length: 1234,
argon2_salt: [2u8; 16],
aes_nonce: [3u8; 12],
payload_hash: [4u8; 32],
bits_per_channel: 2,
reserved: 0,
}
}
#[test]
fn size_is_88() {
assert_eq!(ChunkHeader::SIZE, 88);
}
#[test]
fn roundtrip() {
let h = sample_header();
let bytes = h.to_bytes();
assert_eq!(bytes.len(), 88);
let h2 = ChunkHeader::from_bytes(&bytes).unwrap();
assert_eq!(h, h2);
}
#[test]
fn from_bytes_short_returns_invalid_header() {
let err = ChunkHeader::from_bytes(&[0u8; 87]).unwrap_err();
assert!(matches!(err, StegoError::InvalidHeader));
}
#[test]
fn from_bytes_88_zeroes_fails_validation() {
let err = ChunkHeader::from_bytes(&[0u8; 88]).unwrap_err();
assert!(matches!(err, StegoError::InvalidHeader));
}
#[test]
fn from_bytes_valid_88_bytes_succeeds() {
let h = sample_header();
let bytes = h.to_bytes();
assert!(ChunkHeader::from_bytes(&bytes).is_ok());
}
}