scryptenc-cli 0.8.1

An utility for encrypt and decrypt files
// SPDX-FileCopyrightText: 2023 Shun Sakai
//
// SPDX-License-Identifier: GPL-3.0-or-later

use std::path::Path;

use anyhow::{Context, bail};
use clap::Parser;
use scryptenc::{Decryptor, Error as ScryptencError, scrypt};

use crate::{
    cli::{Command, Opt},
    input, output, params, passphrase,
};

/// Ensures that there are no conflicts if reading the passphrase from standard
/// input.
fn ensure_stdin_does_not_conflict(path: &Path) -> anyhow::Result<()> {
    if path == Path::new("-") {
        bail!("cannot read both passphrase and input data from standard input");
    }
    Ok(())
}

/// Runs the program and returns the result.
#[allow(clippy::too_many_lines)]
pub fn run() -> anyhow::Result<()> {
    let opt = Opt::parse();

    match opt.command {
        Command::Encrypt(arg) => {
            let input = input::read(&arg.input)?;

            let passphrase = match (
                arg.passphrase_from_tty,
                arg.passphrase_from_stdin,
                arg.passphrase_from_tty_once,
                arg.passphrase_from_env,
                arg.passphrase_from_file,
            ) {
                (_, true, ..) => {
                    ensure_stdin_does_not_conflict(&arg.input)?;
                    passphrase::read_passphrase_from_stdin()
                }
                (_, _, true, ..) => passphrase::read_passphrase_from_tty_once(),
                (.., Some(env), _) => passphrase::read_passphrase_from_env(&env),
                (.., Some(file)) => passphrase::read_passphrase_from_file(&file),
                _ => passphrase::read_passphrase_from_tty(),
            }?;

            let params = if let (Some(log_n), Some(r), Some(p)) = (arg.log_n, arg.r, arg.p) {
                scrypt::Params::new(log_n, r, p, scrypt::Params::RECOMMENDED_LEN)
                    .expect("encryption parameters should be valid")
            } else {
                params::new(arg.max_memory, arg.max_memory_fraction, arg.max_time)
            };

            if arg.verbose {
                if arg.force {
                    params::displayln_without_resources(params.log_n(), params.r(), params.p());
                } else {
                    params::displayln_with_resources(
                        params.log_n(),
                        params.r(),
                        params.p(),
                        arg.max_memory,
                        arg.max_memory_fraction,
                        arg.max_time,
                    );
                }
            }

            if !arg.force {
                params::check(
                    arg.max_memory,
                    arg.max_memory_fraction,
                    arg.max_time,
                    params.log_n(),
                    params.r(),
                    params.p(),
                )?;
            }

            let ciphertext = scryptenc::encrypt_with_params(input, passphrase, params);

            if let Some(file) = arg.output {
                output::write_to_file(&file, &ciphertext)?;
            } else {
                output::write_to_stdout(&ciphertext)?;
            }
        }
        Command::Decrypt(arg) => {
            let input = input::read(&arg.input)?;

            let passphrase = match (
                arg.passphrase_from_tty,
                arg.passphrase_from_stdin,
                arg.passphrase_from_env,
                arg.passphrase_from_file,
            ) {
                (_, true, ..) => {
                    ensure_stdin_does_not_conflict(&arg.input)?;
                    passphrase::read_passphrase_from_stdin()
                }
                (.., Some(env), _) => passphrase::read_passphrase_from_env(&env),
                (.., Some(file)) => passphrase::read_passphrase_from_file(&file),
                _ => passphrase::read_passphrase_from_tty_once(),
            }?;

            let params = params::get(&input)?;
            if arg.verbose {
                if arg.force {
                    params::displayln_without_resources(params.log_n(), params.r(), params.p());
                } else {
                    params::displayln_with_resources(
                        params.log_n(),
                        params.r(),
                        params.p(),
                        arg.max_memory,
                        arg.max_memory_fraction,
                        arg.max_time,
                    );
                }
            }

            if !arg.force {
                params::check(
                    arg.max_memory,
                    arg.max_memory_fraction,
                    arg.max_time,
                    params.log_n(),
                    params.r(),
                    params.p(),
                )?;
            }

            let cipher = match Decryptor::new(&input, passphrase) {
                c @ Err(ScryptencError::InvalidHeaderMac(_)) => {
                    c.context("passphrase is incorrect")
                }
                c => c.context("the header in the encrypted data is invalid"),
            }?;
            let plaintext = cipher
                .decrypt_to_vec()
                .context("the encrypted data is corrupted")?;

            if let Some(file) = arg.output {
                output::write_to_file(&file, &plaintext)?;
            } else {
                output::write_to_stdout(&plaintext)?;
            }
        }
        Command::Information(arg) => {
            let input = input::read(&arg.input)?;

            let params = params::get(&input)?;
            #[cfg(feature = "json")]
            if arg.json {
                let params = params::Params::new(params);
                let output =
                    serde_json::to_string(&params).context("could not serialize as JSON")?;
                println!("{output}");
                return Ok(());
            }
            params::displayln_without_resources(params.log_n(), params.r(), params.p());
        }
        Command::Completion(arg) => {
            Opt::print_completion(arg.shell);
        }
    }
    Ok(())
}