pub const SEED_SIZE: usize = 32;
pub const SALT_SIZE: usize = 16;
pub const ARGON2_MEMORY_COST: u32 = 65536;
pub const ARGON2_TIME_COST: u32 = 2;
pub const ARGON2_PARALLELISM: u32 = 1;
mod bit_index;
#[doc(hidden)]
pub mod crypto;
mod data;
mod header;
#[doc(hidden)]
pub mod utils;
pub use bit_index::BitIndex;
use crate::{error::PngerError, strategy::lsb::data::BodyEmbedder};
#[derive(Debug, Clone)]
pub struct LSBConfig {
bit_index: BitIndex,
pattern: EmbeddingPattern,
}
#[derive(Debug, Clone)]
pub enum EmbeddingPattern {
Linear,
Random(RandomConfig),
}
impl Default for EmbeddingPattern {
fn default() -> Self {
Self::Linear
}
}
#[derive(Debug, Clone)]
pub struct RandomConfig {
seed_source: SeedSource,
}
impl Default for RandomConfig {
fn default() -> Self {
Self {
seed_source: SeedSource::default(),
}
}
}
#[derive(Debug, Clone)]
pub enum SeedSource {
Auto,
Password(String),
Manual([u8; SEED_SIZE]),
}
impl Default for SeedSource {
fn default() -> Self {
Self::Auto
}
}
impl LSBConfig {
pub const fn linear() -> Self {
Self {
bit_index: BitIndex::LSB,
pattern: EmbeddingPattern::Linear,
}
}
pub const fn random() -> Self {
Self {
bit_index: BitIndex::LSB,
pattern: EmbeddingPattern::Random(RandomConfig {
seed_source: SeedSource::Auto,
}),
}
}
pub const fn random_with_manual_seed(seed: [u8; SEED_SIZE]) -> Self {
Self {
bit_index: BitIndex::LSB,
pattern: EmbeddingPattern::Random(RandomConfig {
seed_source: SeedSource::Manual(seed),
}),
}
}
pub fn with_bit_index(mut self, index: BitIndex) -> Self {
self.bit_index = index;
self
}
pub fn with_password(mut self, password: String) -> Self {
if let EmbeddingPattern::Random(ref mut config) = self.pattern {
config.seed_source = SeedSource::Password(password);
}
self
}
pub fn with_seed(mut self, seed: [u8; SEED_SIZE]) -> Self {
if let EmbeddingPattern::Random(ref mut config) = self.pattern {
config.seed_source = SeedSource::Manual(seed);
}
self
}
pub fn with_password_if_some(self, password: Option<String>) -> Self {
match password {
Some(pwd) => self.with_password(pwd),
None => self,
}
}
pub fn with_seed_if_some(self, seed: Option<[u8; SEED_SIZE]>) -> Self {
match seed {
Some(seed) => self.with_seed(seed),
None => self,
}
}
pub fn bit_index(&self) -> BitIndex {
self.bit_index
}
pub fn pattern(&self) -> &EmbeddingPattern {
&self.pattern
}
}
impl Default for LSBConfig {
fn default() -> Self {
Self::random()
}
}
#[derive(Debug, Clone)]
pub(crate) struct RuntimeConfig {
bit_index: BitIndex,
pattern: RuntimePattern,
}
#[derive(Debug, Clone)]
pub(crate) enum RuntimePattern {
Linear,
Random {
seed: [u8; SEED_SIZE],
embed_seed: bool,
},
}
impl RuntimeConfig {
fn from_config(config: &LSBConfig) -> Result<Self, PngerError> {
let pattern = match &config.pattern {
EmbeddingPattern::Linear => RuntimePattern::Linear,
EmbeddingPattern::Random(random_config) => {
let (seed, embed_seed) = match &random_config.seed_source {
SeedSource::Auto => {
let seed = crypto::CryptoContext::generate_random_seed()
.map_err(|e| PngerError::CryptoError(e.to_string()))?;
(seed, true)
}
SeedSource::Password(password) => {
let seed = crypto::CryptoContext::derive_seed_from_password(password)
.map_err(|e| PngerError::CryptoError(e.to_string()))?;
(seed, false)
}
SeedSource::Manual(seed) => (*seed, false),
};
RuntimePattern::Random { seed, embed_seed }
}
};
Ok(RuntimeConfig {
bit_index: config.bit_index,
pattern,
})
}
}
impl RuntimePattern {
fn from_header_and_config(
header: &header::CompleteHeader,
config: &LSBConfig,
) -> Result<Self, PngerError> {
if header
.fixed
.flags
.contains(header::HeaderFlags::RANDOM_PATTERN)
{
let seed = Self::reconstruct_seed(header, config)?;
let seed_was_embedded = header
.fixed
.flags
.contains(header::HeaderFlags::SEED_EMBEDDED);
Ok(RuntimePattern::Random {
seed,
embed_seed: seed_was_embedded,
})
} else {
Ok(RuntimePattern::Linear)
}
}
fn reconstruct_seed(
header: &header::CompleteHeader,
config: &LSBConfig,
) -> Result<[u8; SEED_SIZE], PngerError> {
let seed_was_embedded = header
.fixed
.flags
.contains(header::HeaderFlags::SEED_EMBEDDED);
if seed_was_embedded {
header.seed.ok_or_else(|| {
PngerError::InvalidFormat("Seed embedded flag set but no seed data".to_string())
})
} else {
match &config.pattern {
EmbeddingPattern::Random(random_config) => match &random_config.seed_source {
SeedSource::Password(password) => {
crypto::CryptoContext::derive_seed_from_password(password)
.map_err(|e| PngerError::CryptoError(e.to_string()))
}
SeedSource::Manual(seed) => Ok(*seed),
SeedSource::Auto => Err(PngerError::InvalidFormat(
"Auto seed source but no seed embedded".to_string(),
)),
},
EmbeddingPattern::Linear => Err(PngerError::InvalidFormat(
"Linear pattern expected but random pattern found".to_string(),
)),
}
}
}
}
pub struct LSBEmbedder;
#[derive(Debug, Clone)]
pub struct EmbedResult {
pub bytes_used: usize,
pub header_size: usize,
pub seed_embedded: bool,
}
#[derive(Debug, Clone)]
pub struct ExtractResult {
pub payload: Vec<u8>,
pub header_size: usize,
pub seed_was_embedded: bool,
}
impl LSBEmbedder {
pub fn embed(
image_data: &mut [u8],
payload: &[u8],
config: &LSBConfig,
) -> Result<EmbedResult, PngerError> {
let runtime_config = RuntimeConfig::from_config(config)?;
let header_size = header::HeaderEmbedder::required_size(&runtime_config);
let seed_embedded = matches!(
runtime_config.pattern,
RuntimePattern::Random {
embed_seed: true,
..
}
);
let (header_data, body_data) = image_data.split_at_mut(header_size);
let header_bytes_used = header::HeaderEmbedder::new(header_data, runtime_config.clone())
.embed(payload.len() as u32)?;
BodyEmbedder::new(body_data, &runtime_config.pattern, runtime_config.bit_index)
.embed_payload(payload);
Ok(EmbedResult {
bytes_used: header_bytes_used + (payload.len() * 8),
header_size,
seed_embedded,
})
}
pub fn extract(image_data: &mut [u8], config: &LSBConfig) -> Result<ExtractResult, PngerError> {
let fixed_header = header::FixedHeader::read_from_bytes(image_data)?;
let header_size = fixed_header.calculate_total_header_size();
let complete_header = header::CompleteHeader::read_from_bytes(&image_data[..header_size])?;
let seed_was_embedded = complete_header
.fixed
.flags
.contains(header::HeaderFlags::SEED_EMBEDDED);
let runtime_pattern = RuntimePattern::from_header_and_config(&complete_header, config)?;
let body_data = &mut image_data[header_size..];
let mut body_embedder = BodyEmbedder::new(body_data, &runtime_pattern, config.bit_index);
let payload = body_embedder.extract_payload(complete_header.fixed.payload_size as usize);
Ok(ExtractResult {
payload,
header_size,
seed_was_embedded,
})
}
pub fn embed_linear(image_data: &mut [u8], payload: &[u8]) -> Result<EmbedResult, PngerError> {
Self::embed(image_data, payload, &LSBConfig::linear())
}
pub fn embed_random(image_data: &mut [u8], payload: &[u8]) -> Result<EmbedResult, PngerError> {
Self::embed(image_data, payload, &LSBConfig::random())
}
pub fn embed_with_password(
image_data: &mut [u8],
payload: &[u8],
password: &str,
) -> Result<EmbedResult, PngerError> {
let config = LSBConfig::random().with_password(password.to_string());
Self::embed(image_data, payload, &config)
}
pub fn extract_linear(image_data: &mut [u8]) -> Result<ExtractResult, PngerError> {
Self::extract(image_data, &LSBConfig::linear())
}
pub fn extract_with_password(
image_data: &mut [u8],
password: &str,
) -> Result<ExtractResult, PngerError> {
let config = LSBConfig::random().with_password(password.to_string());
Self::extract(image_data, &config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lsb_config_builder_pattern() {
let config = LSBConfig::linear().with_bit_index(BitIndex::Bit2);
assert_eq!(config.bit_index(), BitIndex::Bit2);
assert!(matches!(config.pattern(), EmbeddingPattern::Linear));
let config = LSBConfig::random().with_password("test".to_string());
assert_eq!(config.bit_index(), BitIndex::LSB);
match config.pattern() {
EmbeddingPattern::Random(random_config) => {
assert!(matches!(random_config.seed_source, SeedSource::Password(_)));
}
_ => panic!("Expected Random pattern"),
}
}
#[test]
fn test_linear_embed_extract_roundtrip() {
let mut image_data = vec![0u8; 1000];
let payload = b"Hello, World!";
let result = LSBEmbedder::embed(&mut image_data, payload, &LSBConfig::linear());
assert!(result.is_ok());
let embed_result = result.unwrap();
assert!(!embed_result.seed_embedded);
assert!(embed_result.header_size > 0);
let result = LSBEmbedder::extract(&mut image_data, &LSBConfig::linear());
assert!(result.is_ok());
let extract_result = result.unwrap();
assert_eq!(extract_result.payload, payload);
assert!(!extract_result.seed_was_embedded);
}
#[test]
fn test_random_auto_seed_roundtrip() {
let mut image_data = vec![0u8; 1000];
let payload = b"Hello, World!";
let result = LSBEmbedder::embed(&mut image_data, payload, &LSBConfig::random());
assert!(result.is_ok());
let embed_result = result.unwrap();
assert!(embed_result.seed_embedded);
let result = LSBEmbedder::extract(&mut image_data, &LSBConfig::random());
assert!(result.is_ok());
let extract_result = result.unwrap();
assert_eq!(extract_result.payload, payload);
assert!(extract_result.seed_was_embedded);
}
#[test]
fn test_random_password_roundtrip() {
let mut image_data = vec![0u8; 1000];
let payload = b"Hello, World!";
let password = "test_password";
let config = LSBConfig::random().with_password(password.to_string());
let result = LSBEmbedder::embed(&mut image_data, payload, &config);
assert!(result.is_ok());
let embed_result = result.unwrap();
assert!(!embed_result.seed_embedded);
let config = LSBConfig::random().with_password(password.to_string());
let result = LSBEmbedder::extract(&mut image_data, &config);
assert!(result.is_ok());
let extract_result = result.unwrap();
assert_eq!(extract_result.payload, payload);
assert!(!extract_result.seed_was_embedded);
}
#[test]
fn test_convenience_methods() {
let mut image_data = vec![0u8; 1000];
let payload = b"Hello, World!";
let result = LSBEmbedder::embed_linear(&mut image_data, payload);
assert!(result.is_ok());
let result = LSBEmbedder::extract_linear(&mut image_data);
assert!(result.is_ok());
assert_eq!(result.unwrap().payload, payload);
let mut image_data2 = vec![0u8; 1000];
let result = LSBEmbedder::embed_with_password(&mut image_data2, payload, "test");
assert!(result.is_ok());
let result = LSBEmbedder::extract_with_password(&mut image_data2, "test");
assert!(result.is_ok());
assert_eq!(result.unwrap().payload, payload);
}
#[test]
fn test_config_defaults() {
let config = LSBConfig::default();
assert!(matches!(config.pattern(), EmbeddingPattern::Random(_)));
assert_eq!(config.bit_index(), BitIndex::LSB);
}
#[test]
fn test_conditional_setters() {
let config = LSBConfig::random().with_password_if_some(Some("test".to_string()));
match config.pattern() {
EmbeddingPattern::Random(random_config) => {
assert!(matches!(random_config.seed_source, SeedSource::Password(_)));
}
_ => panic!("Expected Random pattern"),
}
let config = LSBConfig::random().with_password_if_some(None);
match config.pattern() {
EmbeddingPattern::Random(random_config) => {
assert!(matches!(random_config.seed_source, SeedSource::Auto));
}
_ => panic!("Expected Random pattern"),
}
}
}