use crate::crypto::{PrivateKey, PublicKey, encrypt_asymmetric, encrypt_symmetric, sign_message};
use crate::format::{EmbeddedData, Flags, Header, Payload};
use crate::stego::traits::{ChannelMode, EmbedOptions};
use crate::stego::{LsbSteganography, MetadataSteganography, StegoMethod, StegoMethodType};
use anyhow::{Result, anyhow};
use clap::Args;
use std::fs;
use std::path::PathBuf;
#[derive(Args)]
pub struct EncodeArgs {
pub input: PathBuf,
#[arg(short, long)]
pub output: PathBuf,
#[arg(long, conflicts_with = "message_file")]
pub message: Option<String>,
#[arg(long, conflicts_with = "message")]
pub message_file: Option<PathBuf>,
#[arg(long)]
pub audio: Option<PathBuf>,
#[arg(long, conflicts_with = "encrypt_to")]
pub passphrase: Option<String>,
#[arg(long = "encrypt-to", conflicts_with = "passphrase")]
pub encrypt_to: Vec<PathBuf>,
#[arg(long, requires = "key")]
pub sign: bool,
#[arg(long)]
pub key: Option<PathBuf>,
#[arg(long, value_enum, default_value = "lsb")]
pub method: StegoMethodType,
#[arg(long, default_value = "1")]
pub bits: u8,
#[arg(long, value_enum, default_value = "both")]
pub channels: ChannelMode,
}
pub fn run(args: EncodeArgs) -> Result<()> {
if !args.input.exists() {
return Err(anyhow!(
"Input file does not exist: {}",
args.input.display()
));
}
let text = if let Some(ref msg) = args.message {
Some(msg.clone())
} else if let Some(ref path) = args.message_file {
Some(fs::read_to_string(path)?)
} else {
None
};
let audio = if let Some(ref path) = args.audio {
Some(crate::audio::compress_audio(path)?)
} else {
None
};
if text.is_none() && audio.is_none() {
return Err(anyhow!(
"Nothing to embed. Use --message, --message-file, or --audio"
));
}
let payload = Payload { text, audio };
let mut payload_bytes = payload.to_bytes();
let mut flags = Flags {
has_text: payload.text.is_some(),
has_audio: payload.audio.is_some(),
..Default::default()
};
if let Some(ref passphrase) = args.passphrase {
payload_bytes = encrypt_symmetric(&payload_bytes, passphrase)?;
flags.symmetric_encryption = true;
} else if !args.encrypt_to.is_empty() {
let recipients: Vec<PublicKey> = args
.encrypt_to
.iter()
.map(|p| PublicKey::load(p))
.collect::<Result<Vec<_>>>()?;
payload_bytes = encrypt_asymmetric(&payload_bytes, &recipients)?;
flags.asymmetric_encryption = true;
}
let signature = if args.sign {
let key_path = args
.key
.as_ref()
.ok_or_else(|| anyhow!("--key is required for signing"))?;
let private_key = PrivateKey::load(key_path)?;
flags.is_signed = true;
Some(sign_message(&payload_bytes, &private_key))
} else {
None
};
let method_id = match args.method {
StegoMethodType::Lsb => crate::format::payload::StegoMethodId::Lsb,
StegoMethodType::Metadata => crate::format::payload::StegoMethodId::Metadata,
};
let header = Header {
flags,
method: method_id,
payload_length: payload_bytes.len() as u32,
};
let embedded = EmbeddedData {
header,
payload: payload_bytes,
signature,
};
let data_bytes = embedded.to_bytes();
let stego: Box<dyn StegoMethod> = match args.method {
StegoMethodType::Lsb => {
let options = EmbedOptions {
bits_per_sample: args.bits,
channels: args.channels,
};
Box::new(LsbSteganography::new(options))
}
StegoMethodType::Metadata => Box::new(MetadataSteganography::new()),
};
let capacity = stego.capacity(&args.input)?;
if data_bytes.len() > capacity {
return Err(anyhow!(
"Data too large: {} bytes needed, {} bytes available. Try using --method metadata or a longer audio file.",
data_bytes.len(),
capacity
));
}
stego.embed(&args.input, &args.output, &data_bytes)?;
let capacity_used = (data_bytes.len() as f64 / capacity as f64) * 100.0;
eprintln!(
"Embedded {} bytes into {} (capacity: {:.1}%)",
data_bytes.len(),
args.output.display(),
capacity_used
);
Ok(())
}