vault-cli 0.1.3

A cli tool for password based encryption/decryption
use std::error::Error;

use cmd_args::{arg, option, parser, Group};
use zeroize::Zeroize;

#[derive(Copy, Clone)]
enum Mode {
    Open,
    Close,
}

fn main() -> Result<(), Box<dyn Error>> {
    let group = Group::new(
        Box::new(|_args, _options| {
            println!("Check vault --help for usage information");
        }),
        "Vault CLI",
    )
    .add_option(
        option::Descriptor::new(
            "retain-source",
            option::Type::Bool { default: false },
            "Whether to retain the source file(s) after encryption/decryption",
        )
        .add_alias("r"),
    )
    .add_option(
        option::Descriptor::new(
            "password",
            option::Type::Str {
                default: String::new(),
            },
            "The password to use for encryption/decryption",
        )
        .add_alias("p"),
    )
    .add_child(
        "open",
        Some(vec!["o"]),
        Group::new(
            Box::new(|args, options| {
                let retain_source = options.get("retain-source").unwrap().bool().unwrap();
                let source = args.get(0).unwrap().str().unwrap();
                let password = options
                    .get("password")
                    .unwrap()
                    .str()
                    .filter(|s| !s.is_empty())
                    .map(|s| s.to_string());

                if let Err(e) = run(Mode::Open, source, retain_source, password) {
                    println!("Failed to decrypt: {}", e);
                }
            }),
            "Open the vault (Decrypting file(s))",
        )
        .add_argument(arg::Descriptor::new(
            arg::Type::Str,
            "A file or a directory to decrypt all files recursively in",
        )),
    )
    .add_child(
        "close",
        Some(vec!["c"]),
        Group::new(
            Box::new(|args, options| {
                let retain_source = options.get("retain-source").unwrap().bool().unwrap();
                let source = args.get(0).unwrap().str().unwrap();
                let password = options
                    .get("password")
                    .unwrap()
                    .str()
                    .filter(|s| !s.is_empty())
                    .map(|s| s.to_string());

                if let Err(e) = run(Mode::Close, source, retain_source, password) {
                    println!("Failed to encrypt: {}", e);
                }
            }),
            "Close the vault (Encrypting file(s))",
        )
        .add_argument(arg::Descriptor::new(
            arg::Type::Str,
            "A file or a directory to encrypt all files recursively in",
        )),
    );

    parser::parse(group, None)?;

    Ok(())
}

fn run(
    mode: Mode,
    source: &str,
    retain_source: bool,
    password: Option<String>,
) -> Result<(), Box<dyn Error>> {
    let mut password = password
        .map(|p| Ok(p))
        .unwrap_or_else(|| rpassword::prompt_password("Password: "))?;

    let meta_data = std::fs::metadata(source)?;
    if meta_data.is_file() {
        run_for_file(mode, source, &password, retain_source)?;
    } else {
        run_for_directory(mode, source, &password, retain_source)?;
    }

    password.zeroize();

    Ok(())
}

fn run_for_directory(
    mode: Mode,
    source: &str,
    password: &str,
    retain_source: bool,
) -> Result<(), Box<dyn Error>> {
    let entries = std::fs::read_dir(source)?;
    for entry in entries {
        let entry = entry?;
        let path = entry.path();
        let path_str = path.to_str().unwrap();

        let meta_data = entry.metadata()?;
        if meta_data.is_file() {
            run_for_file(mode, path_str, &password, retain_source)?;
        } else {
            run_for_directory(mode, path_str, &password, retain_source)?;
        }
    }

    Ok(())
}

fn run_for_file(
    mode: Mode,
    source: &str,
    password: &str,
    retain_source: bool,
) -> Result<(), Box<dyn Error>> {
    let target_path = match mode {
        Mode::Open => source.strip_suffix(".closed").unwrap_or(source).to_string(),
        Mode::Close => format!("{}.closed", source),
    };

    if let Err(e) = match mode {
        Mode::Open => vault_core::decrypt_file(source, &target_path, &password),
        Mode::Close => vault_core::encrypt_file(source, &target_path, &password),
    } {
        std::fs::remove_file(target_path)?;
        return Err(e);
    }

    let delete_source = !retain_source;
    if delete_source {
        std::fs::remove_file(source)?;
    }

    Ok(())
}