rsign2 0.6.6

A command-line tool to sign files and verify signatures.
#[macro_use]
extern crate clap;

mod helpers;
mod parse_args;

use std::io::Write;
use std::path::{Path, PathBuf};

#[cfg(any(windows, unix))]
use dirs::home_dir;
use minisign::*;

use crate::helpers::{
    create_dir, create_file, create_sig_file, is_printable, open_data_file, unix_timestamp,
};
use crate::parse_args::*;

#[cfg(not(any(windows, unix)))]
fn home_dir() -> Option<PathBuf> {
    Some(PathBuf::from("."))
}

pub fn cmd_generate<P, Q>(
    force: bool,
    pk_path: P,
    sk_path: Q,
    comment: Option<&str>,
    passwordless: bool,
    unencrypted: bool,
) -> Result<KeyPair>
where
    P: AsRef<Path>,
    Q: AsRef<Path>,
{
    let pk_path = pk_path.as_ref();
    let sk_path = sk_path.as_ref();
    if pk_path.exists() {
        if !force {
            return Err(PError::new(
                ErrorKind::Io,
                format!(
                    "Key generation aborted:\n
{} already exists\n
If you really want to overwrite the existing key pair, add the -f switch to\n
force this operation.",
                    pk_path.display()
                ),
            ));
        } else {
            std::fs::remove_file(pk_path)?;
        }
    }
    let mut pk_writer = create_file(pk_path, 0o644)?;
    let mut sk_writer = create_file(sk_path, 0o600)?;
    let kp = if unencrypted {
        let kp = KeyPair::generate_unencrypted_keypair()?;
        pk_writer.write_all(&kp.pk.to_box()?.to_bytes())?;
        sk_writer.write_all(&kp.sk.to_box(comment)?.to_bytes())?;
        kp
    } else {
        KeyPair::generate_and_write_encrypted_keypair(
            &mut pk_writer,
            &mut sk_writer,
            comment,
            if passwordless {
                Some(Default::default())
            } else {
                None
            },
        )?
    };
    pk_writer.flush()?;
    sk_writer.flush()?;
    Ok(kp)
}

pub fn cmd_sign<P, Q, R>(
    pk: Option<PublicKey>,
    sk_path: P,
    signature_path: Q,
    data_path: R,
    trusted_comment: Option<&str>,
    untrusted_comment: Option<&str>,
    passwordless: bool,
) -> Result<()>
where
    P: AsRef<Path>,
    Q: AsRef<Path>,
    R: AsRef<Path>,
{
    if !sk_path.as_ref().exists() {
        return Err(PError::new(
            ErrorKind::Io,
            format!(
                "can't find secret key file at {}, try using -s",
                sk_path.as_ref().display()
            ),
        ));
    }
    let mut signature_box_writer = create_sig_file(&signature_path)?;
    let sk_str = std::fs::read_to_string(sk_path)?;
    let sk_box: SecretKeyBox = sk_str.clone().into();
    let sk = match sk_box.into_unencrypted_secret_key() {
        Ok(sk) => sk,
        Err(_) => {
            let sk_box: SecretKeyBox = sk_str.into();
            sk_box.into_secret_key(if passwordless {
                Some(Default::default())
            } else {
                None
            })?
        }
    };
    let trusted_comment = if let Some(trusted_comment) = trusted_comment {
        trusted_comment.to_string()
    } else {
        format!(
            "timestamp:{}\tfile:{}\tprehashed",
            unix_timestamp(),
            data_path.as_ref().display()
        )
    };
    let data_reader = open_data_file(data_path)?;
    let signature_box = sign(
        pk.as_ref(),
        &sk,
        data_reader,
        Some(trusted_comment.as_str()),
        untrusted_comment,
    )?;
    signature_box_writer.write_all(&signature_box.to_bytes())?;
    signature_box_writer.flush()?;
    Ok(())
}

pub fn cmd_verify<P, Q>(
    pk: PublicKey,
    data_path: P,
    signature_path: Q,
    quiet: bool,
    output: bool,
    allow_legacy: bool,
) -> Result<()>
where
    P: AsRef<Path>,
    Q: AsRef<Path>,
{
    let signature_box = SignatureBox::from_file(&signature_path).map_err(|err| {
        PError::new(
            ErrorKind::Io,
            format!(
                "could not read signature file {}: {}",
                signature_path.as_ref().display(),
                err
            ),
        )
    })?;
    let data_reader = open_data_file(&data_path).map_err(|err| {
        PError::new(
            ErrorKind::Io,
            format!(
                "could not read data file {}: {}",
                data_path.as_ref().display(),
                err
            ),
        )
    })?;
    let trusted_comment = signature_box.trusted_comment()?;
    if !is_printable(&trusted_comment) {
        return Err(PError::new(
            ErrorKind::Verify,
            "Signature file contains unprintable characters",
        ));
    }

    verify(&pk, &signature_box, data_reader, true, output, allow_legacy)?;

    if !quiet {
        eprintln!("Signature and comment signature verified");
        eprintln!("Trusted comment: {}", trusted_comment);
    }
    Ok(())
}

fn create_sk_path_or_default(sk_path_str: Option<&str>, force: bool) -> Result<PathBuf> {
    let sk_path = match sk_path_str {
        Some(path) => {
            let complete_path = PathBuf::from(path);
            let mut dir = complete_path.clone();
            dir.pop();
            create_dir(&dir)?;
            complete_path
        }
        None => match std::env::var(SIG_DEFAULT_CONFIG_DIR_ENV_VAR) {
            Ok(env_path) => {
                let mut complete_path = PathBuf::from(env_path);
                if !complete_path.exists() {
                    return Err(PError::new(
                        ErrorKind::Io,
                        format!(
                            "folder {} referenced by {} doesn't exist, you'll have to create \
                             it yourself",
                            complete_path.display(),
                            SIG_DEFAULT_CONFIG_DIR_ENV_VAR
                        ),
                    ));
                }
                complete_path.push(SIG_DEFAULT_SKFILE);
                complete_path
            }
            Err(_) => {
                let home_path =
                    home_dir().ok_or_else(|| PError::new(ErrorKind::Io, "can't find home dir"))?;
                let mut complete_path = home_path;
                complete_path.push(SIG_DEFAULT_CONFIG_DIR);
                if !complete_path.exists() {
                    create_dir(&complete_path)?;
                }
                complete_path.push(SIG_DEFAULT_SKFILE);
                complete_path
            }
        },
    };
    if sk_path.exists() {
        if !force {
            return Err(PError::new(
                ErrorKind::Io,
                format!(
                    "Key generation aborted:
{} already exists

If you really want to overwrite the existing key pair, add the -f switch to
force this operation.",
                    sk_path.display()
                ),
            ));
        } else {
            std::fs::remove_file(&sk_path)?;
        }
    }
    Ok(sk_path)
}

fn get_pk_path(explicit_path: Option<&str>) -> Result<PathBuf> {
    Ok(PathBuf::from(explicit_path.unwrap_or(SIG_DEFAULT_PKFILE)))
}

fn get_sk_path(explicit_path: Option<&str>) -> Result<PathBuf> {
    let default_file_name = SIG_DEFAULT_SKFILE;
    match explicit_path {
        Some(explicit_path) => Ok(PathBuf::from(explicit_path)),
        None => match std::env::var(SIG_DEFAULT_CONFIG_DIR_ENV_VAR) {
            Ok(env_path) => {
                let mut complete_path = PathBuf::from(env_path);
                complete_path.push(default_file_name);
                Ok(complete_path)
            }
            Err(_) => {
                let home_path =
                    home_dir().ok_or_else(|| PError::new(ErrorKind::Io, "can't find home dir"))?;
                let mut complete_path = home_path;
                complete_path.push(SIG_DEFAULT_CONFIG_DIR);
                complete_path.push(default_file_name);
                Ok(complete_path)
            }
        },
    }
}

fn run(args: clap::ArgMatches, help_usage: &str) -> Result<()> {
    if let Some(generate_action) = args.subcommand_matches("generate") {
        let force = generate_action.get_flag("force");
        let pk_path = get_pk_path(
            generate_action
                .get_one::<String>("pk_path")
                .map(|s| s.as_str()),
        )?;
        let sk_path_str = generate_action.get_one::<String>("sk_path");
        let sk_path = create_sk_path_or_default(sk_path_str.map(|s| s.as_str()), force)?;
        let comment = generate_action.get_one::<String>("comment");
        let passwordless = generate_action.get_flag("passwordless");
        let unencrypted = generate_action.get_flag("unencrypted");
        let KeyPair { pk, .. } = cmd_generate(
            force,
            &pk_path,
            &sk_path,
            comment.map(|s| s.as_str()),
            passwordless,
            unencrypted,
        )?;
        println!(
            "\nThe secret key was saved as {} - Keep it secret!",
            sk_path.display()
        );
        println!(
            "The public key was saved as {} - That one can be public.\n",
            pk_path.display()
        );
        println!("Files signed using this key pair can be verified with the following command:\n");
        println!("rsign verify <file> -P {}", pk.to_base64());
        Ok(())
    } else if let Some(sign_action) = args.subcommand_matches("sign") {
        let sk_path = get_sk_path(sign_action.get_one::<String>("sk_path").map(|s| s.as_str()))?;
        let pk = if let Some(pk_inline) = sign_action.get_one::<String>("public_key") {
            Some(PublicKey::from_base64(pk_inline)?)
        } else if let Some(pk_path) = sign_action.get_one::<String>("pk_path") {
            Some(PublicKey::from_file(get_pk_path(Some(pk_path))?)?)
        } else {
            None
        };
        let data_path = PathBuf::from(sign_action.get_one::<String>("data").unwrap()); // safe to unwrap
        let signature_path = if let Some(file) = sign_action.get_one::<String>("sig_file") {
            PathBuf::from(file)
        } else {
            PathBuf::from(format!("{}{}", data_path.display(), SIG_SUFFIX))
        };
        let trusted_comment = sign_action.get_one::<String>("trusted-comment");
        let trusted_comment = trusted_comment.map(|s| s.as_str());
        let untrusted_comment = sign_action.get_one::<String>("untrusted-comment");
        let untrusted_comment = untrusted_comment.map(|s| s.as_str());
        let passwordless = sign_action.get_flag("passwordless");
        cmd_sign(
            pk,
            sk_path,
            signature_path,
            &data_path,
            trusted_comment,
            untrusted_comment,
            passwordless,
        )
    } else if let Some(verify_action) = args.subcommand_matches("verify") {
        let pk = if let Some(pk_inline) = verify_action.get_one::<String>("public_key") {
            PublicKey::from_base64(pk_inline)?
        } else {
            PublicKey::from_file(get_pk_path(
                verify_action
                    .get_one::<String>("pk_path")
                    .map(|s| s.as_str()),
            )?)?
        };
        let data_path = verify_action.get_one::<String>("file").unwrap();
        let signature_path = if let Some(path) = verify_action.get_one::<String>("sig_file") {
            PathBuf::from(path)
        } else {
            PathBuf::from(format!("{data_path}{SIG_SUFFIX}"))
        };
        let quiet = verify_action.get_flag("quiet");
        let output = verify_action.get_flag("output");
        let allow_legacy = verify_action.get_flag("allow-legacy");
        cmd_verify(pk, data_path, signature_path, quiet, output, allow_legacy)
    } else {
        println!("{help_usage}\n");
        std::process::exit(1);
    }
}

fn main() {
    let (args, help_usage) = parse_args();
    run(args, &help_usage).map_err(|e| e.exit()).unwrap();
    std::process::exit(0);
}