summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjuga <juga@sequoia-pgp.org>2019-06-01 17:55:17 +0000
committerjuga <juga@sequoia-pgp.org>2019-06-11 16:38:42 +0000
commit023dda8b13aa159afc697a5afa7f2a684ec7de70 (patch)
treed53975015056257baf35325c6a5c1f4de79172b1
parent3bcc629bea7b926cb1eaf3fc97b18399779de3a5 (diff)
openpgp: Add module to enarmor TPKs with headers
Part of #213.
-rw-r--r--openpgp/src/armor.rs2
-rw-r--r--openpgp/src/tpk/armor.rs234
-rw-r--r--openpgp/src/tpk/mod.rs1
3 files changed, 236 insertions, 1 deletions
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<char> = 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<char> = 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@walfield.org>",
+ "Neal H. Walfield <neal@gnupg.org>",
+ "Neal H. Walfield <neal@pep-project.org>",
+ "Neal H. Walfield <neal@pep.foundation>",
+ "Neal H. Walfield <neal@sequoia-pgp.org>",
+ "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<char>`
+ 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;