diff options
Diffstat (limited to 'sq/src/sq.rs')
-rw-r--r-- | sq/src/sq.rs | 739 |
1 files changed, 739 insertions, 0 deletions
diff --git a/sq/src/sq.rs b/sq/src/sq.rs new file mode 100644 index 00000000..31107e93 --- /dev/null +++ b/sq/src/sq.rs @@ -0,0 +1,739 @@ +/// A command-line frontend for Sequoia. + +use crossterm; +use tokio_core; + +use crossterm::terminal; +use anyhow::Context as _; +use prettytable::{Table, Cell, Row, row, cell}; +use std::fs::OpenOptions; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process::exit; +use chrono::{DateTime, offset::Utc}; + +use buffered_reader::File; +use sequoia_openpgp as openpgp; +use sequoia_core; +use sequoia_net; +use sequoia_store as store; + +use crate::openpgp::Result; +use crate::openpgp::{armor, Cert}; +use sequoia_autocrypt as autocrypt; +use crate::openpgp::fmt::hex; +use crate::openpgp::types::KeyFlags; +use crate::openpgp::parse::Parse; +use crate::openpgp::serialize::{Serialize, stream::{Message, Armorer}}; +use crate::openpgp::cert::prelude::*; +use crate::openpgp::policy::StandardPolicy as P; +use sequoia_core::{Context, NetworkPolicy}; +use sequoia_net::{KeyServer, wkd}; +use store::{Mapping, LogIter}; + +mod sq_cli; +mod commands; +use commands::dump::Convert; + +fn open_or_stdin(f: Option<&str>) -> Result<Box<dyn io::Read>> { + match f { + Some(f) => Ok(Box::new(File::open(f) + .context("Failed to open input file")?)), + None => Ok(Box::new(io::stdin())), + } +} + +fn create_or_stdout(f: Option<&str>, force: bool) + -> Result<Box<dyn io::Write>> { + match f { + None => Ok(Box::new(io::stdout())), + Some(p) if p == "-" => Ok(Box::new(io::stdout())), + Some(f) => { + let p = Path::new(f); + if !p.exists() || force { + Ok(Box::new(OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .open(f) + .context("Failed to create output file")?)) + } else { + Err(anyhow::anyhow!( + format!("File {:?} exists, use --force to overwrite", p))) + } + } + } +} + +fn create_or_stdout_pgp<'a>(f: Option<&str>, force: bool, + binary: bool, kind: armor::Kind) + -> Result<Message<'a>> +{ + let sink = create_or_stdout(f, force)?; + let mut message = Message::new(sink); + if ! binary { + message = Armorer::new(message).kind(kind).build()?; + } + Ok(message) +} + +fn load_certs<'a, I>(files: I) -> openpgp::Result<Vec<Cert>> + where I: Iterator<Item=&'a str> +{ + let mut certs = vec![]; + for f in files { + certs.push(Cert::from_file(f) + .context(format!("Failed to load key from file {:?}", f))?); + } + Ok(certs) +} + +/// Serializes a keyring, adding descriptive headers if armored. +fn serialize_keyring(mut output: &mut dyn io::Write, certs: &[Cert], binary: bool) + -> openpgp::Result<()> { + // Handle the easy options first. No armor no cry: + if binary { + for cert in certs { + cert.serialize(&mut output)?; + } + return Ok(()); + } + + // Just one Cert? Ez: + if certs.len() == 1 { + return certs[0].armored().serialize(&mut output); + } + + // Otherwise, collect the headers first: + let mut headers = Vec::new(); + for (i, cert) in certs.iter().enumerate() { + headers.push(format!("Key #{}", i)); + headers.append(&mut cert.armor_headers()); + } + + let headers: Vec<_> = headers.iter() + .map(|value| ("Comment", value.as_str())) + .collect(); + let mut output = armor::Writer::with_headers(&mut output, + armor::Kind::PublicKey, + headers)?; + for cert in certs { + cert.serialize(&mut output)?; + } + output.finalize()?; + Ok(()) +} + +fn parse_armor_kind(kind: Option<&str>) -> armor::Kind { + match kind.expect("has default value") { + "message" => armor::Kind::Message, + "publickey" => armor::Kind::PublicKey, + "secretkey" => armor::Kind::SecretKey, + "signature" => armor::Kind::Signature, + "file" => armor::Kind::File, + _ => unreachable!(), + } +} + +/// Prints a warning if the user supplied "help" or "-help" to an +/// positional argument. +/// +/// This should be used wherever a positional argument is followed by +/// an optional positional argument. +fn help_warning(arg: &str) { + if arg == "help" { + eprintln!("Warning: \"help\" is not a subcommand here. \ + Did you mean --help?"); + } +} + +fn main() -> Result<()> { + let policy = &mut P::new(); + + let matches = sq_cli::build().get_matches(); + + let known_notations: Vec<&str> = matches.values_of("known-notation") + .unwrap_or_default() + .collect(); + policy.good_critical_notations(&known_notations); + + let network_policy = match matches.value_of("policy") { + None => NetworkPolicy::Encrypted, + Some("offline") => NetworkPolicy::Offline, + Some("anonymized") => NetworkPolicy::Anonymized, + Some("encrypted") => NetworkPolicy::Encrypted, + Some("insecure") => NetworkPolicy::Insecure, + Some(_) => { + eprintln!("Bad network policy, must be offline, anonymized, encrypted, or insecure."); + exit(1); + }, + }; + let force = matches.is_present("force"); + let (realm_name, mapping_name) = { + let s = matches.value_of("mapping").expect("has a default value"); + if let Some(i) = s.find('/') { + (&s[..i], &s[i+1..]) + } else { + (s, "default") + } + }; + let mut builder = Context::configure() + .network_policy(network_policy); + if let Some(dir) = matches.value_of("home") { + builder = builder.home(dir); + } + let ctx = builder.build()?; + let mut core = tokio_core::reactor::Core::new()?; + + match matches.subcommand() { + ("decrypt", Some(m)) => { + let mut input = open_or_stdin(m.value_of("input"))?; + let mut output = create_or_stdout(m.value_of("output"), force)?; + let signatures: usize = + m.value_of("signatures").unwrap_or("0").parse()?; + let certs = m.values_of("sender-cert-file") + .map(load_certs) + .unwrap_or(Ok(vec![]))?; + let secrets = m.values_of("secret-key-file") + .map(load_certs) + .unwrap_or(Ok(vec![]))?; + let mut mapping = Mapping::open(&ctx, realm_name, mapping_name) + .context("Failed to open the mapping")?; + commands::decrypt(&ctx, policy, &mut mapping, + &mut input, &mut output, + signatures, certs, secrets, + m.is_present("dump-session-key"), + m.is_present("dump"), m.is_present("hex"))?; + }, + ("encrypt", Some(m)) => { + let mapping = Mapping::open(&ctx, realm_name, mapping_name) + .context("Failed to open the mapping")?; + let mut recipients = m.values_of("recipient-key-file") + .map(load_certs) + .unwrap_or(Ok(vec![]))?; + if let Some(r) = m.values_of("recipient") { + for recipient in r { + recipients.push(mapping.lookup(recipient) + .context("No such key found")?.cert()?); + } + } + let mut input = open_or_stdin(m.value_of("input"))?; + let output = + create_or_stdout_pgp(m.value_of("output"), force, + m.is_present("binary"), + armor::Kind::Message)?; + let additional_secrets = m.values_of("signer-key-file") + .map(load_certs) + .unwrap_or(Ok(vec![]))?; + let mode = match m.value_of("mode").expect("has default") { + "rest" => KeyFlags::empty() + .set_storage_encryption(), + "transport" => KeyFlags::empty() + .set_transport_encryption(), + "all" => KeyFlags::empty() + .set_storage_encryption() + .set_transport_encryption(), + _ => unreachable!("uses possible_values"), + }; + let time = if let Some(time) = m.value_of("time") { + Some(parse_iso8601(time, chrono::NaiveTime::from_hms(0, 0, 0)) + .context(format!("Bad value passed to --time: {:?}", + time))?.into()) + } else { + None + }; + commands::encrypt(policy, &mut input, output, + m.occurrences_of("symmetric") as usize, + &recipients, additional_secrets, + mode, + m.value_of("compression").expect("has default"), + time.into())?; + }, + ("sign", Some(m)) => { + let mut input = open_or_stdin(m.value_of("input"))?; + let output = m.value_of("output"); + let detached = m.is_present("detached"); + let binary = m.is_present("binary"); + let append = m.is_present("append"); + let notarize = m.is_present("notarize"); + let secrets = m.values_of("secret-key-file") + .map(load_certs) + .unwrap_or(Ok(vec![]))?; + let time = if let Some(time) = m.value_of("time") { + Some(parse_iso8601(time, chrono::NaiveTime::from_hms(0, 0, 0)) + .context(format!("Bad value passed to --time: {:?}", + time))?.into()) + } else { + None + }; + commands::sign(policy, &mut input, output, secrets, detached, binary, + append, notarize, time, force)?; + }, + ("verify", Some(m)) => { + let mut input = open_or_stdin(m.value_of("input"))?; + let mut output = create_or_stdout(m.value_of("output"), force)?; + let mut detached = if let Some(f) = m.value_of("detached") { + Some(File::open(f)?) + } else { + None + }; + let signatures: usize = + m.value_of("signatures").unwrap_or("0").parse()?; + let certs = m.values_of("sender-cert-file") + .map(load_certs) + .unwrap_or(Ok(vec![]))?; + let mut mapping = Mapping::open(&ctx, realm_name, mapping_name) + .context("Failed to open the mapping")?; + commands::verify(&ctx, policy, &mut mapping, &mut input, + detached.as_mut().map(|r| r as &mut dyn io::Read), + &mut output, signatures, certs)?; + }, + + ("enarmor", Some(m)) => { + let mut input = open_or_stdin(m.value_of("input"))?; + let mut output = + create_or_stdout_pgp(m.value_of("output"), force, + false, + parse_armor_kind(m.value_of("kind")))?; + io::copy(&mut input, &mut output)?; + output.finalize()?; + }, + ("dearmor", Some(m)) => { + let mut input = open_or_stdin(m.value_of("input"))?; + let mut output = create_or_stdout(m.value_of("output"), force)?; + let mut filter = armor::Reader::new(&mut input, None); + io::copy(&mut filter, &mut output)?; + }, + ("autocrypt", Some(m)) => { + match m.subcommand() { + ("decode", Some(m)) => { + let input = open_or_stdin(m.value_of("input"))?; + let mut output = + create_or_stdout_pgp(m.value_of("output"), force, + true, + armor::Kind::PublicKey)?; + let ac = autocrypt::AutocryptHeaders::from_reader(input)?; + for h in &ac.headers { + if let Some(ref cert) = h.key { + cert.serialize(&mut output)?; + } + } + output.finalize()?; + }, + ("encode-sender", Some(m)) => { + let input = open_or_stdin(m.value_of("input"))?; + let mut output = create_or_stdout(m.value_of("output"), + force)?; + let cert = Cert::from_reader(input)?; + let addr = m.value_of("address").map(|a| a.to_string()) + .or_else(|| { + cert.with_policy(policy, None) + .and_then(|vcert| vcert.primary_userid()).ok() + .map(|ca| ca.userid().to_string()) + }); + let ac = autocrypt::AutocryptHeader::new_sender( + policy, + &cert, + &addr.ok_or(anyhow::anyhow!( + "No well-formed primary userid found, use \ + --address to specify one"))?, + m.value_of("prefer-encrypt").expect("has default"))?; + write!(&mut output, "Autocrypt: ")?; + ac.serialize(&mut output)?; + }, + _ => unreachable!(), + } + }, + + ("inspect", Some(m)) => { + let mut output = create_or_stdout(m.value_of("output"), force)?; + commands::inspect(m, policy, &mut output)?; + }, + + ("packet", Some(m)) => match m.subcommand() { + ("dump", Some(m)) => { + let mut input = open_or_stdin(m.value_of("input"))?; + let mut output = create_or_stdout(m.value_of("output"), force)?; + let session_key: Option<openpgp::crypto::SessionKey> = + if let Some(sk) = m.value_of("session-key") { + Some(hex::decode_pretty(sk)?.into()) + } else { + None + }; + let width = terminal::size().ok().map(|(cols, _)| cols as usize); + commands::dump(&mut input, &mut output, + m.is_present("mpis"), m.is_present("hex"), + session_key.as_ref(), width)?; + }, + + ("decrypt", Some(m)) => { + let mut input = open_or_stdin(m.value_of("input"))?; + let mut output = + create_or_stdout_pgp(m.value_of("output"), force, + m.is_present("binary"), + armor::Kind::Message)?; + let secrets = m.values_of("secret-key-file") + .map(load_certs) + .unwrap_or(Ok(vec![]))?; + let mut mapping = Mapping::open(&ctx, realm_name, mapping_name) + .context("Failed to open the mapping")?; + commands::decrypt::decrypt_unwrap( + &ctx, policy, &mut mapping, + &mut input, &mut output, + secrets, m.is_present("dump-session-key"))?; + output.finalize()?; + }, + + ("split", Some(m)) => { + let mut input = open_or_stdin(m.value_of("input"))?; + let prefix = + // The prefix is either specified explicitly... + m.value_of("prefix").map(|p| p.to_owned()) + .unwrap_or( + // ... or we derive it from the input file... + m.value_of("input").and_then(|i| { + let p = PathBuf::from(i); + // (but only use the filename) + p.file_name().map(|f| String::from(f.to_string_lossy())) + }) + // ... or we use a generic prefix... + .unwrap_or(String::from("output")) + // ... finally, add a hyphen to the derived prefix. + + "-"); + commands::split(&mut input, &prefix)?; + }, + ("join", Some(m)) => { + let mut output = + create_or_stdout_pgp(m.value_of("output"), force, + m.is_present("binary"), + parse_armor_kind(m.value_of("kind")))?; + commands::join(m.values_of("input"), &mut output)?; + output.finalize()?; + }, + _ => unreachable!(), + }, + + ("keyserver", Some(m)) => { + let mut ks = if let Some(uri) = m.value_of("server") { + KeyServer::new(&ctx, &uri) + } else { + KeyServer::keys_openpgp_org(&ctx) + }.context("Malformed keyserver URI")?; + + match m.subcommand() { + ("get", Some(m)) => { + let keyid = m.value_of("keyid").unwrap(); + let id = keyid.parse(); + if id.is_err() { + eprintln!("Malformed key ID: {:?}\n\ + (Note: only long Key IDs are supported.)", + keyid); + exit(1); + } + let id = id.unwrap(); + + let mut output = create_or_stdout(m.value_of("output"), force)?; + let cert = core.run(ks.get(&id)) + .context("Failed to retrieve key")?; + if ! m.is_present("binary") { + cert.armored().serialize(&mut output) + } else { + cert.serialize(&mut output) + }.context("Failed to serialize key")?; + }, + ("send", Some(m)) => { + let mut input = open_or_stdin(m.value_of("input"))?; + let cert = Cert::from_reader(&mut input). + context("Malformed key")?; + + core.run(ks.send(&cert)) + .context("Failed to send key to server")?; + }, + _ => unreachable!(), + } + }, + ("mapping", Some(m)) => { + let mapping = Mapping::open(&ctx, realm_name, mapping_name) + .context("Failed to open the mapping")?; + + match m.subcommand() { + ("list", Some(_)) => { + list_bindings(&mapping, realm_name, mapping_name)?; + }, + ("add", Some(m)) => { + let fp = m.value_of("fingerprint").unwrap().parse() + .expect("Malformed fingerprint"); + mapping.add(m.value_of("label").unwrap(), &fp)?; + }, + ("import", Some(m)) => { + let label = m.value_of("label").unwrap(); + help_warning(label); + let mut input = open_or_stdin(m.value_of("input"))?; + let cert = Cert::from_reader(&mut input)?; + mapping.import(label, &cert)?; + }, + ("export", Some(m)) => { + let cert = mapping.lookup(m.value_of("label").unwrap())?.cert()?; + let mut output = create_or_stdout(m.value_of("output"), force)?; + if m.is_present("binary") { + cert.serialize(&mut output)?; + } else { + cert.armored().serialize(&mut output)?; + } + }, + ("delete", Some(m)) => { + if m.is_present("label") == m.is_present("the-mapping") { + eprintln!("Please specify either a label or --the-mapping."); + exit(1); + } + + if m.is_present("the-mapping") { + mapping.delete().context("Failed to delete the mapping")?; + } else { + let binding = mapping.lookup(m.value_of("label").unwrap()) + .context("Failed to get key")?; + binding.delete().context("Failed to delete the binding")?; + } + }, + ("stats", Some(m)) => { + commands::mapping_print_stats(&mapping, + m.value_of("label").unwrap())?; + }, + ("log", Some(m)) => { + if m.is_present("label") { + let binding = mapping.lookup(m.value_of("label").unwrap()) + .context("No such key")?; + print_log(binding.log().context("Failed to get log")?, false); + } else { + print_log(mapping.log().context("Failed to get log")?, true); + } + }, + _ => unreachable!(), + } + }, + ("list", Some(m)) => { + match m.subcommand() { + ("mappings", Some(m)) => { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE); + table.set_titles(row!["realm", "name", "network policy"]); + + for (realm, name, network_policy, _) + in Mapping::list(&ctx, m.value_of("prefix").unwrap_or(""))? { + table.add_row(Row::new(vec![ + Cell::new(&realm), + Cell::new(&name), + Cell::new(&format!("{:?}", network_policy)) + ])); + } + + table.printstd(); + }, + ("bindings", Some(m)) => { + for (realm, name, _, mapping) + in Mapping::list(&ctx, m.value_of("prefix").unwrap_or(""))? { + list_bindings(&mapping, &realm, &name)?; + } + }, + ("keys", Some(_)) => { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE); + table.set_titles(row!["fingerprint", "updated", "status"]); + + for (fingerprint, key) in store::Store::list_keys(&ctx)? { + let stats = key.stats() + .context("Failed to get key stats")?; + table.add_row(Row::new(vec![ + Cell::new(&fingerprint.to_string()), + if let Some(t) = stats.updated { + Cell::new(&t.convert().to_string()) + } else { + Cell::new("") + }, + Cell::new("") + ])); + } + + table.printstd(); + }, + ("log", Some(_)) => { + print_log(store::Store::server_log(&ctx)?, true); + }, + _ => unreachable!(), + } + }, + ("key", Some(m)) => match m.subcommand() { + ("generate", Some(m)) => commands::key::generate(m, force)?, + _ => unreachable!(), + }, + ("wkd", Some(m)) => { + match m.subcommand() { + ("url", Some(m)) => { + let email_address = m.value_of("input").unwrap(); + let wkd_url = wkd::Url::from(email_address)?; + // XXX: Add other subcomand to specify whether it should be + // created with the advanced or the direct method. + let url = wkd_url.to_url(None)?; + println!("{}", url); + }, + ("get", Some(m)) => { + let email_address = m.value_of("input").unwrap(); + // XXX: EmailAddress could be created here to + // check it's a valid email address, print the error to + // stderr and exit. + // Because it might be created a WkdServer struct, not + // doing it for now. + let certs = core.run(wkd::get(&email_address))?; + // ```text + // The HTTP GET method MUST return the binary representation of the + // OpenPGP key for the given mail address. + // [draft-koch]: https://datatracker.ietf.org/doc/html/draft-koch-openpgp-webkey-service-07 + // ``` + // But to keep the parallelism with `store export` and `keyserver get`, + // The output is armored if not `--binary` option is given. + let mut output = create_or_stdout(m.value_of("output"), force)?; + serialize_keyring(&mut output, &certs, + m.is_present("binary"))?; + }, + ("generate", Some(m)) => { + let domain = m.value_of("domain").unwrap(); + let f = open_or_stdin(m.value_of("input"))?; + let base_path = + m.value_of("base_directory").expect("required"); + let variant = if m.is_present("direct_method") { + wkd::Variant::Direct + } else { + wkd::Variant::Advanced + }; + let parser = CertParser::from_reader(f)?; + let certs: Vec<Cert> = parser.filter_map(|cert| cert.ok()) + .collect(); + for cert in certs { + wkd::insert(&base_path, domain, variant, &cert) + .context(format!("Failed to generate the WKD in \ + {}.", base_path))?; + } + }, + _ => unreachable!(), + } + }, + _ => unreachable!(), + } + + return Ok(()) +} + +fn list_bindings(mapping: &Mapping, realm: &str, name: &str) + -> Result<()> { + if mapping.iter()?.count() == 0 { + println!("No label-key bindings in the \"{}/{}\" mapping.", + realm, name); + return Ok(()); + } + + println!("Realm: {:?}, mapping: {:?}:", realm, name); + + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE); + table.set_titles(row!["label", "fingerprint"]); + for (label, fingerprint, _) in mapping.iter()? { + table.add_row(Row::new(vec![ + Cell::new(&label), + Cell::new(&fingerprint.to_string())])); + } + table.printstd(); + Ok(()) +} + +fn print_log(iter: LogIter, with_slug: bool) { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE); + let mut head = row!["timestamp", "message"]; + if with_slug { + head.insert_cell(1, Cell::new("slug")); + } + table.set_titles(head); + + for entry in iter { + let mut row = row![&entry.timestamp.convert().to_string(), + &entry.short()]; + if with_slug { + row.insert_cell(1, Cell::new(&entry.slug)); + } + table.add_row(row); + } + + table.printstd(); +} + +/// Parses the given string depicting a ISO 8601 timestamp. +fn parse_iso8601(s: &str, pad_date_with: chrono::NaiveTime) + -> Result<DateTime<Utc>> +{ + // If you modify this function this function, synchronize the + // changes with the copy in sqv.rs! + for f in &[ + "%Y-%m-%dT%H:%M:%S%#z", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%dT%H:%M%#z", + "%Y-%m-%dT%H:%M", + "%Y-%m-%dT%H%#z", + "%Y-%m-%dT%H", + "%Y%m%dT%H%M%S%#z", + "%Y%m%dT%H%M%S", + "%Y%m%dT%H%M%#z", + "%Y%m%dT%H%M", + "%Y%m%dT%H%#z", + "%Y%m%dT%H", + ] { + if f.ends_with("%#z") { + if let Ok(d) = DateTime::parse_from_str(s, *f) { + return Ok(d.into()); + } + } else { + if let Ok(d) = chrono::NaiveDateTime::parse_from_str(s, *f) { + return Ok(DateTime::from_utc(d, Utc)); + } + } + } + for f in &[ + "%Y-%m-%d", + "%Y-%m", + "%Y-%j", + "%Y%m%d", + "%Y%m", + "%Y%j", + "%Y", + ] { + if let Ok(d) = chrono::NaiveDate::parse_from_str(s, *f) { + return Ok(DateTime::from_utc(d.and_time(pad_date_with), Utc)); + } + } + Err(anyhow::anyhow!("Malformed ISO8601 timestamp: {}", s)) +} + +#[test] +fn test_parse_iso8601() { + let z = chrono::NaiveTime::from_hms(0, 0, 0); + parse_iso8601("2017-03-04T13:25:35Z", z).unwrap(); + parse_iso8601("2017-03-04T13:25:35+08:30", z).unwrap(); + parse_iso8601("2017-03-04T13:25:35", z).unwrap(); + parse_iso8601("2017-03-04T13:25Z", z).unwrap(); + parse_iso8601("2017-03-04T13:25", z).unwrap(); + // parse_iso8601("2017-03-04T13Z", z).unwrap(); // XXX: chrono doesn't like + // parse_iso8601("2017-03-04T13", z).unwrap(); // ditto + parse_iso8601("2017-03-04", z).unwrap(); + // parse_iso8601("2017-03", z).unwrap(); // ditto + parse_iso8601("2017-031", z).unwrap(); + parse_iso8601("20170304T132535Z", z).unwrap(); + parse_iso8601("20170304T132535+0830", z).unwrap(); + parse_iso8601("20170304T132535", z).unwrap(); + parse_iso8601("20170304T1325Z", z).unwrap(); + parse_is |