diff options
author | Justus Winter <justus@sequoia-pgp.org> | 2020-02-06 17:46:56 +0100 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2020-02-06 18:01:33 +0100 |
commit | 6953b4f9902f5f524e92a15518d0132fb662704a (patch) | |
tree | a02b4e656196a674146709ad42f32f4b295dcac3 /autocrypt | |
parent | 2bd001a81fa53e4f3cbc972f6354c8e3e510d1c7 (diff) |
autocrypt: New crate.
- Move the autocrypt-related functionality to a new crate.
- Fixes #424.
Diffstat (limited to 'autocrypt')
-rw-r--r-- | autocrypt/Cargo.toml | 25 | ||||
-rw-r--r-- | autocrypt/README.md | 12 | ||||
-rw-r--r-- | autocrypt/src/cert.rs | 83 | ||||
-rw-r--r-- | autocrypt/src/lib.rs | 1111 | ||||
-rw-r--r-- | autocrypt/src/serialize.rs | 25 | ||||
-rw-r--r-- | autocrypt/tests/data/README.txt | 2 | ||||
-rw-r--r-- | autocrypt/tests/data/setup-message.txt | 135 | ||||
-rw-r--r-- | autocrypt/tests/data/testy-private.pgp | bin | 0 -> 2540 bytes | |||
-rw-r--r-- | autocrypt/tests/data/testy.pgp | bin | 0 -> 1238 bytes |
9 files changed, 1393 insertions, 0 deletions
diff --git a/autocrypt/Cargo.toml b/autocrypt/Cargo.toml new file mode 100644 index 00000000..dc91fd2f --- /dev/null +++ b/autocrypt/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "sequoia-autocrypt" +description = "Autocrypt support" +version = "0.13.0" +authors = [ + "Justus Winter <justus@sequoia-pgp.org>", + "Kai Michaelis <kai@sequoia-pgp.org>", + "Neal H. Walfield <neal@sequoia-pgp.org>", +] +documentation = "https://docs.sequoia-pgp.org/0.13.0/sequoia_autocrypt" +homepage = "https://sequoia-pgp.org/" +repository = "https://gitlab.com/sequoia-pgp/sequoia" +readme = "README.md" +keywords = ["autocrypt", "mua", "opportunistic", "mail", "encryption"] +categories = ["cryptography", "authentication", "email"] +license = "GPL-2.0-or-later" +edition = "2018" + +[badges] +gitlab = { repository = "sequoia-pgp/sequoia" } +maintenance = { status = "actively-developed" } + +[dependencies] +sequoia-openpgp = { path = "../openpgp", version = "0.13" } +base64 = "0.10.1" diff --git a/autocrypt/README.md b/autocrypt/README.md new file mode 100644 index 00000000..3a502ea3 --- /dev/null +++ b/autocrypt/README.md @@ -0,0 +1,12 @@ +# sequoia-autocrypt + +This module deals with Autocrypt encoded data (see the [Autocrypt +Spec]). + + [Autocrypt Spec]: https://autocrypt.org/level1.html#openpgp-based-key-data + +# Scope + +This implements low-level functionality like encoding and decoding of +Autocrypt headers and setup messages. Note: Autocrypt is more than +just headers; it requires tight integration with the MUA. diff --git a/autocrypt/src/cert.rs b/autocrypt/src/cert.rs new file mode 100644 index 00000000..772a3f6e --- /dev/null +++ b/autocrypt/src/cert.rs @@ -0,0 +1,83 @@ +use sequoia_openpgp as openpgp; +use openpgp::packet; +use openpgp::cert::{ + CertBuilder, + CipherSuite, +}; +use openpgp::types::{ + KeyFlags, +}; + +use super::{ + Autocrypt, +}; + +/// Generates a key compliant to +/// [Autocrypt](https://autocrypt.org/). +/// +/// If no version is given the latest one is used. +/// +/// The autocrypt specification requires a UserID. However, +/// because it can be useful to add the UserID later, it is +/// permitted to be none. +pub fn cert_builder<'a, V, U>(version: V, userid: Option<U>) + -> CertBuilder + where V: Into<Option<Autocrypt>>, + U: Into<packet::UserID> +{ + let builder = CertBuilder::new() + .set_cipher_suite(match version.into().unwrap_or_default() { + Autocrypt::V1 => CipherSuite::RSA3k, + Autocrypt::V1_1 => CipherSuite::Cv25519, + }) + .set_primary_key_flags( + KeyFlags::default() + .set_certification(true) + .set_signing(true)) + .add_subkey( + KeyFlags::default() + .set_transport_encryption(true) + .set_storage_encryption(true), + None); + + if let Some(userid) = userid { + builder.add_userid(userid.into()) + } else { + builder + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use openpgp::types::PublicKeyAlgorithm; + + #[test] + fn autocrypt_v1() { + let (cert1, _) = cert_builder(Autocrypt::V1, Some("Foo")) + .generate().unwrap(); + assert_eq!(cert1.primary_key().pk_algo(), + PublicKeyAlgorithm::RSAEncryptSign); + assert_eq!(cert1.keys().subkeys().next().unwrap().key().pk_algo(), + PublicKeyAlgorithm::RSAEncryptSign); + assert_eq!(cert1.userids().count(), 1); + } + + #[test] + fn autocrypt_v1_1() { + let (cert1, _) = cert_builder(Autocrypt::V1_1, Some("Foo")) + .generate().unwrap(); + assert_eq!(cert1.primary_key().pk_algo(), + PublicKeyAlgorithm::EdDSA); + assert_eq!(cert1.keys().subkeys().next().unwrap().key().pk_algo(), + PublicKeyAlgorithm::ECDH); + match cert1.keys().subkeys().next().unwrap().key().mpis() { + openpgp::crypto::mpis::PublicKey::ECDH { + curve: openpgp::types::Curve::Cv25519, .. + } => (), + m => panic!("unexpected mpi: {:?}", m), + } + assert_eq!(cert1.userids().count(), 1); + } +} diff --git a/autocrypt/src/lib.rs b/autocrypt/src/lib.rs new file mode 100644 index 00000000..95e6b297 --- /dev/null +++ b/autocrypt/src/lib.rs @@ -0,0 +1,1111 @@ +//! Autocrypt. +//! +//! This module deals with Autocrypt encoded data (see the [Autocrypt +//! Spec]). +//! +//! [Autocrypt Spec]: https://autocrypt.org/level1.html#openpgp-based-key-data +//! +//! # Scope +//! +//! This implements low-level functionality like encoding and decoding +//! of Autocrypt headers and setup messages. Note: Autocrypt is more +//! than just headers; it requires tight integration with the MUA. + +use base64; + +use std::io; +use std::io::prelude::*; +use std::io::BufReader; +use std::path::Path; +use std::fs::File; +use std::str; + +use sequoia_openpgp as openpgp; +use openpgp::armor; +use openpgp::Error; +pub use openpgp::Result; +use openpgp::Packet; +use openpgp::packet::SKESK; +use openpgp::Cert; +use openpgp::cert::components::Amalgamation; +use openpgp::parse::{ + Parse, + PacketParserResult, PacketParser, +}; +use openpgp::serialize::Serialize; +use openpgp::serialize::stream::{ + Message, LiteralWriter, Encryptor, +}; +use openpgp::crypto::Password; +use openpgp::policy::Policy; + +mod cert; +pub use cert::cert_builder; +mod serialize; + +/// Version of Autocrypt to use. `Autocrypt::default()` always returns the +/// latest version. +pub enum Autocrypt { + /// Autocrypt <= 1.0.1 + V1, + /// Autocrypt version 1.1 (January 2019) + V1_1, +} + +impl Default for Autocrypt { + fn default() -> Self { Autocrypt::V1_1 } +} + +/// An autocrypt header attribute. +#[derive(Debug, PartialEq)] +pub struct Attribute { + /// Whether the attribute is critical. + pub critical: bool, + /// The attribute's name. + /// + /// If the attribute is not critical, the leading underscore has + /// been stripped. + pub key: String, + /// The attribute's value. + pub value: String, +} + +/// Whether the data comes from an "Autocrypt" or "Autocrypt-Gossip" +/// header. +#[derive(Debug, PartialEq)] +pub enum AutocryptHeaderType { + /// An "Autocrypt" header. + Sender, + /// An "Autocrypt-Gossip" header. + Gossip, +} + +/// A parsed Autocrypt header. +#[derive(Debug, PartialEq)] +pub struct AutocryptHeader { + /// Whether this is an "Autocrypt" or "Autocrypt-Gossip" header. + pub header_type: AutocryptHeaderType, + + /// The parsed key data. + pub key: Option<Cert>, + + /// All attributes. + pub attributes: Vec<Attribute>, +} + +impl AutocryptHeader { + fn empty(header_type: AutocryptHeaderType) -> Self { + AutocryptHeader { + header_type: header_type, + key: None, + attributes: Vec::new(), + } + } + + /// Creates a new "Autocrypt" header. + pub fn new_sender<'a, P>(policy: &dyn Policy, + cert: &Cert, addr: &str, prefer_encrypt: P) + -> Result<Self> + where P: Into<Option<&'a str>> + { + // Minimize Cert. + let mut acc = Vec::new(); + + // The primary key and the most recent selfsig. + let primary = cert.primary_key().bundle(); + acc.push(primary.key().clone().mark_role_primary().into()); + primary.self_signatures().iter().take(1) + .for_each(|s| acc.push(s.clone().into())); + + // The subkeys and the most recent selfsig. + for skb in cert.keys().subkeys() { + // Skip if revoked. + if ! skb.self_revocations().is_empty() + || ! skb.other_revocations().is_empty() + { + continue; + } + + let k = skb.key().clone(); + acc.push(k.into()); + skb.self_signatures().iter().take(1) + .for_each(|s| acc.push(s.clone().into())); + } + + // The UserIDs matching ADDR. + for uidb in cert.userids().with_policy(policy, None) { + // XXX: Fix match once we have the rfc2822-name-addr. + if let Ok(Some(a)) = uidb.userid().email() { + if &a == addr { + acc.push(uidb.userid().clone().into()); + acc.push(uidb.binding_signature().clone().into()); + } else { + // Address is not matching. + continue; + } + } else { + // Malformed UserID. + continue; + } + } + + let cleaned_cert = Cert::from_packet_pile(acc.into())?; + + Ok(AutocryptHeader { + header_type: AutocryptHeaderType::Sender, + key: Some(cleaned_cert), + attributes: vec![ + Attribute { + critical: true, + key: "addr".into(), + value: addr.into(), + }, + Attribute { + critical: true, + key: "prefer-encrypt".into(), + value: prefer_encrypt.into() + .unwrap_or("nopreference").into(), + }, + ], + }) + } + + /// Looks up an attribute. + pub fn get(&self, key: &str) -> Option<&Attribute> { + for a in &self.attributes { + if a.key == key { + return Some(a); + } + } + + None + } +} + +/// A set of parsed Autocrypt headers. +#[derive(Debug, PartialEq)] +pub struct AutocryptHeaders { + /// The value in the from header. + pub from: Option<String>, + + /// Any autocrypt headers. + pub headers: Vec<AutocryptHeader>, +} + +impl AutocryptHeaders { + fn empty() -> Self { + AutocryptHeaders { + from: None, + headers: Vec::new(), + } + } + + fn from_lines<I: Iterator<Item = io::Result<String>>>(mut lines: I) + -> Result<Self> + { + let mut headers = AutocryptHeaders::empty(); + + let mut next_line = lines.next(); + while let Some(line) = next_line { + // Return any error. + let mut line = line?; + + if line == "" { + // End of headers. + break; + } + + next_line = lines.next(); + + // If the line is folded (a line break was inserted in + // front of whitespace (either a space (0x20) or a + // horizontal tab (0x09)), then unfold it. + // + // See https://tools.ietf.org/html/rfc5322#section-2.2.3 + while let Some(Ok(nl)) = next_line { + if nl.len() > 0 && (&nl[0..1] == " " || &nl[0..1] == "\t") { + line.push_str(&nl[..]); + next_line = lines.next(); + } else { + // Put it back. + next_line = Some(Ok(nl)); + break; + } + } + + const AUTOCRYPT : &str = "Autocrypt: "; + const FROM : &str = "From: "; + + if line.starts_with(FROM) { + headers.from + = Some(line[FROM.len()..].trim_matches(' ').into()); + } else if line.starts_with(AUTOCRYPT) { + let ac_value = &line[AUTOCRYPT.len()..]; + + let mut header = AutocryptHeader::empty( + AutocryptHeaderType::Sender); + + for pair in ac_value.split(';') { + let pair = pair + .splitn(2, |c| c == '=') + .collect::<Vec<&str>>(); + + let (key, value) : (String, String) = if pair.len() == 1 { + // No value... + (pair[0].trim_matches(' ').into(), "".into()) + } else { + (pair[0].trim_matches(' ').into(), + pair[1].trim_matches(' ').into()) + }; + + if key == "keydata" { + if let Ok(decoded) = base64::decode( + &value.replace(" ", "")[..]) { + if let Ok(cert) = Cert::from_bytes(&decoded[..]) { + header.key = Some(cert); + } + } + } + + let critical = key.len() >= 1 && &key[0..1] == "_"; + header.attributes.push(Attribute { + critical: critical, + key: if critical { + key[1..].to_string() + } else { + key + }, + value: value, + }); + } + + headers.headers.push(header); + } + } + + return Ok(headers) + } + + /// Parses an autocrypt header. + /// + /// `data` should be all of a mail's headers. + pub fn from_bytes(data: &[u8]) -> Result<Self> { + let lines = BufReader::new(io::Cursor::new(data)).lines(); + Self::from_lines(lines) + } + + /// Parses an autocrypt header. + /// + /// `path` should name a file containing a single mail. If the + /// file is in mbox format, then only the first mail is + /// considered. + pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> { + Self::from_reader(File::open(path)?) + } + + /// Parses an autocrypt header. + /// + /// `reader` contain a single mail. If it contains multiple + /// emails, then only the first mail is considered. + pub fn from_reader<R: io::Read>(reader: R) -> Result<Self> { + Self::from_lines(BufReader::new(reader).lines()) + } +} + +/// Holds an Autocrypt Setup Message. +/// +/// An [Autocrypt Setup Message] is used to transfer a private key from +/// one device to another. +/// +/// [Autocrypt Setup Message]: +/// https://autocrypt.org/level1.html#autocrypt-setup-message +#[derive(Debug, PartialEq)] +pub struct AutocryptSetupMessage { + prefer_encrypt: Option<String>, + passcode_format: Option<String>, + passcode: Option<Password>, + // We only emit a "Passcode-Begin" header if this is set. Note: + // we don't check if this actually matches the start of the + // passcode. + passcode_begin: Option<String>, + + cert: Cert, +} + +impl AutocryptSetupMessage { + /// Creates a new Autocrypt Setup Message for the specified `Cert`. + /// + /// You can set the `prefer_encrypt` setting, which defaults to + /// "nopreference", using `set_prefer_encrypt`. + /// + /// Note: this generates a random passcode. To retrieve the + /// passcode, use the `passcode` method. + /// + /// To decode an Autocrypt Setup Message, use the `from_bytes` or + /// `from_reader` methods. + pub fn new(cert: Cert) -> Self { + AutocryptSetupMessage { + prefer_encrypt: None, + passcode: None, + passcode_format: None, + passcode_begin: None, + cert: cert, + } + } + + /// Sets the prefer encrypt header. + pub fn set_prefer_encrypt(mut self, value: &str) -> Self { + self.prefer_encrypt = Some(value.into()); + self + } + + /// Returns the prefer encrypt header. + pub fn prefer_encrypt(&self) -> Option<&str> { + self.prefer_encrypt.as_ref().map(|v| &v[..]) + } + + + /// Sets the "Passcode-Format" header. + pub fn set_passcode_format(mut self, value: &str) -> Self { + self.passcode_format = Some(value.into()); + self + } + + /// Returns the "Passcode-Format" header. + pub fn passcode_format(&self) -> Option<&str> { + self.passcode_format.as_ref().map(|v| &v[..]) + } + + + /// Sets the passcode. + pub fn set_passcode(mut self, passcode: Password) -> Self { + self.passcode = Some(passcode); + self + } + + /// Returns the ASM's passcode. + /// + /// If the passcode has not yet been generated, this returns + /// `None`. + pub fn passcode(&self) -> Option<&Password> { + self.passcode.as_ref() + } + + + /// Sets the "Passcode-Begin" header. + pub fn set_passcode_begin(mut self, value: &str) -> Self { + self.passcode_begin = Some(value.into()); + self + } + + /// Returns the "Passcode-Begin" header. + pub fn passcode_begin(&self) -> Option<&str> { + self.passcode_begin.as_ref().map(|v| &v[..]) + } + + + // Generates a new passcode in "numeric9x4" format. + fn passcode_gen() -> Password { + use openpgp::crypto::mem; + // Generate a random passcode. + + // The passcode consists of 36 digits, which encode + // approximately 119 bits of information. 120 bits = 15 + // bytes. + let mut p_as_vec = mem::Protected::from(vec![0; 15]); + openpgp::crypto::random(&mut p_as_vec[..]); + + // Turn it into a 128-bit number. + let mut p_as_u128 = 0u128; + for v in p_as_vec.iter() { + p_as_u128 = (p_as_u128 << 8) + *v as u128; + } + + // Turn it into ASCII. + let mut p : Vec<u8> = Vec::new(); + for i in 0..36 { + if i > 0 && i % 4 == 0 { + p.push('-' as u8); + } + + p.push(('0' as u8) + ((p_as_u128 as u8) % 10)); + p_as_u128 = p_as_u128 / 10; + } + + p.into() + } + + /// If there is no passcode, generates one. + fn passcode_ensure(&mut self) { + if self.passcode.is_some() { + return; + } + + let passcode = Self::passcode_gen(); + self.passcode_format = Some("numeric9x4".into()); + self.passcode_begin = passcode.map(|p| { + Some(str::from_utf8(&p[..2]).unwrap().into()) + }); + self.passcode = Some(passcode); + } + + /// Generates the Autocrypt Setup Message. + /// + /// The message is written to `w`. + pub fn serialize<W: io::Write>(&mut self, w: &mut W) -> Result<()> { + // The outer message is an ASCII-armored encoded message + // containing a single SK-ESK and a single SEIP packet. The + // SEIP packet contains a literal data packet whose content is + // the inner message. + + self.passcode_ensure(); + + let mut headers : Vec<(&str, &str)> = Vec::new(); + if let Some(ref format) = self.passcode_format { + headers.push( + (&"Passphrase-Format"[..], &format[..])); + } + if let Some(ref begin) = self.passcode_begin { + headers.push( + (&"Passphrase-Begin"[..], &begin[..])); + } + + let mut armor_writer = + armor::Writer::new(w, armor::Kind::Message, &headers[..])?; + + { + // Passphrase-Format header with value numeric9x4 + let m = Message::new(&mut armor_writer); + let w = Encryptor::with_password(m, self.passcode.clone().unwrap()) + .build()?; + + let mut w = LiteralWriter::new(w).build()?; + + // The inner message is an ASCII-armored encoded Cert. + let mut w = armor::Writer::new( + &mut w, armor::Kind::SecretKey, + &[ (&"Autocrypt-Prefer-Encrypt"[..], + self.prefer_encrypt().unwrap_or(&"nopreference"[..])) ])?; + + self.cert.as_tsk().serialize(&mut w)?; + w.finalize()?; + } + armor_writer.finalize()?; + Ok(()) + } + + + /// Parses the autocrypt setup message in `r`. + /// + /// `passcode` is the passcode used to protect the message. + pub fn from_bytes<'a>(bytes: &'a [u8]) + -> Result<AutocryptSetupMessageParser<'a>> + { + Self::from_reader(bytes) + } + + /// Parses the autocrypt setup message in `r`. + /// + /// `passcode` is the passcode used to protect the message. + pub fn from_reader<'a, R: io::Read + 'a>(r: R) + -> Result<AutocryptSetupMessageParser<'a>> { + // The outer message uses ASCII-armor. It includes a password + // hint. Hence, we need to parse it aggressively. + let mut r = armor::Reader::new( + r, armor::ReaderMode::Tolerant(Some(armor::Kind::Message))); + + // Note, it is essential that we call r.headers here so that + // we can return any error now and not in + // AutocryptSetupMessageParser::header. + let (format, begin) = { + let headers = r.headers()?; + + let format = headers.iter() + .filter_map(|(k, v)| { + if k == "Passphrase-Format" { Some(v) } else { None } + }) + .collect::<Vec<&String>>(); + let format = if format.len() > 0 { + // If there are multiple headers, then just silently take + // the first one. + Some(format[0].clone()) + } else { + None + }; + + let begin = headers.iter() + .filter_map(|(k, v)| { + if k == "Passphrase-Begin" { Some(v) } else { None } + }) + .collect::<Vec<&String>>(); + let begin = if begin.len() > 0 { + // If there are multiple headers, then just silently take + // the first one. + Some(begin[0].clone()) + } else { + None + }; + + (format, begin) + }; + + // Get the first packet, which is the SK-ESK packet. + + let mut ppr = PacketParser::from_reader(r)?; + + // The outer message consists of exactly three packets: a + // SK-ESK and a SEIP packet, which contains a Literal data + // packet. + + let pp = if let PacketParserResult::Some(pp) = ppr { + pp + } else { + return Err( + Error::MalformedMessage( + "Premature EOF: expected an SK-ESK, encountered EOF".into()) + .into()); + }; + + let (packet, ppr_) = pp.next()?; + ppr = ppr_; + + let skesk = match packet { + Packet::SKESK(skesk) => skesk, + p => return Err( + Error::MalformedMessage( + format!("Expected a SKESK packet, found a {}", p.tag()) + .into()) + .into()), + }; + + let pp = match ppr { + PacketParserResult::EOF(_) => + return Err( + Error::MalformedMessage( + "Pre-mature EOF after reading SK-ESK packet".into()) + .into()), + PacketParserResult::Some(pp) => { + match pp.packet { + Packet::SEIP(_) => (), + ref p => return Err( + Error::MalformedMessage( + format!("Expected a SEIP packet, found a {}", + p.tag()) + .into()) + .into()), + } + + pp + } + }; + + Ok(AutocryptSetupMessageParser { + passcode_format: format, + passcode_begin: begin, + skesk: skesk, + pp: pp, + passcode: None, + }) + } + + /// Returns the Cert consuming the `AutocryptSetupMessage` in the + /// process. + pub fn into_cert(self) -> Cert { + self.cert + } +} + +/// A Parser for an `AutocryptSetupMessage`. +pub struct AutocryptSetupMessageParser<'a> { + passcode_format: Option<String>, + passcode_begin: Option<String>, + skesk: SKESK, + pp: PacketParser<'a>, + passcode: Option<Password>, +} + +impl<'a> AutocryptSetupMessageParser<'a> { + /// Returns the "Passcode-Format" header. + pub fn passcode_format(&self) -> Option<&str> { + self.passcode_format.as_ref().map(|v| &v[..]) + } + + /// Returns the "Passcode-Begin" header. + pub fn passcode_begin(&self) -> Option<&str> { + self.passcode_begin.as_ref().map(|v| &v[..]) + } + + /// Tries to decrypt the message. + /// + /// On success, follow up with + /// `AutocryptSetupMessageParser::parse()` to extract the + /// `AutocryptSetupMessage`. + pub fn decrypt(&mut self, passcode: &Password) -> Result<()> { + if self.pp.decrypted() { + return Err( + Error::InvalidOperation("Already decrypted".into()).into()); + } + + let (algo, key) = se |