From 023dda8b13aa159afc697a5afa7f2a684ec7de70 Mon Sep 17 00:00:00 2001 From: juga Date: Sat, 1 Jun 2019 17:55:17 +0000 Subject: openpgp: Add module to enarmor TPKs with headers Part of #213. --- openpgp/src/armor.rs | 2 +- openpgp/src/tpk/armor.rs | 234 +++++++++++++++++++++++++++++++++++++++++++++++ openpgp/src/tpk/mod.rs | 1 + 3 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 openpgp/src/tpk/armor.rs diff --git a/openpgp/src/armor.rs b/openpgp/src/armor.rs index c6213bb9..16b2f965 100644 --- a/openpgp/src/armor.rs +++ b/openpgp/src/armor.rs @@ -43,7 +43,7 @@ use serialize::SerializeInto; /// than 76 characters each (see (see [RFC 4880, section /// 6.3](https://tools.ietf.org/html/rfc4880#section-6.3). GnuPG uses /// 64. -const LINE_LENGTH: usize = 64; +pub(crate) const LINE_LENGTH: usize = 64; const LINE_ENDING: &str = "\n"; diff --git a/openpgp/src/tpk/armor.rs b/openpgp/src/tpk/armor.rs new file mode 100644 index 00000000..067ac381 --- /dev/null +++ b/openpgp/src/tpk/armor.rs @@ -0,0 +1,234 @@ +//! Module to serialize and enarmor a TPK and add informative headers. +use std::io; +use std::str; + +use armor; +use Result; +use RevocationStatus; +use serialize::Serialize; +use TPK; + + +/// Whether or not a character is printable. +pub(crate) fn is_printable(c: &char) -> bool { + // c.is_ascii_alphanumeric || c.is_whitespace || c.is_ascii_punctuation + // would exclude any utf8 character, so it seems that to obtain all + // printable chars, it works just excluding the control chars. + !c.is_control() && !c.is_ascii_control() +} + + +/// A `TPK` to be armored and serialized. +pub struct Encoder<'a> { + tpk: &'a TPK, +} + + +impl<'a> Encoder<'a> { + /// Returns a new Encoder to enarmor and serialize a `TPK`. + pub fn new(tpk: &'a TPK) -> Self { + Self { + tpk: tpk, + } + } +} + +impl<'a> Serialize for Encoder<'a> { + /// Enarmor and serialize the `TPK` including headers. + fn serialize(&self, o: &mut dyn io::Write) -> Result<()> { + let length_value = armor::LINE_LENGTH - "Comment: ".len(); + // Create a header per userid. + let mut headers: Vec<(String, String)> = self.tpk.userids() + // Ignore revoked userids. + .filter_map(|uidb| { + if let RevocationStatus::Revoked(_) = uidb.revoked(None) { + None + } else { + Some(uidb) + } + // Ignore userids not "alive". + }).filter_map(|uidb| { + if uidb.binding_signature()?.signature_alive() { + Some(uidb) + } else { + None + } + // Ignore userids with non-printable characters. + }).filter_map(|uidb| { + let value = str::from_utf8(uidb.userid().value()).ok()?; + for c in value.chars().take(length_value) { + if !is_printable(&c){ + return None; + } + } + // Make sure the line length does not exceed armor::LINE_LENGTH + let header: String = value.chars().take(length_value) + .collect(); + Some(("Comment".into(), header)) + }).collect(); + + // Add the fingerprint to the headers + let fpr = self.tpk.fingerprint().to_string(); + headers.push(("Comment".into(), fpr)); + + // Convert the Vec<(String, String)> into Vec<(&str, &str)> + // `iter_into` can not be used here because will take ownership and + // what is needed is the reference. + let headers: Vec<_> = headers.iter() + .map(|&(ref x, ref y)| (&x[..], &y[..])) + .collect(); + + let mut w = armor::Writer::new(o, armor::Kind::PublicKey, &headers)?; + self.tpk.serialize(&mut w) + } +} + + +#[cfg(test)] +mod tests { + use armor::{Kind, Reader, ReaderMode}; + use tpk::{TPKParser, TPKBuilder}; + use Message; + use parse::Parse; + + use super::*; + + #[test] + fn is_printable_succeed() { + let chars: Vec = vec![ + 'a', 'z', 'A', 'Z', '1', '9', '0', + '|', '!', '#', '$', '%', '^', '&', '*', '-', '+', '/', + // The following unicode characters were taken from: + // https://doc.rust-lang.org/std/primitive.char.html + 'é', 'ß', 'ℝ', '💣', '❤', '東', '京', '𝕊', '💝', 'δ', + 'Δ', '中', '越', '٣', '7', '৬', '¾', '①', 'K', + 'و', '藏', '山', 'I', 'ï', 'İ', 'i' + ]; + for c in &chars { + assert!(is_printable(c)); + } + } + + #[test] + fn is_printable_fail() { + let chars: Vec = vec![ + '\n', 0x1b_u8.into(), + // U+009C, STRING TERMINATOR + 'œ' + ]; + for c in &chars { + assert!(!is_printable(c)); + } + } + + #[test] + fn serialize_succeed() { + let tpk = TPK::from_bytes(include_bytes!( + "../../tests/data/keys/neal.pgp")) + .unwrap(); + + // Enarmor the TPK. + let mut buffer = Vec::new(); + Encoder::new(&tpk) + .serialize(&mut buffer) + .unwrap(); + + // Parse the armor. + let mut cursor = io::Cursor::new(&buffer); + let mut reader = Reader::new( + &mut cursor, ReaderMode::Tolerant(Some(Kind::PublicKey))); + + // Extract the headers. + let mut headers: Vec<&str> = reader.headers() + .unwrap() + .into_iter() + .map(|header| { + assert_eq!(&header.0[..], "Comment"); + &header.1[..]}) + .collect(); + headers.sort(); + + // Ensure the headers are correct + let mut expected_headers = [ + "Neal H. Walfield ", + "Neal H. Walfield ", + "Neal H. Walfield ", + "Neal H. Walfield ", + "Neal H. Walfield ", + "8F17 7771 18A3 3DDA 9BA4 8E62 AACB 3243 6300 52D9"]; + expected_headers.sort(); + + assert_eq!(&expected_headers[..], &headers[..]); + } + + #[test] + fn serialize_length_succeed() { + let length_value = armor::LINE_LENGTH - "Comment: ".len(); + + // Create userids one character longer than the size allowed in the + // header and expect headers with the correct length. + // 1 byte characer + // Can not use `to_string` here because not such method for + //`std::vec::Vec` + let userid1: String = vec!['a'; length_value + 1].into_iter() + .collect(); + let userid1_expected: String = vec!['a'; length_value].into_iter() + .collect(); + // 2 bytes character. + let userid2: String = vec!['ß'; length_value + 1].into_iter() + .collect(); + let userid2_expected: String = vec!['ß'; length_value].into_iter() + .collect(); + // 3 bytes character. + let userid3: String = vec!['€'; length_value + 1].into_iter() + .collect(); + let userid3_expected: String = vec!['€'; length_value].into_iter() + .collect(); + // 4 bytes character. + let userid4: String = vec!['𐍈'; length_value + 1].into_iter() + .collect(); + let userid4_expected: String = vec!['𐍈'; length_value].into_iter() + .collect(); + let mut userid5 = vec!['a'; length_value]; + userid5[length_value-1] = 'ß'; + let userid5: String = userid5.into_iter().collect(); + + // Create a TPK with the userids. + let (tpk, _) = TPKBuilder::autocrypt(None, Some(&userid1[..])) + .add_userid(&userid2[..]) + .add_userid(&userid3[..]) + .add_userid(&userid4[..]) + .add_userid(&userid5[..]) + .generate() + .unwrap(); + + // Enarmor the TPK. + let mut buffer = Vec::new(); + Encoder::new(&tpk) + .serialize(&mut buffer) + .unwrap(); + + // Parse the armor. + let mut cursor = io::Cursor::new(&buffer); + let mut reader = Reader::new( + &mut cursor, ReaderMode::Tolerant(Some(Kind::PublicKey))); + + // Extract the headers. + let mut headers: Vec<&str> = reader.headers() + .unwrap() + .into_iter() + .map(|header| { + assert_eq!(&header.0[..], "Comment"); + &header.1[..]}) + .collect(); + headers.sort(); + + // Ignore the first header since it is the fingerprint + let mut headers_iter = headers[1..].into_iter(); + assert_eq!(headers_iter.next().unwrap(), &userid1_expected); + assert_eq!(headers_iter.next().unwrap(), &userid5); + assert_eq!(headers_iter.next().unwrap(), &userid2_expected); + assert_eq!(headers_iter.next().unwrap(), &userid3_expected); + assert_eq!(headers_iter.next().unwrap(), &userid4_expected); + } +} diff --git a/openpgp/src/tpk/mod.rs b/openpgp/src/tpk/mod.rs index 30fd6676..01630127 100644 --- a/openpgp/src/tpk/mod.rs +++ b/openpgp/src/tpk/mod.rs @@ -36,6 +36,7 @@ use parse::{Parse, PacketParserResult, PacketParser}; use serialize::SerializeInto; use constants::ReasonForRevocation; +pub mod armor; mod lexer; mod grammar; mod builder; -- cgit v1.2.3