use crate::db::Database;
use crate::identity::IdKeyData;
use anyhow::bail;
use pico_args::Arguments;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::process::{Command, exit};
use std::str::FromStr;
use tempfile::NamedTempFile;
#[derive(Debug)]
enum Mode {
Encrypt,
Decrypt,
}
pub fn wrapper(mut pargs: Arguments, db_path: &Path) -> anyhow::Result<()> {
let verbose = pargs.contains(["-v", "--verbose"]);
let contains_encrypt = pargs.contains(["-e", "--encrypt"]);
let contains_decrypt = pargs.contains(["-d", "--decrypt"]);
if contains_encrypt && contains_decrypt {
bail!("cannot specify both --encrypt and --decrypt, pick one");
}
let mode = if contains_decrypt {
Mode::Decrypt
} else {
Mode::Encrypt
};
let output: Option<PathBuf> = pargs.opt_value_from_str(["-o", "--output"])?;
let armor = pargs.contains(["-a", "--armor"]);
let passphrase = pargs.contains(["-p", "--passphrase"]);
let mut recipients = vec![];
loop {
let maybe_r: Option<String> = pargs.opt_value_from_str(["-r", "--recipient"])?;
if let Some(r) = maybe_r {
recipients.push(r);
} else {
break;
}
}
if let Ok(Some(f)) = pargs.opt_value_from_str::<[&str; 2], PathBuf>(["-R", "--recipients-file"])
{
recipients.extend(load_keyfile(&f)?);
}
let mut identities = vec![];
loop {
let maybe_i: Option<String> = pargs.opt_value_from_str(["-i", "--identity"])?;
if let Some(i) = maybe_i {
let Ok(path) = PathBuf::from_str(&i);
if path.exists() {
identities.extend(load_keyfile(&path)?);
} else {
identities.push(i);
}
} else {
break;
}
}
let input: Option<PathBuf> = pargs.opt_free_from_str()?;
if verbose {
println!(
"resolved args: {:?} output={:?} armor?{:?} passphrase?{:?} recipients=>{:?} identities=>{:?} input={:?}",
mode, output, armor, passphrase, recipients, identities, input
);
}
// Resolve recipients and identities to the database
let db = Database::load_or_create(db_path)?;
// resolve recipients
let resolved_recipients = recipients
.iter()
.map(|u| {
// is this already a valid key?
if u.starts_with("age1") {
// regular ol' pubkey
return u.clone();
}
// otherwise, resolve it in the database
let results = db.fuzzy_search(u.clone());
if results.is_empty() {
eprintln!("no results for recipient {u}");
exit(1);
}
if results.len() > 1 {
eprintln!("recipient {u} is ambiguous, please narrow it down");
exit(1);
}
let r = &results[0];
if verbose {
eprintln!(
"resolved rcpt {u} => {:?} (keyid {})",
r,
r.keys.keyid().unwrap()
);
}
r.keys.pk().unwrap()
})
.collect::<Vec<_>>();
// resolve identities
let resolved_identities = identities.iter()
.map(|u| {
// is this already a valid key?
if u.starts_with("AGE-SECRET-KEY") {
// regular ol' sk
return u.clone()
}
// otherwise, resolve it in the database
let results = db.fuzzy_search(u.clone());
if results.is_empty() {
eprintln!("no results for identity {u}");
exit(1);
}
if results.len() > 1 {
eprintln!("identity {u} is ambiguous, please narrow it down");
exit(1);
}
let r = &results[0];
if verbose {
eprintln!("resolved id {u} => {:?} (keyid {})", r, r.keys.keyid().unwrap());
}
match &r.keys {
IdKeyData::Local(sk) => sk.clone(),
IdKeyData::Peer(_) => {
eprintln!("resolved identity is a peer key, not a local key. perhaps revise your search terms?");
exit(1);
}
}
})
.collect::<Vec<_>>();
if verbose {
eprintln!("resolved recipients: {:#?}", resolved_recipients);
}
// write recipients to a temp file
let r_tmpfile = NamedTempFile::new()?;
std::fs::write(r_tmpfile.path(), resolved_recipients.join("\n"))?;
let i_tmpfile = NamedTempFile::new()?;
std::fs::write(i_tmpfile.path(), resolved_identities.join("\n"))?;
// execute age
let mut args = vec![];
args.push(
match mode {
Mode::Encrypt => "--encrypt",
Mode::Decrypt => "--decrypt",
}
.to_string(),
);
if let Some(o) = output {
args.push("--output".to_string());
args.push(o.display().to_string());
}
if armor {
args.push("--armor".to_string());
}
if passphrase {
args.push("--passphrase".to_string());
}
if !resolved_recipients.is_empty() {
args.push("--recipients-file".to_string());
args.push(r_tmpfile.path().display().to_string());
}
if !resolved_identities.is_empty() {
args.push("--identity".to_string());
args.push(i_tmpfile.path().display().to_string());
}
if let Some(i) = input {
args.push(i.display().to_string());
}
if verbose {
eprintln!("exec age {:?}", args);
}
let mut c = match Command::new("age").args(args).spawn() {
Ok(c) => c,
Err(e) if e.kind() == ErrorKind::NotFound => {
eprintln!("{e}");
eprintln!("is `age` installed?");
exit(0);
}
Err(e) => {
return Err(e)?;
}
};
c.wait()?;
Ok(())
}
fn load_keyfile(path: &Path) -> anyhow::Result<Vec<String>> {
let contents = std::fs::read_to_string(path)?;
Ok(contents
.lines()
.filter(|u| !u.is_empty())
.filter(|u| !u.starts_with("#"))
.map(|u| u.to_string())
.collect())
}