diff options
author | Neal H. Walfield <neal@pep.foundation> | 2021-01-22 09:38:40 +0100 |
---|---|---|
committer | Neal H. Walfield <neal@pep.foundation> | 2021-01-22 09:38:40 +0100 |
commit | eb0b42f5933772652b8e66fd7a853495b8efcd1e (patch) | |
tree | e42ae49c74b07c204bd323207f9a58e1dccb9a13 /sq/src/commands/keyring.rs | |
parent | e5af90ca2a9ca0552d2b7c21384d6faa51323224 (diff) |
sq: Rename 'sq certring' to 'sq keyring'.
- All of the 'sq certring' operations apply equally well to
'keyrings'.
- Consistent with the new name, don't strip secret key material.
Diffstat (limited to 'sq/src/commands/keyring.rs')
-rw-r--r-- | sq/src/commands/keyring.rs | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/sq/src/commands/keyring.rs b/sq/src/commands/keyring.rs new file mode 100644 index 00000000..5ebeb7aa --- /dev/null +++ b/sq/src/commands/keyring.rs @@ -0,0 +1,318 @@ +use std::{ + collections::HashMap, + collections::hash_map::Entry, + fs::File, + io, + path::PathBuf, +}; +use anyhow::Context; + +use sequoia_openpgp as openpgp; +use openpgp::{ + Result, + armor, + cert::{ + Cert, + CertParser, + }, + Fingerprint, + packet::{ + UserID, + UserAttribute, + Key, + }, + parse::Parse, + serialize::Serialize, +}; + +use crate::{ + open_or_stdin, + create_or_stdout_pgp, +}; + +pub fn dispatch(m: &clap::ArgMatches, force: bool) -> Result<()> { + match m.subcommand() { + ("filter", Some(m)) => { + let any_uid_predicates = + m.is_present("name") + || m.is_present("email") + || m.is_present("domain"); + let uid_predicate = |uid: &UserID| { + let mut keep = false; + + if let Some(names) = m.values_of("name") { + for name in names { + keep |= uid + .name().unwrap_or(None) + .map(|n| n == name) + .unwrap_or(false); + } + } + + if let Some(emails) = m.values_of("email") { + for email in emails { + keep |= uid + .email().unwrap_or(None) + .map(|n| n == email) + .unwrap_or(false); + } + } + + if let Some(domains) = m.values_of("domain") { + for domain in domains { + keep |= uid + .email().unwrap_or(None) + .map(|n| n.ends_with(&format!("@{}", domain))) + .unwrap_or(false); + } + } + + keep + }; + + let any_ua_predicates = false; + let ua_predicate = |_ua: &UserAttribute| false; + + let any_key_predicates = false; + let key_predicate = |_key: &Key<_, _>| false; + + let filter_fn = |c: Cert| -> Option<Cert> { + if ! (c.userids().any(|c| uid_predicate(&c)) + || c.user_attributes().any(|c| ua_predicate(&c)) + || c.keys().subkeys().any(|c| key_predicate(&c))) { + None + } else if m.is_present("prune-certs") { + let c = c + .retain_userids(|c| { + ! any_uid_predicates || uid_predicate(&c) + }) + .retain_user_attributes(|c| { + ! any_ua_predicates || ua_predicate(&c) + }) + .retain_subkeys(|c| { + ! any_key_predicates || key_predicate(&c) + }); + if c.userids().count() == 0 + && c.user_attributes().count() == 0 + && c.keys().subkeys().count() == 0 + { + // We stripped all components, omit this cert. + None + } else { + Some(c) + } + } else { + Some(c) + } + }; + + // XXX: Armor type selection is a bit problematic. If any + // of the certificates contain a secret key, it would be + // better to use Kind::SecretKey here. However, this + // requires buffering all certs, which has its own + // problems. + let mut output = create_or_stdout_pgp(m.value_of("output"), + force, + m.is_present("binary"), + armor::Kind::PublicKey)?; + filter(m.values_of("input"), &mut output, filter_fn)?; + output.finalize() + }, + ("join", Some(m)) => { + // XXX: Armor type selection is a bit problematic. If any + // of the certificates contain a secret key, it would be + // better to use Kind::SecretKey here. However, this + // requires buffering all certs, which has its own + // problems. + let mut output = create_or_stdout_pgp(m.value_of("output"), + force, + m.is_present("binary"), + armor::Kind::PublicKey)?; + filter(m.values_of("input"), &mut output, |c| Some(c))?; + output.finalize() + }, + ("merge", Some(m)) => { + let mut output = create_or_stdout_pgp(m.value_of("output"), + force, + m.is_present("binary"), + armor::Kind::PublicKey)?; + merge(m.values_of("input"), &mut output)?; + output.finalize() + }, + ("list", Some(m)) => { + let mut input = open_or_stdin(m.value_of("input"))?; + list(&mut input) + }, + ("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. + + "-"); + split(&mut input, &prefix, m.is_present("binary")) + }, + + _ => unreachable!(), + } +} + +/// Joins certificates and keyrings into a keyring, applying a filter. +fn filter<F>(inputs: Option<clap::Values>, output: &mut dyn io::Write, + mut filter: F) + -> Result<()> + where F: FnMut(Cert) -> Option<Cert>, +{ + if let Some(inputs) = inputs { + for name in inputs { + for cert in CertParser::from_file(name)? { + let cert = cert.context( + format!("Malformed certificate in keyring {:?}", name))?; + if let Some(cert) = filter(cert) { + cert.as_tsk().serialize(output)?; + } + } + } + } else { + for cert in CertParser::from_reader(io::stdin())? { + let cert = cert.context("Malformed certificate in keyring")?; + if let Some(cert) = filter(cert) { + cert.as_tsk().serialize(output)?; + } + } + } + Ok(()) +} + +/// Lists certs in a keyring. +fn list(input: &mut (dyn io::Read + Sync + Send)) + -> Result<()> { + for (i, cert) in CertParser::from_reader(input)?.enumerate() { + let cert = cert.context("Malformed certificate in keyring")?; + print!("{}. {:X}", i, cert.fingerprint()); + // Try to be more helpful by including the first userid in the + // listing. + if let Some(email) = cert.userids().nth(0) + .and_then(|uid| uid.email().unwrap_or(None)) + { + print!(" {}", email); + } + println!(); + } + Ok(()) +} + +/// Splits a keyring into individual certs. +fn split(input: &mut (dyn io::Read + Sync + Send), prefix: &str, binary: bool) + -> Result<()> { + for (i, cert) in CertParser::from_reader(input)?.enumerate() { + let cert = cert.context("Malformed certificate in keyring")?; + let filename = format!( + "{}{}-{:X}", + prefix, + i, + cert.fingerprint()); + + // Try to be more helpful by including the first userid in the + // filename. + let mut sink = if let Some(f) = cert.userids().nth(0) + .and_then(|uid| uid.email().unwrap_or(None)) + .and_then(to_filename_fragment) + { + let filename_email = format!("{}-{}", filename, f); + if let Ok(s) = File::create(filename_email) { + s + } else { + // Degrade gracefully in case our sanitization + // produced an invalid filename on this system. + File::create(&filename) + .context(format!("Writing cert to {:?} failed", filename))? + } + } else { + File::create(&filename) + .context(format!("Writing cert to {:?} failed", filename))? + }; + + if binary { + cert.as_tsk().serialize(&mut sink)?; + } else { + cert.as_tsk().armored().serialize(&mut sink)?; + } + } + Ok(()) +} + +/// Merge multiple keyrings. +fn merge(inputs: Option<clap::Values>, output: &mut dyn io::Write) + -> Result<()> +{ + let mut certs: HashMap<Fingerprint, Option<Cert>> = HashMap::new(); + + if let Some(inputs) = inputs { + for name in inputs { + for cert in CertParser::from_file(name)? { + let cert = cert.context( + format!("Malformed certificate in keyring {:?}", name))?; + match certs.entry(cert.fingerprint()) { + e @ Entry::Vacant(_) => { + e.or_insert(Some(cert)); + } + Entry::Occupied(mut e) => { + let e = e.get_mut(); + let curr = e.take().unwrap(); + *e = Some(curr.merge_public_and_secret(cert) + .expect("Same certificate")); + } + } + } + } + } else { + for cert in CertParser::from_reader(io::stdin())? { + let cert = cert.context("Malformed certificate in keyring")?; + match certs.entry(cert.fingerprint()) { + e @ Entry::Vacant(_) => { + e.or_insert(Some(cert)); + } + Entry::Occupied(mut e) => { + let e = e.get_mut(); + let curr = e.take().unwrap(); + *e = Some(curr.merge_public_and_secret(cert) + .expect("Same certificate")); + } + } + } + } + + for (_, cert) in certs.iter_mut() { + cert.take().unwrap().as_tsk().serialize(output)?; + } + + Ok(()) +} + +/// Sanitizes a string to a safe filename fragment. +fn to_filename_fragment<S: AsRef<str>>(s: S) -> Option<String> { + let mut r = String::with_capacity(s.as_ref().len()); + + s.as_ref().chars().filter_map(|c| match c { + '/' | ':' | '\\' => None, + c if c.is_ascii_whitespace() => None, + c if c.is_ascii() => Some(c), + _ => None, + }).for_each(|c| r.push(c)); + + if r.len() > 0 { + Some(r) + } else { + None + } +} |