pub mod lsb;
use anyhow::bail;
use clap::{Parser, ValueEnum};
use pnger::{
EmbeddingOptions, Obfuscation,
strategy::{Strategy, lsb::SEED_SIZE},
};
use std::path::PathBuf;
use lsb::LSBPatternArg;
const PNGER_DEFAULT_XOR_KEY: &str = "PNGER_DEFAULT_XOR_KEY";
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum StrategyArg {
Lsb,
}
#[derive(Parser)]
#[command(name = "pnger")]
#[command(version = "0.1.0")]
#[command(about = "A cross-platform tool for embedding & extracting payloads within PNG files")]
#[command(after_help = "Examples:
# Embed payload.json into image.png and save to output.png
pnger -i image.png -p payload.json -o output.png
# Use explicit LSB strategy
pnger -i image.png -p payload.bin -o output.png --strategy lsb
# Output raw binary data to stdout
pnger -i image.png -p payload.txt --raw > output.png
# LSB with password-based pattern (secure, nothing embedded)
pnger -i image.png -p secret.txt -o output.png --lsb-password \"mypassword123\"
# LSB with manual hex seed (32 bytes = 64 hex chars)
pnger -i image.png -p data.bin -o output.png --lsb-seed \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"
# LSB linear pattern instead of random
pnger -i image.png -p data.txt -o output.png --lsb-pattern linear
# LSB with custom bit index (target bit 3 instead of 0)
pnger -i image.png -p secret.bin -o output.png --lsb-bit-index 3
# XOR obfuscation with default key
pnger -i image.png -p sensitive.txt -o output.png --xor
# XOR obfuscation with custom key
pnger -i image.png -p data.json -o output.png --xor --xor-key \"mykey123\"
# Combined: LSB password + XOR
pnger -i image.png -p payload.bin -o output.png --lsb-password \"mypassword\" --xor --xor-key \"encrypt\"
# Extract payload from image.png and save to payload.json
pnger -x -i output.png -o payload.json
# Extract with matching LSB and XOR parameters
pnger -x -i output.png -o extracted.txt --lsb-password \"mypassword\" --xor --xor-key \"encrypt\"
# Extract payload to stdout
pnger -x -i output.png --raw")]
pub struct Cli {
#[arg(short, long, value_name = "FILE")]
pub input: PathBuf,
#[arg(short, long, value_name = "FILE")]
pub payload: Option<PathBuf>,
#[arg(short, long, value_name = "FILE", conflicts_with = "raw")]
pub output: Option<PathBuf>,
#[arg(long, conflicts_with = "output")]
pub raw: bool,
#[arg(short, long, value_enum, default_value_t = StrategyArg::Lsb)]
pub strategy: StrategyArg,
#[arg(short = 'x', long)]
pub extract: bool,
#[arg(long)]
pub xor: bool,
#[arg(long)]
pub xor_key: Option<String>,
#[arg(long, value_enum)]
pub lsb_pattern: Option<LSBPatternArg>,
#[arg(long, value_parser = clap::value_parser!(u8).range(0..=7))]
pub lsb_bit_index: Option<u8>,
#[arg(long)]
pub lsb_password: Option<String>,
#[arg(long)]
pub lsb_seed: Option<String>,
}
impl Cli {
pub fn parse_and_validate() -> anyhow::Result<Self> {
let cli = Self::parse();
cli.validate().map(|_| cli)
}
fn get_strategy(&self) -> anyhow::Result<Strategy> {
match self.strategy {
StrategyArg::Lsb => {
let pattern = self.lsb_pattern.unwrap_or(LSBPatternArg::Random);
let seed = if let Some(seed_hex) = &self.lsb_seed {
let seed_bytes = hex::decode(seed_hex)
.map_err(|e| anyhow::anyhow!("Invalid hex seed: {}", e))?;
if seed_bytes.len() != SEED_SIZE {
bail!(
"Seed must be exactly {} bytes ({} hex characters), got {}",
SEED_SIZE,
SEED_SIZE * 2,
seed_bytes.len()
);
}
Some(seed_bytes)
} else {
None
};
let lsb_config = pattern
.to_lsb_config(self.lsb_password.clone(), seed, self.lsb_bit_index)
.map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(Strategy::LSB(lsb_config))
}
}
}
fn get_obfuscation(&self) -> Option<Obfuscation> {
if self.xor {
Some(Obfuscation::Xor {
key: self
.xor_key
.clone()
.unwrap_or(PNGER_DEFAULT_XOR_KEY.to_owned())
.into_bytes(),
})
} else {
None
}
}
pub fn get_options(&self) -> Result<EmbeddingOptions, anyhow::Error> {
let strategy = self.get_strategy()?;
let mut options = EmbeddingOptions::new(strategy);
if let Some(obfuscation) = self.get_obfuscation() {
options.set_obfuscation(Some(obfuscation));
}
Ok(options)
}
fn validate(&self) -> anyhow::Result<()> {
if self.output.is_none() && !self.raw {
bail!(
"Error: must specify either --output <FILE> or --raw for output method.\nUse --help for more information."
);
}
if !self.extract && self.payload.is_none() {
bail!("Error: a payload file has to be specified with --payload")
}
Ok(())
}
}