Skip to main content

shadow_crypt_shell/encryption/
cli.rs

1use std::path::PathBuf;
2
3use clap::{Parser, ValueEnum};
4use shadow_crypt_core::profile::SecurityProfile;
5
6use crate::errors::{WorkflowError, WorkflowResult};
7
8/// The security profile as selected on the command line. Run with
9/// `--profiles` for each profile's key derivation parameters.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
11pub enum CliProfile {
12    /// OWASP-recommended key derivation (the default)
13    #[default]
14    Standard,
15    /// Maximum-cost key derivation; needs 1 GiB of free RAM per file
16    Paranoid,
17    /// For automated testing only — insecure, skips password strength checks
18    #[value(hide = true)]
19    Test,
20}
21
22impl From<CliProfile> for SecurityProfile {
23    fn from(profile: CliProfile) -> Self {
24        match profile {
25            CliProfile::Standard => SecurityProfile::Standard,
26            CliProfile::Paranoid => SecurityProfile::Paranoid,
27            CliProfile::Test => SecurityProfile::Test,
28        }
29    }
30}
31
32/// Encryption CLI arguments structure
33#[derive(Debug, Clone, Default, Parser)]
34#[command(
35    name = "shadow",
36    about = "Encrypt files using shadow format",
37    version,
38    after_help = "A directory input becomes a single encrypted archive that hides the file \
39                  count, names, and sizes inside it.\n\n\
40                  Run with --profiles to see each security profile's key derivation \
41                  parameters.\n\n\
42                  Exit codes: 0 success; 1 operation failed; 2 invalid usage or input; \
43                  3 authentication failure (wrong password or corrupted file)."
44)]
45pub struct EncryptionCliArgs {
46    /// Input files or directories to encrypt
47    #[arg(value_name = "PATH")]
48    pub input_files: Vec<String>,
49
50    /// Security profile controlling the key derivation cost
51    #[arg(long = "profile", value_enum, default_value_t = CliProfile::Standard)]
52    pub profile: CliProfile,
53
54    /// Print the available security profiles and their parameters, then exit
55    #[arg(long = "profiles")]
56    pub list_profiles: bool,
57
58    /// Write encrypted files to this directory (created if missing; defaults to the current directory)
59    #[arg(long = "output-dir", short = 'o', value_name = "DIR")]
60    pub output_dir: Option<PathBuf>,
61
62    /// Read the password from this file instead of prompting (one trailing newline is ignored)
63    #[arg(long = "password-file", value_name = "FILE")]
64    pub password_file: Option<PathBuf>,
65
66    /// Suppress progress and per-file success output (errors are still shown)
67    #[arg(long = "quiet", short = 'q')]
68    pub quiet: bool,
69
70    /// Delete originals after successful encryption. Best-effort removal:
71    /// on SSDs and journaling filesystems the data may remain recoverable
72    /// until overwritten.
73    #[arg(long = "delete")]
74    pub delete: bool,
75}
76
77/// Parse encryption command line arguments
78pub fn get_cli_args(args: Vec<String>) -> WorkflowResult<EncryptionCliArgs> {
79    let cli_args = EncryptionCliArgs::try_parse_from(args).map_err(|e| {
80        // If it's help or version, it's not a user input error
81        if e.kind() == clap::error::ErrorKind::DisplayHelp
82            || e.kind() == clap::error::ErrorKind::DisplayVersion
83        {
84            // Print the message and exit successfully
85            eprintln!("{}", e);
86            std::process::exit(0);
87        }
88        WorkflowError::UserInput(e.to_string())
89    })?;
90
91    if !cli_args.list_profiles && cli_args.input_files.is_empty() {
92        return Err(WorkflowError::UserInput(
93            "No input files provided".to_string(),
94        ));
95    }
96
97    Ok(cli_args)
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_profile_defaults_to_standard() {
106        let args = get_cli_args(vec!["shadow".to_string(), "file1.txt".to_string()]).unwrap();
107        assert_eq!(args.profile, CliProfile::Standard);
108        assert_eq!(
109            SecurityProfile::from(args.profile),
110            SecurityProfile::Standard
111        );
112    }
113
114    #[test]
115    fn test_profile_parses_all_levels() {
116        for (name, expected) in [
117            ("standard", SecurityProfile::Standard),
118            ("paranoid", SecurityProfile::Paranoid),
119            ("test", SecurityProfile::Test),
120        ] {
121            let args = get_cli_args(vec![
122                "shadow".to_string(),
123                "--profile".to_string(),
124                name.to_string(),
125                "file1.txt".to_string(),
126            ])
127            .unwrap();
128            assert_eq!(SecurityProfile::from(args.profile), expected);
129        }
130    }
131
132    #[test]
133    fn test_parse_cli_args_with_files() {
134        let args = vec![
135            "shadow".to_string(),
136            "file1.txt".to_string(),
137            "file2.txt".to_string(),
138        ];
139        let cli_args = get_cli_args(args).unwrap();
140        assert_eq!(
141            cli_args.input_files,
142            vec!["file1.txt".to_string(), "file2.txt".to_string()]
143        );
144    }
145
146    #[test]
147    fn test_parse_cli_args_no_files() {
148        let args = vec!["shadow".to_string()];
149        let result = get_cli_args(args);
150        assert!(result.is_err());
151        if let Err(WorkflowError::UserInput(msg)) = result {
152            assert_eq!(msg, "No input files provided");
153        } else {
154            panic!("Expected UserInput error");
155        }
156    }
157
158    #[test]
159    fn test_profiles_flag_needs_no_input_files() {
160        let args = get_cli_args(vec!["shadow".to_string(), "--profiles".to_string()]).unwrap();
161        assert!(args.list_profiles);
162        assert!(args.input_files.is_empty());
163    }
164}