use std::os::unix::prelude::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{exit, Command, Stdio};
use std::str::FromStr;
use std::thread::sleep;
use std::time::Duration;
use anyhow::bail;
use pico_args::Arguments;
use tempfile::NamedTempFile;
use crate::db::Database;
use crate::identity::IdKeyData;
#[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 {
if let Ok(path) = PathBuf::from_str(&i) && 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);
}
Err(Command::new("age")
.args(args)
.exec())?;
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())
}