summaryrefslogtreecommitdiffstats
path: root/sq/src/commands/keyring.rs
diff options
context:
space:
mode:
authorNeal H. Walfield <neal@pep.foundation>2021-01-22 09:38:40 +0100
committerNeal H. Walfield <neal@pep.foundation>2021-01-22 09:38:40 +0100
commiteb0b42f5933772652b8e66fd7a853495b8efcd1e (patch)
treee42ae49c74b07c204bd323207f9a58e1dccb9a13 /sq/src/commands/keyring.rs
parente5af90ca2a9ca0552d2b7c21384d6faa51323224 (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.rs318
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
+ }
+}