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 = 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 = 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 = 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 = 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::>(); // 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::>(); 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> { 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()) }