mod crypto;
use anyhow::{Context, Result};
use clap::Parser;
use clap::builder::Styles;
use clap::builder::styling::{AnsiColor, Effects};
use std::{
fs,
io::{Read, Write, stderr, stdout},
path::PathBuf,
};
use zeroize::Zeroizing;
const STYLE: Styles = Styles::styled()
.header(AnsiColor::Green.on_default().effects(Effects::BOLD))
.usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
.literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
.placeholder(AnsiColor::Cyan.on_default())
.error(AnsiColor::Red.on_default().effects(Effects::BOLD))
.valid(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
.invalid(AnsiColor::Yellow.on_default().effects(Effects::BOLD));
#[derive(Debug, Parser)]
#[command(
version,
about,
styles = STYLE,
help_template(
"\
{before-help}{name} {version} - {about}
{usage-heading} {usage}
{all-args}{after-help}
"
)
)]
struct Args {
#[arg()]
file: Option<PathBuf>,
#[arg(short, long)]
decrypt: bool,
#[arg(short, long)]
keyfile: Option<PathBuf>,
#[arg(short, long)]
remove: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
let input_data = read_input(args.file.as_ref())?;
let secret = if let Some(keyfile) = args.keyfile.as_ref() { read_keyfile(keyfile)? } else { prompt("Password: ")? };
let output_data = if args.decrypt { crypto::decrypt(&input_data, &secret)? } else { crypto::encrypt(&input_data, &secret)? };
stdout().write_all(&output_data).context("write output")?;
stdout().flush().context("flush stdout")?;
if args.remove
&& let Some(file) = &args.file
{
fs::remove_file(file).with_context(|| format!("remove input file '{}'", file.display()))?;
}
Ok(())
}
fn read_input(file: Option<&PathBuf>) -> Result<Vec<u8>> {
if let Some(file) = file {
fs::read(file).with_context(|| format!("read input file '{}'", file.display()))
} else {
let mut data = Vec::new();
let mut stdin = std::io::stdin().lock();
stdin.read_to_end(&mut data).context("read stdin")?;
Ok(data)
}
}
fn read_keyfile(file: &PathBuf) -> Result<Zeroizing<Vec<u8>>> {
let data = fs::read(file).with_context(|| format!("read key file '{}'", file.display()))?;
if data.is_empty() {
anyhow::bail!("keyfile is empty");
}
Ok(Zeroizing::new(data))
}
fn prompt(prompt_text: &str) -> Result<Zeroizing<Vec<u8>>> {
eprint!("{prompt_text}");
stderr().flush().context("flush stderr")?;
let password = console::Term::stderr().read_secure_line().context("read password")?;
if password.is_empty() {
anyhow::bail!("password is empty");
}
Ok(Zeroizing::new(password.into_bytes()))
}