1use std::path::PathBuf;
4
5use clap::{Parser, Subcommand, ValueEnum};
6use clap_complete::Shell;
7
8#[derive(Parser, Debug)]
10#[command(
11 name = "shadowforge",
12 version,
13 about = "Quantum-resistant steganography toolkit",
14 long_about = None,
15 propagate_version = true
16)]
17pub struct Cli {
18 #[command(subcommand)]
20 pub command: Commands,
21}
22
23#[derive(Subcommand, Debug)]
25pub enum Commands {
26 Version,
28
29 Keygen(KeygenArgs),
31
32 Embed(EmbedArgs),
34
35 Extract(ExtractArgs),
37
38 #[command(name = "embed-distributed")]
40 EmbedDistributed(EmbedDistributedArgs),
41
42 #[command(name = "extract-distributed")]
44 ExtractDistributed(ExtractDistributedArgs),
45
46 #[command(name = "analyse")]
48 Analyse(AnalyseArgs),
49
50 Archive(ArchiveArgs),
52
53 Scrub(ScrubArgs),
55
56 #[command(name = "dead-drop")]
58 DeadDrop(DeadDropArgs),
59
60 #[command(name = "time-lock")]
62 TimeLock(TimeLockArgs),
63
64 Watermark(WatermarkArgs),
66
67 Corpus(CorpusArgs),
69
70 #[command(hide = true)]
72 Panic(PanicArgs),
73
74 Completions(CompletionsArgs),
76
77 Cipher(CipherArgs),
79}
80
81#[derive(Debug, Clone, Copy, ValueEnum)]
85pub enum Algorithm {
86 Kyber1024,
88 Dilithium3,
90}
91
92#[derive(Debug, Clone, Copy, ValueEnum)]
94pub enum Technique {
95 Lsb,
97 Dct,
99 Palette,
101 #[value(name = "lsb-audio")]
103 LsbAudio,
104 Phase,
106 Echo,
108 #[value(name = "zero-width")]
110 ZeroWidth,
111 #[value(name = "pdf-stream")]
113 PdfStream,
114 #[value(name = "pdf-meta")]
116 PdfMeta,
117 Corpus,
119}
120
121#[derive(Debug, Clone, Copy, ValueEnum)]
123pub enum Profile {
124 Standard,
126 Adaptive,
128 Survivable,
130}
131
132#[derive(Debug, Clone, Copy, ValueEnum)]
134pub enum Platform {
135 Instagram,
137 Twitter,
139 Whatsapp,
141 Telegram,
143 Imgur,
145}
146
147#[derive(Debug, Clone, Copy, ValueEnum)]
149pub enum ArchiveFormat {
150 Zip,
152 Tar,
154 #[value(name = "tar-gz")]
156 TarGz,
157}
158
159#[derive(Parser, Debug)]
163pub struct KeygenArgs {
164 #[command(subcommand)]
166 pub subcmd: Option<KeygenSubcommand>,
167 #[arg(long, value_enum)]
169 pub algorithm: Option<Algorithm>,
170 #[arg(long)]
172 pub output: Option<PathBuf>,
173}
174
175#[derive(Subcommand, Debug)]
177pub enum KeygenSubcommand {
178 Sign {
180 #[arg(long)]
182 input: PathBuf,
183 #[arg(long)]
185 secret_key: PathBuf,
186 #[arg(long)]
188 output: PathBuf,
189 },
190 Verify {
192 #[arg(long)]
194 input: PathBuf,
195 #[arg(long)]
197 public_key: PathBuf,
198 #[arg(long)]
200 signature: PathBuf,
201 },
202}
203
204#[derive(Parser, Debug)]
206pub struct EmbedArgs {
207 #[arg(long)]
209 pub input: PathBuf,
210 #[arg(long)]
212 pub cover: Option<PathBuf>,
213 #[arg(long)]
215 pub output: PathBuf,
216 #[arg(long, value_enum)]
218 pub technique: Technique,
219 #[arg(long, value_enum, default_value = "standard")]
221 pub profile: Profile,
222 #[arg(long, value_enum)]
224 pub platform: Option<Platform>,
225 #[arg(long)]
227 pub amnesia: bool,
228 #[arg(long)]
230 pub scrub_style: bool,
231 #[arg(long)]
233 pub deniable: bool,
234 #[arg(long)]
236 pub decoy_payload: Option<PathBuf>,
237 #[arg(long)]
239 pub decoy_key: Option<PathBuf>,
240 #[arg(long)]
242 pub key: Option<PathBuf>,
243}
244
245#[derive(Parser, Debug)]
247pub struct ExtractArgs {
248 #[arg(long)]
250 pub input: PathBuf,
251 #[arg(long)]
253 pub output: PathBuf,
254 #[arg(long, value_enum)]
256 pub technique: Technique,
257 #[arg(long)]
259 pub key: Option<PathBuf>,
260 #[arg(long)]
262 pub amnesia: bool,
263}
264
265#[derive(Parser, Debug)]
267pub struct EmbedDistributedArgs {
268 #[arg(long)]
270 pub input: PathBuf,
271 #[arg(long)]
273 pub covers: String,
274 #[arg(long, default_value = "3")]
276 pub data_shards: u8,
277 #[arg(long, default_value = "2")]
279 pub parity_shards: u8,
280 #[arg(long)]
282 pub output_archive: PathBuf,
283 #[arg(long, value_enum)]
285 pub technique: Technique,
286 #[arg(long, value_enum, default_value = "standard")]
288 pub profile: Profile,
289 #[arg(long, value_enum)]
291 pub platform: Option<Platform>,
292 #[arg(long)]
294 pub canary: bool,
295 #[arg(long)]
297 pub geo_manifest: Option<PathBuf>,
298 #[arg(long)]
301 pub hmac_key: Option<PathBuf>,
302}
303
304#[derive(Parser, Debug)]
306pub struct ExtractDistributedArgs {
307 #[arg(long)]
309 pub input_archive: PathBuf,
310 #[arg(long)]
312 pub output: PathBuf,
313 #[arg(long, value_enum)]
315 pub technique: Technique,
316 #[arg(long, default_value = "3")]
318 pub data_shards: u8,
319 #[arg(long, default_value = "2")]
321 pub parity_shards: u8,
322 #[arg(long)]
324 pub hmac_key: Option<PathBuf>,
325}
326
327#[derive(Parser, Debug)]
329pub struct AnalyseArgs {
330 #[arg(long)]
332 pub cover: PathBuf,
333 #[arg(long, value_enum)]
335 pub technique: Technique,
336 #[arg(long)]
338 pub json: bool,
339}
340
341#[derive(Parser, Debug)]
343pub struct ArchiveArgs {
344 #[command(subcommand)]
346 pub subcmd: ArchiveSubcommand,
347}
348
349#[derive(Subcommand, Debug)]
351pub enum ArchiveSubcommand {
352 Pack {
354 #[arg(long, num_args = 1..)]
356 files: Vec<PathBuf>,
357 #[arg(long, value_enum)]
359 format: ArchiveFormat,
360 #[arg(long)]
362 output: PathBuf,
363 },
364 Unpack {
366 #[arg(long)]
368 input: PathBuf,
369 #[arg(long, value_enum)]
371 format: ArchiveFormat,
372 #[arg(long)]
374 output_dir: PathBuf,
375 },
376}
377
378#[derive(Parser, Debug)]
380pub struct ScrubArgs {
381 #[arg(long)]
383 pub input: PathBuf,
384 #[arg(long)]
386 pub output: PathBuf,
387 #[arg(long, default_value = "15")]
389 pub avg_sentence_len: u32,
390 #[arg(long, default_value = "1000")]
392 pub vocab_size: usize,
393}
394
395#[derive(Parser, Debug)]
397pub struct DeadDropArgs {
398 #[arg(long)]
400 pub cover: PathBuf,
401 #[arg(long)]
403 pub input: PathBuf,
404 #[arg(long, value_enum)]
406 pub platform: Platform,
407 #[arg(long)]
409 pub output: PathBuf,
410 #[arg(long, value_enum, default_value = "lsb")]
412 pub technique: Technique,
413}
414
415#[derive(Parser, Debug)]
417pub struct TimeLockArgs {
418 #[command(subcommand)]
420 pub subcmd: TimeLockSubcommand,
421}
422
423#[derive(Subcommand, Debug)]
425pub enum TimeLockSubcommand {
426 Lock {
428 #[arg(long)]
430 input: PathBuf,
431 #[arg(long)]
433 unlock_at: String,
434 #[arg(long)]
436 output_puzzle: PathBuf,
437 },
438 Unlock {
440 #[arg(long)]
442 puzzle: PathBuf,
443 #[arg(long)]
445 output: PathBuf,
446 },
447 TryUnlock {
449 #[arg(long)]
451 puzzle: PathBuf,
452 },
453}
454
455#[derive(Parser, Debug)]
457pub struct WatermarkArgs {
458 #[command(subcommand)]
460 pub subcmd: WatermarkSubcommand,
461}
462
463#[derive(Subcommand, Debug)]
465pub enum WatermarkSubcommand {
466 #[command(name = "embed-tripwire")]
468 EmbedTripwire {
469 #[arg(long)]
471 cover: PathBuf,
472 #[arg(long)]
474 output: PathBuf,
475 #[arg(long)]
477 recipient_id: String,
478 },
479 Identify {
481 #[arg(long)]
483 cover: PathBuf,
484 #[arg(long)]
486 tags: PathBuf,
487 },
488}
489
490#[derive(Parser, Debug)]
492pub struct CorpusArgs {
493 #[command(subcommand)]
495 pub subcmd: CorpusSubcommand,
496}
497
498#[derive(Subcommand, Debug)]
500pub enum CorpusSubcommand {
501 Build {
503 #[arg(long)]
505 dir: PathBuf,
506 },
507 Search {
509 #[arg(long)]
511 input: PathBuf,
512 #[arg(long, value_enum)]
514 technique: Technique,
515 #[arg(long, default_value = "5")]
517 top: usize,
518 #[arg(long)]
520 model: Option<String>,
521 #[arg(long)]
524 resolution: Option<String>,
525 },
526}
527
528#[derive(Parser, Debug)]
530pub struct PanicArgs {
531 #[arg(long, num_args = 0..)]
533 pub key_paths: Vec<String>,
534}
535
536#[derive(Parser, Debug)]
538pub struct CompletionsArgs {
539 #[arg(value_enum)]
541 pub shell: Shell,
542
543 #[arg(short, long)]
545 pub output: Option<PathBuf>,
546}
547
548#[derive(Parser, Debug)]
550pub struct CipherArgs {
551 #[command(subcommand)]
553 pub subcmd: CipherSubcommand,
554}
555
556#[derive(Subcommand, Debug)]
558pub enum CipherSubcommand {
559 Encrypt {
562 #[arg(long)]
564 input: PathBuf,
565 #[arg(long)]
567 key: PathBuf,
568 #[arg(long)]
570 output: PathBuf,
571 },
572 Decrypt {
575 #[arg(long)]
577 input: PathBuf,
578 #[arg(long)]
580 key: PathBuf,
581 #[arg(long)]
583 output: PathBuf,
584 },
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590
591 #[test]
592 fn cli_parse_version() {
593 let cli = Cli::try_parse_from(["shadowforge", "version"]);
594 assert!(cli.is_ok());
595 }
596
597 #[test]
598 fn cli_parse_keygen() {
599 let cli = Cli::try_parse_from([
600 "shadowforge",
601 "keygen",
602 "--algorithm",
603 "kyber1024",
604 "--output",
605 "/tmp/keys",
606 ]);
607 assert!(cli.is_ok());
608 }
609
610 #[test]
611 fn cli_parse_keygen_sign() {
612 let cli = Cli::try_parse_from([
613 "shadowforge",
614 "keygen",
615 "sign",
616 "--input",
617 "payload.bin",
618 "--secret-key",
619 "secret.key",
620 "--output",
621 "payload.sig",
622 ]);
623 assert!(cli.is_ok());
624 }
625
626 #[test]
627 fn cli_parse_keygen_verify() {
628 let cli = Cli::try_parse_from([
629 "shadowforge",
630 "keygen",
631 "verify",
632 "--input",
633 "payload.bin",
634 "--public-key",
635 "public.key",
636 "--signature",
637 "payload.sig",
638 ]);
639 assert!(cli.is_ok());
640 }
641
642 #[test]
643 fn cli_parse_embed() {
644 let cli = Cli::try_parse_from([
645 "shadowforge",
646 "embed",
647 "--input",
648 "payload.bin",
649 "--cover",
650 "cover.png",
651 "--output",
652 "stego.png",
653 "--technique",
654 "lsb",
655 ]);
656 assert!(cli.is_ok());
657 }
658
659 #[test]
660 fn cli_parse_extract() {
661 let cli = Cli::try_parse_from([
662 "shadowforge",
663 "extract",
664 "--input",
665 "stego.png",
666 "--output",
667 "payload.bin",
668 "--technique",
669 "lsb",
670 ]);
671 assert!(cli.is_ok());
672 }
673
674 #[test]
675 fn cli_parse_analyse_json() {
676 let cli = Cli::try_parse_from([
677 "shadowforge",
678 "analyse",
679 "--cover",
680 "cover.png",
681 "--technique",
682 "lsb",
683 "--json",
684 ]);
685 assert!(cli.is_ok());
686 }
687
688 #[test]
689 fn cli_parse_scrub() {
690 let cli = Cli::try_parse_from([
691 "shadowforge",
692 "scrub",
693 "--input",
694 "text.txt",
695 "--output",
696 "clean.txt",
697 ]);
698 assert!(cli.is_ok());
699 }
700
701 #[test]
702 fn cli_parse_time_lock_lock() {
703 let cli = Cli::try_parse_from([
704 "shadowforge",
705 "time-lock",
706 "lock",
707 "--input",
708 "secret.bin",
709 "--unlock-at",
710 "2025-12-31T00:00:00Z",
711 "--output-puzzle",
712 "puzzle.json",
713 ]);
714 assert!(cli.is_ok());
715 }
716
717 #[test]
718 fn cli_parse_completions() {
719 let cli = Cli::try_parse_from(["shadowforge", "completions", "bash"]);
720 assert!(cli.is_ok());
721 }
722
723 #[test]
724 fn cli_parse_embed_distributed() {
725 let cli = Cli::try_parse_from([
726 "shadowforge",
727 "embed-distributed",
728 "--input",
729 "payload.bin",
730 "--covers",
731 "covers/*.png",
732 "--output-archive",
733 "dist.zip",
734 "--technique",
735 "lsb",
736 ]);
737 assert!(cli.is_ok());
738 }
739
740 #[test]
741 fn cli_panic_hidden() {
742 let cli = Cli::try_parse_from(["shadowforge", "panic"]);
744 assert!(cli.is_ok());
745 }
746
747 #[test]
748 fn version_output_contains_semver() {
749 let version = env!("CARGO_PKG_VERSION");
750 assert!(version.contains('.'), "version should be semver");
751 }
752
753 #[test]
754 fn cli_parse_cipher_encrypt() {
755 let cli = Cli::try_parse_from([
756 "shadowforge",
757 "cipher",
758 "encrypt",
759 "--input",
760 "payload.bin",
761 "--key",
762 "key.bin",
763 "--output",
764 "out.enc",
765 ]);
766 assert!(cli.is_ok());
767 }
768
769 #[test]
770 fn cli_parse_cipher_decrypt() {
771 let cli = Cli::try_parse_from([
772 "shadowforge",
773 "cipher",
774 "decrypt",
775 "--input",
776 "out.enc",
777 "--key",
778 "key.bin",
779 "--output",
780 "recovered.bin",
781 ]);
782 assert!(cli.is_ok());
783 }
784}