use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
use clap_complete::Shell;
#[derive(Parser, Debug)]
#[command(
name = "shadowforge",
version,
about = "Quantum-resistant steganography toolkit",
long_about = None,
propagate_version = true
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Version,
Keygen(KeygenArgs),
Embed(EmbedArgs),
Extract(ExtractArgs),
#[command(name = "embed-distributed")]
EmbedDistributed(EmbedDistributedArgs),
#[command(name = "extract-distributed")]
ExtractDistributed(ExtractDistributedArgs),
#[command(name = "analyse")]
Analyse(AnalyseArgs),
Archive(ArchiveArgs),
Scrub(ScrubArgs),
#[command(name = "dead-drop")]
DeadDrop(DeadDropArgs),
#[command(name = "time-lock")]
TimeLock(TimeLockArgs),
Watermark(WatermarkArgs),
Corpus(CorpusArgs),
#[command(hide = true)]
Panic(PanicArgs),
Completions(CompletionsArgs),
Cipher(CipherArgs),
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum Algorithm {
Kyber1024,
Dilithium3,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum Technique {
Lsb,
Dct,
Palette,
#[value(name = "lsb-audio")]
LsbAudio,
Phase,
Echo,
#[value(name = "zero-width")]
ZeroWidth,
#[value(name = "pdf-stream")]
PdfStream,
#[value(name = "pdf-meta")]
PdfMeta,
Corpus,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum Profile {
Standard,
Adaptive,
Survivable,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum Platform {
Instagram,
Twitter,
Whatsapp,
Telegram,
Imgur,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ArchiveFormat {
Zip,
Tar,
#[value(name = "tar-gz")]
TarGz,
}
#[derive(Parser, Debug)]
pub struct KeygenArgs {
#[command(subcommand)]
pub subcmd: Option<KeygenSubcommand>,
#[arg(long, value_enum)]
pub algorithm: Option<Algorithm>,
#[arg(long)]
pub output: Option<PathBuf>,
}
#[derive(Subcommand, Debug)]
pub enum KeygenSubcommand {
Sign {
#[arg(long)]
input: PathBuf,
#[arg(long)]
secret_key: PathBuf,
#[arg(long)]
output: PathBuf,
},
Verify {
#[arg(long)]
input: PathBuf,
#[arg(long)]
public_key: PathBuf,
#[arg(long)]
signature: PathBuf,
},
}
#[derive(Parser, Debug)]
pub struct EmbedArgs {
#[arg(long)]
pub input: PathBuf,
#[arg(long, required_unless_present = "amnesia", conflicts_with = "amnesia")]
pub cover: Option<PathBuf>,
#[arg(long, required_unless_present = "amnesia", conflicts_with = "amnesia")]
pub output: Option<PathBuf>,
#[arg(long, value_enum)]
pub technique: Technique,
#[arg(long, value_enum, default_value = "standard")]
pub profile: Profile,
#[arg(long, value_enum, required_if_eq("profile", "survivable"))]
pub platform: Option<Platform>,
#[arg(long)]
pub amnesia: bool,
#[arg(long)]
pub scrub_style: bool,
#[arg(long, conflicts_with = "amnesia")]
pub deniable: bool,
#[arg(long, required_if_eq("deniable", "true"))]
pub decoy_payload: Option<PathBuf>,
#[arg(long, requires = "deniable")]
pub decoy_key: Option<PathBuf>,
#[arg(long, requires = "deniable")]
pub key: Option<PathBuf>,
}
#[derive(Parser, Debug)]
pub struct ExtractArgs {
#[arg(long, required_unless_present = "amnesia", conflicts_with = "amnesia")]
pub input: Option<PathBuf>,
#[arg(long, required_unless_present = "amnesia", conflicts_with = "amnesia")]
pub output: Option<PathBuf>,
#[arg(long, value_enum)]
pub technique: Technique,
#[arg(long)]
pub key: Option<PathBuf>,
#[arg(long)]
pub amnesia: bool,
}
#[derive(Parser, Debug)]
pub struct EmbedDistributedArgs {
#[arg(long)]
pub input: PathBuf,
#[arg(long)]
pub covers: String,
#[arg(long, default_value = "3")]
pub data_shards: u8,
#[arg(long, default_value = "2")]
pub parity_shards: u8,
#[arg(long)]
pub output_archive: PathBuf,
#[arg(long, value_enum)]
pub technique: Technique,
#[arg(long, value_enum, default_value = "standard")]
pub profile: Profile,
#[arg(long, value_enum)]
pub platform: Option<Platform>,
#[arg(long)]
pub canary: bool,
#[arg(long)]
pub geo_manifest: Option<PathBuf>,
#[arg(long)]
pub hmac_key: Option<PathBuf>,
}
#[derive(Parser, Debug)]
pub struct ExtractDistributedArgs {
#[arg(long)]
pub input_archive: PathBuf,
#[arg(long)]
pub output: PathBuf,
#[arg(long, value_enum)]
pub technique: Technique,
#[arg(long, default_value = "3")]
pub data_shards: u8,
#[arg(long, default_value = "2")]
pub parity_shards: u8,
#[arg(long)]
pub hmac_key: Option<PathBuf>,
}
#[derive(Parser, Debug)]
pub struct AnalyseArgs {
#[arg(long)]
pub cover: PathBuf,
#[arg(long, value_enum)]
pub technique: Technique,
#[arg(long)]
pub json: bool,
}
#[derive(Parser, Debug)]
pub struct ArchiveArgs {
#[command(subcommand)]
pub subcmd: ArchiveSubcommand,
}
#[derive(Subcommand, Debug)]
pub enum ArchiveSubcommand {
Pack {
#[arg(long, num_args = 1..)]
files: Vec<PathBuf>,
#[arg(long, value_enum)]
format: ArchiveFormat,
#[arg(long)]
output: PathBuf,
},
Unpack {
#[arg(long)]
input: PathBuf,
#[arg(long, value_enum)]
format: ArchiveFormat,
#[arg(long)]
output_dir: PathBuf,
},
}
#[derive(Parser, Debug)]
pub struct ScrubArgs {
#[arg(long)]
pub input: PathBuf,
#[arg(long)]
pub output: PathBuf,
#[arg(long, default_value = "15")]
pub avg_sentence_len: u32,
#[arg(long, default_value = "1000")]
pub vocab_size: usize,
}
#[derive(Parser, Debug)]
pub struct DeadDropArgs {
#[arg(long)]
pub cover: PathBuf,
#[arg(long)]
pub input: PathBuf,
#[arg(long, value_enum)]
pub platform: Platform,
#[arg(long)]
pub output: PathBuf,
#[arg(long, value_enum, default_value = "lsb")]
pub technique: Technique,
}
#[derive(Parser, Debug)]
pub struct TimeLockArgs {
#[command(subcommand)]
pub subcmd: TimeLockSubcommand,
}
#[derive(Subcommand, Debug)]
pub enum TimeLockSubcommand {
Lock {
#[arg(long)]
input: PathBuf,
#[arg(long)]
unlock_at: String,
#[arg(long)]
output_puzzle: PathBuf,
},
Unlock {
#[arg(long)]
puzzle: PathBuf,
#[arg(long)]
output: PathBuf,
},
TryUnlock {
#[arg(long)]
puzzle: PathBuf,
},
}
#[derive(Parser, Debug)]
pub struct WatermarkArgs {
#[command(subcommand)]
pub subcmd: WatermarkSubcommand,
}
#[derive(Subcommand, Debug)]
pub enum WatermarkSubcommand {
#[command(name = "embed-tripwire")]
EmbedTripwire {
#[arg(long)]
cover: PathBuf,
#[arg(long)]
output: PathBuf,
#[arg(long)]
recipient_id: String,
},
Identify {
#[arg(long)]
cover: PathBuf,
#[arg(long)]
tags: PathBuf,
},
}
#[derive(Parser, Debug)]
pub struct CorpusArgs {
#[command(subcommand)]
pub subcmd: CorpusSubcommand,
}
#[derive(Subcommand, Debug)]
pub enum CorpusSubcommand {
Build {
#[arg(long)]
dir: PathBuf,
},
Search {
#[arg(long)]
input: PathBuf,
#[arg(long, value_enum)]
technique: Technique,
#[arg(long, default_value = "5")]
top: usize,
#[arg(long)]
model: Option<String>,
#[arg(long)]
resolution: Option<String>,
},
}
#[derive(Parser, Debug)]
pub struct PanicArgs {
#[arg(long, num_args = 0..)]
pub key_paths: Vec<String>,
}
#[derive(Parser, Debug)]
pub struct CompletionsArgs {
#[arg(value_enum)]
pub shell: Shell,
#[arg(short, long)]
pub output: Option<PathBuf>,
}
#[derive(Parser, Debug)]
pub struct CipherArgs {
#[command(subcommand)]
pub subcmd: CipherSubcommand,
}
#[derive(Subcommand, Debug)]
pub enum CipherSubcommand {
Encrypt {
#[arg(long)]
input: PathBuf,
#[arg(long)]
key: PathBuf,
#[arg(long)]
output: PathBuf,
},
Decrypt {
#[arg(long)]
input: PathBuf,
#[arg(long)]
key: PathBuf,
#[arg(long)]
output: PathBuf,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_parse_version() {
let cli = Cli::try_parse_from(["shadowforge", "version"]);
assert!(cli.is_ok());
}
#[test]
fn cli_parse_keygen() {
let cli = Cli::try_parse_from([
"shadowforge",
"keygen",
"--algorithm",
"kyber1024",
"--output",
"/tmp/keys",
]);
assert!(cli.is_ok());
}
#[test]
fn cli_parse_keygen_sign() {
let cli = Cli::try_parse_from([
"shadowforge",
"keygen",
"sign",
"--input",
"payload.bin",
"--secret-key",
"secret.key",
"--output",
"payload.sig",
]);
assert!(cli.is_ok());
}
#[test]
fn cli_parse_keygen_verify() {
let cli = Cli::try_parse_from([
"shadowforge",
"keygen",
"verify",
"--input",
"payload.bin",
"--public-key",
"public.key",
"--signature",
"payload.sig",
]);
assert!(cli.is_ok());
}
#[test]
fn cli_parse_embed() {
let cli = Cli::try_parse_from([
"shadowforge",
"embed",
"--input",
"payload.bin",
"--cover",
"cover.png",
"--output",
"stego.png",
"--technique",
"lsb",
]);
assert!(cli.is_ok());
}
#[test]
fn cli_parse_extract() {
let cli = Cli::try_parse_from([
"shadowforge",
"extract",
"--input",
"stego.png",
"--output",
"payload.bin",
"--technique",
"lsb",
]);
assert!(cli.is_ok());
}
#[test]
fn cli_parse_analyse_json() {
let cli = Cli::try_parse_from([
"shadowforge",
"analyse",
"--cover",
"cover.png",
"--technique",
"lsb",
"--json",
]);
assert!(cli.is_ok());
}
#[test]
fn cli_parse_scrub() {
let cli = Cli::try_parse_from([
"shadowforge",
"scrub",
"--input",
"text.txt",
"--output",
"clean.txt",
]);
assert!(cli.is_ok());
}
#[test]
fn cli_parse_time_lock_lock() {
let cli = Cli::try_parse_from([
"shadowforge",
"time-lock",
"lock",
"--input",
"secret.bin",
"--unlock-at",
"2025-12-31T00:00:00Z",
"--output-puzzle",
"puzzle.json",
]);
assert!(cli.is_ok());
}
#[test]
fn cli_parse_completions() {
let cli = Cli::try_parse_from(["shadowforge", "completions", "bash"]);
assert!(cli.is_ok());
}
#[test]
fn cli_parse_embed_distributed() {
let cli = Cli::try_parse_from([
"shadowforge",
"embed-distributed",
"--input",
"payload.bin",
"--covers",
"covers/*.png",
"--output-archive",
"dist.zip",
"--technique",
"lsb",
]);
assert!(cli.is_ok());
}
#[test]
fn cli_panic_hidden() {
let cli = Cli::try_parse_from(["shadowforge", "panic"]);
assert!(cli.is_ok());
}
#[test]
fn version_output_contains_semver() {
let version = env!("CARGO_PKG_VERSION");
assert!(version.contains('.'), "version should be semver");
}
#[test]
fn cli_parse_cipher_encrypt() {
let cli = Cli::try_parse_from([
"shadowforge",
"cipher",
"encrypt",
"--input",
"payload.bin",
"--key",
"key.bin",
"--output",
"out.enc",
]);
assert!(cli.is_ok());
}
#[test]
fn cli_parse_cipher_decrypt() {
let cli = Cli::try_parse_from([
"shadowforge",
"cipher",
"decrypt",
"--input",
"out.enc",
"--key",
"key.bin",
"--output",
"recovered.bin",
]);
assert!(cli.is_ok());
}
}