diff options
author | Justus Winter <justus@sequoia-pgp.org> | 2018-10-09 17:50:22 +0200 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2018-10-11 16:27:23 +0200 |
commit | 12da5b662d965abeed98b4f1b0f74b6ab887cefe (patch) | |
tree | 8365e143ba859f175c27405449a69eda3499f65a | |
parent | 99fbdcf9d73a469638e5d035e58e841451ff3579 (diff) |
openpgp: Add support for the AEAD encrypted data packet.
- This adds a new packet type, and enough infrastructure to decrypt
messages encrypted using AEAD.
-rw-r--r-- | openpgp/src/aead.rs | 427 | ||||
-rw-r--r-- | openpgp/src/lib.rs | 4 | ||||
-rw-r--r-- | openpgp/src/message/grammar.lalrpop | 14 | ||||
-rw-r--r-- | openpgp/src/message/lexer.rs | 2 | ||||
-rw-r--r-- | openpgp/src/message/mod.rs | 34 | ||||
-rw-r--r-- | openpgp/src/packet/aed.rs | 167 | ||||
-rw-r--r-- | openpgp/src/packet/mod.rs | 5 | ||||
-rw-r--r-- | openpgp/src/parse/parse.rs | 260 | ||||
-rw-r--r-- | openpgp/src/parse/stream.rs | 2 | ||||
-rw-r--r-- | openpgp/src/serialize/mod.rs | 32 | ||||
-rw-r--r-- | openpgp/tests/data/messages/aed/msg-aes128-eax-chunk-size-134217728-password-123.pgp | bin | 0 -> 5314 bytes | |||
-rw-r--r-- | openpgp/tests/data/messages/aed/msg-aes128-eax-chunk-size-64-password-123.pgp | bin | 0 -> 6611 bytes | |||
-rw-r--r-- | tool/src/commands/dump.rs | 9 |
13 files changed, 870 insertions, 86 deletions
diff --git a/openpgp/src/aead.rs b/openpgp/src/aead.rs index 0058d4b2..83bd0830 100644 --- a/openpgp/src/aead.rs +++ b/openpgp/src/aead.rs @@ -1,11 +1,21 @@ +use std::cmp; +use std::fmt; +use std::io::{self, Read}; + use nettle::{aead, cipher}; +use buffered_reader::BufferedReader; +use buffered_reader::BufferedReaderGeneric; -use Error; -use Result; use constants::{ AEADAlgorithm, SymmetricAlgorithm, }; +use conversions::{ + write_be_u64, +}; +use Error; +use Result; +use SessionKey; impl AEADAlgorithm { /// Returns the digest size of the AEAD algorithm. @@ -63,3 +73,416 @@ impl AEADAlgorithm { } } } + +const AD_PREFIX_LEN: usize = 5; + +/// A `Read`er for decrypting AEAD-encrypted data. +pub struct Decryptor<R: io::Read> { + // The encrypted data. + source: R, + + cipher: SymmetricAlgorithm, + aead: AEADAlgorithm, + key: SessionKey, + iv: Box<[u8]>, + ad: [u8; AD_PREFIX_LEN + 8 + 8], + + digest_size: usize, + chunk_size: usize, + chunk_index: u64, + bytes_decrypted: u64, + // Up to a chunk of unread data. + buffer: Vec<u8>, +} + +impl<R: io::Read> Decryptor<R> { + /// Instantiate a new AEAD decryptor. + /// + /// `source` is the source to wrap. + pub fn new(version: u8, cipher: SymmetricAlgorithm, aead: AEADAlgorithm, + chunk_size: usize, iv: &[u8], key: &SessionKey, source: R) + -> Result<Self> { + Ok(Decryptor { + source: source, + cipher: cipher, + aead: aead, + key: key.clone(), + iv: Vec::from(iv).into_boxed_slice(), + ad: [ + // Prefix. + 0xd4, version, cipher.into(), aead.into(), + chunk_size.trailing_zeros() as u8 - 6, + // Chunk index. + 0, 0, 0, 0, 0, 0, 0, 0, + // Message size. + 0, 0, 0, 0, 0, 0, 0, 0, + ], + digest_size: aead.digest_size()?, + chunk_size: chunk_size, + chunk_index: 0, + bytes_decrypted: 0, + buffer: Vec::with_capacity(chunk_size), + }) + } + + fn hash_associated_data(&mut self, aead: &mut Box<aead::Aead>, + final_digest: bool) { + // Prepare the associated data. + write_be_u64(&mut self.ad[AD_PREFIX_LEN..AD_PREFIX_LEN + 8], + self.chunk_index); + + if final_digest { + write_be_u64(&mut self.ad[AD_PREFIX_LEN + 8..], + self.bytes_decrypted); + aead.update(&self.ad); + } else { + aead.update(&self.ad[..AD_PREFIX_LEN + 8]); + } + } + + fn make_aead(&mut self) -> Result<Box<aead::Aead>> { + // The chunk index is XORed into the IV. + let mut chunk_index_be64 = vec![0u8; 8]; + write_be_u64(&mut chunk_index_be64, self.chunk_index); + + match self.aead { + AEADAlgorithm::EAX => { + // The nonce for EAX mode is computed by treating the + // starting initialization vector as a 16-octet, + // big-endian value and exclusive-oring the low eight + // octets of it with the chunk index. + let iv_len = self.iv.len(); + for (i, o) in &mut self.iv[iv_len - 8..].iter_mut() + .enumerate() + { + // The lower eight octets of the associated data + // are the big endian representation of the chunk + // index. + *o ^= chunk_index_be64[i]; + } + + // Instantiate the AEAD cipher. + let aead = self.aead.context(self.cipher, &self.key, &self.iv)?; + + // Restore the IV. + for (i, o) in &mut self.iv[iv_len - 8..].iter_mut() + .enumerate() + { + *o ^= chunk_index_be64[i]; + } + + Ok(aead) + } + _ => Err(Error::UnsupportedAEADAlgorithm(self.aead).into()), + } + } + + fn read_helper(&mut self, plaintext: &mut [u8]) -> Result<usize> { + let mut pos = 0; + + // 1. Copy any buffered data. + if self.buffer.len() > 0 { + let to_copy = cmp::min(self.buffer.len(), plaintext.len()); + &plaintext[..to_copy].copy_from_slice(&self.buffer[..to_copy]); + self.buffer.drain(..to_copy); + pos = to_copy; + } + + if pos == plaintext.len() { + return Ok(pos); + } + + // 2. Decrypt as many whole chunks as `plaintext` can hold. + let n_chunks = (plaintext.len() - pos) / self.chunk_size; + let chunk_digest_size = self.chunk_size + self.digest_size; + let mut to_copy = n_chunks * self.chunk_size; + let to_read = n_chunks * chunk_digest_size; + + let mut ciphertext = Vec::new(); + let result = (&mut self.source).take(to_read as u64) + .read_to_end(&mut ciphertext); + let short_read; + match result { + Ok(amount) => { + if to_read != 0 && amount == 0 { + // Exhausted source. + return Ok(pos); + } + + short_read = amount < to_copy; + to_copy = amount; + ciphertext.truncate(to_copy); + }, + // We encountered an error, but we did read some. + Err(_) if pos > 0 => return Ok(pos), + Err(e) => return Err(e.into()), + } + + // Buffer to hold digests. + let mut digest = vec![0u8; self.digest_size]; + + // At the end of the stream, there is an additional tag. Be + // careful not to consume this tag. + let ciphertext_end = if short_read { + ciphertext.len() - self.digest_size + } else { + ciphertext.len() + }; + + for chunk in (&ciphertext[..ciphertext_end]).chunks(chunk_digest_size) { + let mut aead = self.make_aead()?; + + // Digest the associated data. + self.hash_associated_data(&mut aead, false); + + // Decrypt the chunk. + aead.decrypt( + &mut plaintext[pos..pos + chunk.len() - self.digest_size], + &chunk[..chunk.len() - self.digest_size]); + self.bytes_decrypted += (chunk.len() - self.digest_size) as u64; + + // Check digest. + aead.digest(&mut digest); + if &digest[..] != &chunk[chunk.len() - self.digest_size..] { + return Err(Error::ManipulatedMessage.into()); + } + + // Increase index, update position in plaintext. + self.chunk_index += 1; + pos += chunk.len() - self.digest_size; + } + + if short_read { + // We read the whole ciphertext, now check the final digest. + let mut aead = self.make_aead()?; + self.hash_associated_data(&mut aead, true); + let mut nada = [0; 0]; + aead.decrypt(&mut nada, b""); + aead.digest(&mut digest); + if &digest[..] != &ciphertext[ciphertext_end..] { + return Err(Error::ManipulatedMessage.into()); + } + } + + if short_read || pos == plaintext.len() { + return Ok(pos); + } + + // 3. The last bit is a partial chunk. Buffer it. + let mut to_copy = plaintext.len() - pos; + assert!(0 < to_copy); + assert!(to_copy < self.chunk_size); + + let mut ciphertext = Vec::new(); + let result = (&mut self.source).take(chunk_digest_size as u64) + .read_to_end(&mut ciphertext); + let short_read; + match result { + Ok(amount) => { + if amount == 0 { + return Ok(pos); + } + + short_read = amount < chunk_digest_size; + + // Make sure `ciphertext` is not larger than the + // amount of data that was actually read. + ciphertext.truncate(amount); + + // Make sure we don't read more than is available. + to_copy = cmp::min(to_copy, + ciphertext.len() - self.digest_size + - if short_read { + self.digest_size + } else { + 0 + }); + }, + // We encountered an error, but we did read some. + Err(_) if pos > 0 => return Ok(pos), + Err(e) => return Err(e.into()), + } + assert!(ciphertext.len() <= self.chunk_size + self.digest_size); + + let mut aead = self.make_aead()?; + + // Digest the associated data. + self.hash_associated_data(&mut aead, false); + + // At the end of the stream, there is an additional tag. Be + // careful not to consume this tag. + let ciphertext_end = if short_read { + ciphertext.len() - self.digest_size + } else { + ciphertext.len() + }; + + while self.buffer.len() < ciphertext_end - self.digest_size { + self.buffer.push(0u8); + } + self.buffer.truncate(ciphertext_end - self.digest_size); + + // Decrypt the chunk. + aead.decrypt(&mut self.buffer, + &ciphertext[..ciphertext_end - self.digest_size]); + self.bytes_decrypted += (ciphertext_end - self.digest_size) as u64; + + // Check digest. + aead.digest(&mut digest); + if &digest[..] + != &ciphertext[ciphertext_end - self.digest_size..ciphertext_end] { + return Err(Error::ManipulatedMessage.into()); + } + + // Increase index. + self.chunk_index += 1; + + &plaintext[pos..pos + to_copy].copy_from_slice(&self.buffer[..to_copy]); + self.buffer.drain(..to_copy); + pos += to_copy; + + if short_read { + // We read the whole ciphertext, now check the final digest. + let mut aead = self.make_aead()?; + self.hash_associated_data(&mut aead, true); + let mut nada = [0; 0]; + aead.decrypt(&mut nada, b""); + aead.digest(&mut digest); + if &digest[..] != &ciphertext[ciphertext_end..] { + return Err(Error::ManipulatedMessage.into()); + } + } + + Ok(pos) + } +} + +// Note: this implementation tries *very* hard to make sure we don't +// gratuitiously do a short read. Specifically, if the return value +// is less than `plaintext.len()`, then it is either because we +// reached the end of the input or an error occured. +impl<R: io::Read> io::Read for Decryptor<R> { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + match self.read_helper(buf) { + Ok(n) => Ok(n), + Err(e) => match e.downcast::<io::Error>() { + // An io::Error. Pass as-is. + Ok(e) => Err(e), + // A failure. Create a compat object and wrap it. + Err(e) => Err(io::Error::new(io::ErrorKind::Other, + e.compat())), + }, + } + } +} + +/// A `BufferedReader` that decrypts AEAD-encrypted data as it is +/// read. +pub(crate) struct BufferedReaderDecryptor<R: BufferedReader<C>, C> { + reader: BufferedReaderGeneric<Decryptor<R>, C>, +} + +impl <R: BufferedReader<C>, C> BufferedReaderDecryptor<R, C> { + /// Like `new()`, but sets a cookie, which can be retrieved using + /// the `cookie_ref` and `cookie_mut` methods, and set using + /// the `cookie_set` method. + pub fn with_cookie(version: u8, cipher: SymmetricAlgorithm, + aead: AEADAlgorithm, chunk_size: usize, iv: &[u8], + key: &SessionKey, source: R, cookie: C) + -> Result<Self> + { + Ok(BufferedReaderDecryptor { + reader: BufferedReaderGeneric::with_cookie( + Decryptor::new(version, cipher, aead, chunk_size, iv, key, + source)?, + None, cookie), + }) + } +} + +impl<R: BufferedReader<C>, C> io::Read for BufferedReaderDecryptor<R, C> { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + self.reader.read(buf) + } +} + +impl <R: BufferedReader<C>, C> fmt::Debug for BufferedReaderDecryptor<R, C> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("BufferedReaderDecryptor") + .field("reader", &self.get_ref().unwrap()) + .finish() + } +} + +impl<R: BufferedReader<C>, C> BufferedReader<C> + for BufferedReaderDecryptor<R, C> { + fn buffer(&self) -> &[u8] { + return self.reader.buffer(); + } + + fn data(&mut self, amount: usize) -> io::Result<&[u8]> { + return self.reader.data(amount); + } + + fn data_hard(&mut self, amount: usize) -> io::Result<&[u8]> { + return self.reader.data_hard(amount); + } + + fn data_eof(&mut self) -> io::Result<&[u8]> { + return self.reader.data_eof(); + } + + fn consume(&mut self, amount: usize) -> &[u8] { + return self.reader.consume(amount); + } + + fn data_consume(&mut self, amount: usize) + -> io::Result<&[u8]> { + return self.reader.data_consume(amount); + } + + fn data_consume_hard(&mut self, amount: usize) -> io::Result<&[u8]> { + return self.reader.data_consume_hard(amount); + } + + fn read_be_u16(&mut self) -> io::Result<u16> { + return self.reader.read_be_u16(); + } + + fn read_be_u32(&mut self) -> io::Result<u32> { + return self.reader.read_be_u32(); + } + + fn steal(&mut self, amount: usize) -> io::Result<Vec<u8>> { + return self.reader.steal(amount); + } + + fn steal_eof(&mut self) -> io::Result<Vec<u8>> { + return self.reader.steal_eof(); + } + + fn get_mut(&mut self) -> Option<&mut BufferedReader<C>> { + Some(&mut self.reader.reader.source) + } + + fn get_ref(&self) -> Option<&BufferedReader<C>> { + Some(&self.reader.reader.source) + } + + fn into_inner<'b>(self: Box<Self>) + -> Option<Box<BufferedReader<C> + 'b>> where Self: 'b { + Some(Box::new(self.reader.reader.source)) + } + + fn cookie_set(&mut self, cookie: C) -> C { + self.reader.cookie_set(cookie) + } + + fn cookie_ref(&self) -> &C { + self.reader.cookie_ref() + } + + fn cookie_mut(&mut self) -> &mut C { + self.reader.cookie_mut() + } +} diff --git a/openpgp/src/lib.rs b/openpgp/src/lib.rs index 6316448b..22843a57 100644 --- a/openpgp/src/lib.rs +++ b/openpgp/src/lib.rs @@ -311,6 +311,8 @@ pub enum Packet { SEIP(seip::SEIP), /// Modification detection code packet. MDC(mdc::MDC), + /// AEAD Encrypted Data Packet. + AED(packet::AED), } impl Packet { @@ -336,6 +338,7 @@ impl Packet { &Packet::SKESK(_) => Tag::SKESK, &Packet::SEIP(_) => Tag::SEIP, &Packet::MDC(_) => Tag::MDC, + &Packet::AED(_) => Tag::AED, } } @@ -363,6 +366,7 @@ impl Packet { &Packet::SKESK(_) => Some(Tag::SKESK), &Packet::SEIP(_) => Some(Tag::SEIP), &Packet::MDC(_) => Some(Tag::MDC), + &Packet::AED(_) => Some(Tag::AED), } } } diff --git a/openpgp/src/message/grammar.lalrpop b/openpgp/src/message/grammar.lalrpop index 1fe2c2c7..d5c0ea36 100644 --- a/openpgp/src/message/grammar.lalrpop +++ b/openpgp/src/message/grammar.lalrpop @@ -21,10 +21,19 @@ SeipPart: () = { SEIP OPAQUE_CONTENT POP, } -// An encrypted part is 0 or more ESKs followed by a SEIP packet. +AedPart: () = { + AED Message POP, +} + +// An encrypted part is 0 or more ESKs followed by an encryption container. EncryptedPart: () = { + EncryptionContainer, + ESKS EncryptionContainer, +}; + +EncryptionContainer: () = { SeipPart, - ESKS SeipPart, + AedPart, }; ESKS: () = { @@ -61,6 +70,7 @@ extern { PKESK => lexer::Token::PKESK, SEIP => lexer::Token::SEIP, MDC => lexer::Token::MDC, + AED => lexer::Token::AED, OPS => lexer::Token::OPS, SIG => lexer::Token::SIG, POP => lexer::Token::Pop, diff --git a/openpgp/src/message/lexer.rs b/openpgp/src/message/lexer.rs index 778735d3..fc561d62 100644 --- a/openpgp/src/message/lexer.rs +++ b/openpgp/src/message/lexer.rs @@ -23,6 +23,8 @@ pub enum Token { SEIP, /// An MDC packet. MDC, + /// An AED packet. + AED, /// A OnePassSig packet. OPS, diff --git a/openpgp/src/message/mod.rs b/openpgp/src/message/mod.rs index 557c94e0..dfa332a9 100644 --- a/openpgp/src/message/mod.rs +++ b/openpgp/src/message/mod.rs @@ -209,6 +209,7 @@ impl MessageValidator { Tag::PKESK => Token::PKESK, Tag::SEIP => Token::SEIP, Tag::MDC => Token::MDC, + Tag::AED => Token::AED, Tag::OnePassSig => Token::OPS, Tag::Signature => Token::SIG, _ => { @@ -307,7 +308,8 @@ impl Message { v.push(packet.tag(), path.len() as isize - 1); match packet { - Packet::CompressedData(_) | Packet::SEIP(_) => { + Packet::CompressedData(_) | Packet::SEIP(_) | Packet::AED(_) => + { // If a container's content is not unpacked, then // we treat the content as an opaque message. @@ -467,6 +469,36 @@ mod tests { }, TestVector { + s: &[AED, Literal, Pop], + result: true, + }, + TestVector { + s: &[CompressedData, AED, Literal, Pop, Pop], + result: true, + }, + TestVector { + s: &[CompressedData, AED, CompressedData, Literal, + Pop, Pop, Pop], + result: true, + }, + TestVector { + s: &[AED, Pop], + result: false, + }, + TestVector { + s: &[SKESK, AED, Literal, Pop], + result: true, + }, + TestVector { + s: &[PKESK, AED, Literal, Pop], + result: true, + }, + TestVector { + s: &[SKESK, SKESK, AED, Literal, Pop], + result: true, + }, + + TestVector { s: &[OPS, Literal, SIG], result: true, }, diff --git a/openpgp/src/packet/aed.rs b/openpgp/src/packet/aed.rs new file mode 100644 index 00000000..8edc4dc2 --- /dev/null +++ b/openpgp/src/packet/aed.rs @@ -0,0 +1,167 @@ +use std::ops::{Deref, DerefMut}; + +use constants::{ + AEADAlgorithm, + SymmetricAlgorithm, +}; +use packet::{self, Common}; +use Packet; +use Error; +use Result; + +/// Holds an AEAD encrypted data packet. +/// +/// An AEAD encrypted data packet is a container. See [Section 5.16 +/// of RFC 4880bis] for details. +/// +/// [Section 5.16 of RFC 4880bis]: https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-05#section-5.16 +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +pub struct AED { + /// CTB packet header fields. + pub(crate) common: packet::Common, + /// AED version. Must be 1. + version: u8, + /// Cipher algorithm. + cipher: SymmetricAlgorithm, + /// AEAD algorithm. + aead: AEADAlgorithm, + /// Chunk size. + chunk_size: usize, + /// Initialization vector for the AEAD algorithm. + iv: Box<[u8]>, +} + +impl AED { + /// Creates a new AED object. + pub fn new(cipher: SymmetricAlgorithm, + aead: AEADAlgorithm, + chunk_size: usize, + iv: Box<[u8]>) -> Result<Self> { + if chunk_size.count_ones() != 1 { + return Err(Error::InvalidArgument( + format!("chunk size is not a power of two: {}", chunk_size)) + .into()); + } + + if chunk_size < 64 { + return Err(Error::InvalidArgument( + format!("chunk size is too small: {}", chunk_size)) + .into()); + } + + Ok(AED { + common: Default::default(), + version: 1, + cipher: cipher, + aead: aead, + chunk_size: chunk_size, + iv: iv, + }) + } + + /// Gets the version. + pub fn version(&self) -> u8 { + self.version + } + + /// Gets the cipher algorithm. + pub fn cipher(&self) -> SymmetricAlgorithm { + self.cipher + } + + /// Sets the cipher algorithm. + pub fn set_cipher(&mut self, cipher: SymmetricAlgorithm) { + self.cipher = cipher; + } + + /// Gets the AEAD algorithm. + pub fn aead(&self) -> AEADAlgorithm { + self.aead + } + + /// Sets the AEAD algorithm. + pub fn set_aead(&mut self, aead: AEADAlgorithm) { + self.aead = aead; + } + + /// Gets the chunk size. + pub fn chunk_size(&self) -> usize { + self.chunk_size + } + + /// Gets the chunk size. + pub fn set_chunk_size(&mut self, chunk_size: usize) -> Result<()> { + if chunk_size.count_ones() != 1 { + return Err(Error::InvalidArgument( + format!("chunk size is not a power of two: {}", chunk_size)) + .into()); + } + + if chunk_size < 64 { + return Err(Error::InvalidArgument( + format!("chunk size is too small: {}", chunk_size)) + .into()); + } + + self.chunk_size = chunk_size; + Ok(()) + } + + /// Gets the size of a chunk with digest. + pub fn chunk_digest_size(&self) -> Result<usize> { + Ok(self.chunk_size + self.aead.digest_size()?) + } + + /// Gets the initialization vector for the AEAD algorithm. + pub fn iv(&self) -> &[u8] { + &self.iv + } + + /// Sets the initialization vector for the AEAD algorithm. + pub fn set_iv(&mut self, iv: Box<[u8]>) { + self.iv = iv; + } + + /// Convert the `AED` struct to a `Packet`. + pub fn to_packet(self) -> Packet { + Packet::AED(self) + } +} + +impl From<AED> for Packet { + fn from(s: AED) -> Self { + s.to_packet() + } +} + +// Allow transparent access of common fields. +impl<'a> Deref for AED { + type Target = Common; + + fn deref(&self) -> &Self::Target { + &self.common + } +} + +// Allow transparent access of common fields. +impl<'a> DerefMut for AED { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.common + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deref() { + let mut s = AED::new(SymmetricAlgorithm::AES128, + AEADAlgorithm::EAX, + 64, + vec![].into_boxed_slice()).unwrap(); + assert_eq!(s.body(), None); + |