diff options
Diffstat (limited to 'src/openpgp')
-rw-r--r-- | src/openpgp/armor.rs | 723 | ||||
-rw-r--r-- | src/openpgp/mod.rs | 1 |
2 files changed, 724 insertions, 0 deletions
diff --git a/src/openpgp/armor.rs b/src/openpgp/armor.rs new file mode 100644 index 00000000..60b24542 --- /dev/null +++ b/src/openpgp/armor.rs @@ -0,0 +1,723 @@ +//! Handling ASCII Armor (see [RFC 4880, section +//! 6](https://tools.ietf.org/html/rfc4880#section-6)). +//! +//! # Scope +//! +//! This implements a subset of the ASCII Armor specification. Not +//! supported features are: +//! +//! - Multipart messages +//! - Headers +//! +//! The former is likely no longer useful today, and the latter seems +//! to be of questionable value because the data is not authenticated. +//! Reading armored data with headers is supported, but they are +//! merely swallowed. +//! +//! # Memory allocations +//! +//! Both the reader and the writer allocate memory in the order of the +//! size of chunks read or written. +//! +//! # Example +//! +//! ```rust,no_run +//! use std::fs::File; +//! use sequoia::openpgp::armor::{Reader, Kind}; +//! +//! let mut file = File::open("somefile.asc").unwrap(); +//! let mut r = Reader::new(&mut file, Kind::File); +//! ``` + +extern crate base64; +use std::io::{Read, Write}; +use std::io::{Error, ErrorKind}; +use std::cmp::min; + +/// The encoded output stream must be represented in lines of no more +/// 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; + +const LINE_ENDING: &str = "\n"; + +/// Specifies the type of data that is to be encoded (see [RFC 4880, +/// section 6.2](https://tools.ietf.org/html/rfc4880#section-6.2)). +#[derive(Copy, Clone, PartialEq)] +pub enum Kind { + /// A generic OpenPGP message. + Message, + /// A transferable public key. + PublicKey, + /// A transferable secret key. + PrivateKey, + /// Alias for PrivateKey. + SecretKey, + /// A detached signature. + Signature, + /// A generic file. This is a GnuPG extension. + File, + /// When reading an Armored file, accept any type. + Any, +} + +impl Kind { + fn detect(blurb: &[u8]) -> Option<Self> { + if blurb.len() < 16 || ! blurb.starts_with(b"-----BEGIN PGP ") { + return None; + } + + match &blurb[15..17] { + b"ME" => Some(Kind::Message), + b"PU" => Some(Kind::PublicKey), + b"PR" => Some(Kind::SecretKey), + b"SI" => Some(Kind::Signature), + b"AR" => Some(Kind::File), + _ => None, + } + } + + fn blurb(&self) -> &str { + match self { + &Kind::Message => "MESSAGE", + &Kind::PublicKey => "PUBLIC KEY BLOCK", + &Kind::PrivateKey => "PRIVATE KEY BLOCK", + &Kind::SecretKey => "PRIVATE KEY BLOCK", + &Kind::Signature => "SIGNATURE", + &Kind::File => "ARMORED FILE", + &Kind::Any => unreachable!(), + } + } + + fn begin(&self) -> String { + format!("-----BEGIN PGP {}-----", self.blurb()) + } + + fn begin_len(&self) -> usize { + 20 + self.blurb().len() + } + + fn end(&self) -> String { + format!("-----END PGP {}-----", self.blurb()) + } +} + +/// A filter that applies ASCII Armor to the data written to it. +pub struct Writer<'a, W: 'a + Write> { + sink: &'a mut W, + kind: Kind, + stash: Vec<u8>, + column: usize, + crc: CRC, + initialized: bool, + finalized: bool, +} + +impl<'a, W: Write> Writer<'a, W> { + /// Construct a new filter for the given type of data. + pub fn new(inner: &'a mut W, kind: Kind) -> Self { + assert!(kind != Kind::Any); + Writer { + sink: inner, + kind: kind, + stash: Vec::<u8>::with_capacity(2), + column: 0, + crc: CRC::new(), + initialized: false, + finalized: false, + } + } + + /// Write the header if not already done. + fn initialize(&mut self) -> Result<(), Error> { + if self.initialized { return Ok(()) } + + write!(self.sink, "{}{}{}", self.kind.begin(), + LINE_ENDING, LINE_ENDING)?; + + self.initialized = true; + Ok(()) + } + + /// Write the footer. No more data can be written after this + /// call. If this is not called explicitly, the header is written + /// once the writer is dropped. + pub fn finalize(&mut self) -> Result<(), Error> { + self.initialize()?; + if self.finalized { + return Err(Error::new(ErrorKind::BrokenPipe, "Writer is finalized.")); + } + + // Write any stashed bytes and pad. + if self.stash.len() > 0 { + self.sink.write_all(base64::encode_config(&self.stash, + base64::STANDARD).as_bytes())?; + self.column += 4; + } + self.linebreak()?; + if self.column > 0 { + write!(self.sink, "{}", LINE_ENDING)?; + } + + let crc = self.crc.finalize(); + let bytes: [u8; 3] = [ + (crc >> 16) as u8, + (crc >> 8) as u8, + (crc >> 0) as u8, + ]; + + // CRC and footer. + write!(self.sink, "={}{}{}{}", + base64::encode_config(&bytes, base64::STANDARD_NO_PAD), + LINE_ENDING, self.kind.end(), LINE_ENDING)?; + + self.finalized = true; + Ok(()) + } + + /// Insert a line break if necessary. + fn linebreak(&mut self) -> Result<(), Error> { + assert!(self.column <= LINE_LENGTH); + if self.column == LINE_LENGTH { + write!(self.sink, "{}", LINE_ENDING)?; + self.column = 0; + } + Ok(()) + } +} + +impl<'a, W: Write> Write for Writer<'a, W> { + fn write(&mut self, buf: &[u8]) -> Result<usize, Error> { + self.initialize()?; + if self.finalized { + return Err(Error::new(ErrorKind::BrokenPipe, "Writer is finalized.")); + } + + // Update CRC on the unencoded data. + self.crc.update(buf); + + let mut input = buf; + let mut written = 0; + + // First of all, if there are stashed bytes, fill the stash + // and encode it. + assert!(self.stash.len() < 3); + if self.stash.len() > 0 { + while self.stash.len() < 3 { + if input.len() == 0 { + /* We exhausted the input. Return now, any + * stashed bytes are encoded when finalizing the + * writer. */ + return Ok(written); + } + self.stash.push(input[0]); + input = &input[1..]; + written += 1; + } + + self.sink.write_all(base64::encode_config(&self.stash, + base64::STANDARD_NO_PAD).as_bytes())?; + self.column += 4; + self.linebreak()?; + self.stash.clear(); + } + + // Ensure that a multiple of 3 bytes are encoded, stash the + // rest from the end of input. + while input.len() % 3 > 0 { + self.stash.push(input[input.len()-1]); + input = &input[..input.len()-1]; + written += 1; + } + // We popped values from the end of the input, fix the order. + self.stash.reverse(); + assert!(self.stash.len() < 3); + + // We know that we have a multiple of 3 bytes, encode them and write them out. + assert!(input.len() % 3 == 0); + let encoded = base64::encode_config(input, base64::STANDARD_NO_PAD); + written += input.len(); + let mut enc = encoded.as_bytes(); + while enc.len() > 0 { + let n = min(LINE_LENGTH - self.column, enc.len()); + self.sink.write_all(&enc[..n])?; + enc = &enc[n..]; + self.column += n; + self.linebreak()?; + } + + assert_eq!(written, buf.len()); + Ok(written) + } + + fn flush(&mut self) -> Result<(), Error> { + self.sink.flush() + } +} + +impl<'a, W: Write> Drop for Writer<'a, W> { + fn drop(&mut self) { + let _ = self.finalize(); + } +} + +/// A filter that strips ASCII Armor from a stream of data. +pub struct Reader<'a, R: 'a + Read> { + source: &'a mut R, + kind: Kind, + stash: Vec<u8>, + crc: CRC, + expect_crc: Option<u32>, + initialized: bool, + finalized: bool, +} + +impl<'a, R: Read> Reader<'a, R> { + /// Construct a new filter for the given type of data. + pub fn new(inner: &'a mut R, kind: Kind) -> Self { + Reader { + source: inner, + kind: kind, + stash: Vec::<u8>::with_capacity(2), + crc: CRC::new(), + expect_crc: None, + initialized: false, + finalized: false, + } + } + + /// Return the kind of data this reader is for. Useful in + /// combination with 'Kind::Any'. + pub fn kind(&self) -> Kind { + self.kind + } + + /// Consume the header if not already done. + fn initialize(&mut self) -> Result<(), Error> { + if self.initialized { return Ok(()) } + + let buf = if self.kind == Kind::Any { + let peek = 17; + let mut buf: Vec<u8> = vec![0; peek]; + self.source.read_exact(&mut buf)?; + + if let Some(k) = Kind::detect(&buf) { + self.kind = k; + } else { + return Err(Error::new(ErrorKind::InvalidInput, "Invalid ASCII Armor header.")); + } + + buf.resize(self.kind.begin_len(), 0); + self.source.read_exact(&mut buf[peek..])?; + buf + } else { + let mut buf: Vec<u8> = vec![0; self.kind.begin_len()]; + self.source.read_exact(&mut buf)?; + buf + }; + + if buf != self.kind.begin().into_bytes() { + return Err(Error::new(ErrorKind::InvalidInput, "Invalid ASCII Armor header.")); + } + self.linebreak()?; + + while self.line()? != 0 { + /* Swallow headers. */ + } + + self.initialized = true; + Ok(()) + } + + /// Consume the footer. No more data can be read after this + /// call. + fn finalize(&mut self, buf: &[u8]) -> Result<(), Error> { + if self.finalized { + return Err(Error::new(ErrorKind::BrokenPipe, "Reader is finalized.")); + } + + let mut rest = Vec::new(); + self.source.read_to_end(&mut rest)?; + + let mut footer = Vec::new(); + footer.extend(buf); + footer.extend(&rest); + let mut off = 0; + + /* Look for CRC. The CRC is optional. */ + if footer.len() >= 6 && footer[0] == '=' as u8 { + /* Found. */ + let crc = match base64::decode_config(&footer[1..5], base64::MIME) { + Ok(d) => d, + Err(e) => return Err(Error::new(ErrorKind::InvalidInput, e)), + }; + self.expect_crc = Some((crc[0] as u32) << 16 + | (crc[1] as u32) << 8 + | crc[2] as u32); + + /* Update offset, skip whitespace. */ + off += 5; + while off < footer.len() && is_ascii_whitespace(footer[off]) { + off += 1; + } + } + + if ! footer[off..].starts_with(&self.kind.end().into_bytes()) { + return Err(Error::new(ErrorKind::InvalidInput, "Invalid ASCII Armor footer.")); + } + + self.finalized = true; + Ok(()) + } + + /// Consume a linebreak. + fn linebreak(&mut self) -> Result<(), Error> { + if self.line()? != 0 { + return Err(Error::new(ErrorKind::InvalidInput, "Expected newline.")); + } + Ok(()) + } + + /// Consume a line, returning the number of non-whitespace bytes. + fn line(&mut self) -> Result<usize, Error> { + let mut buf = [0; 1]; + let mut c = 0; + + loop { + self.source.read_exact(&mut buf)?; + if buf[0] == '\r' as u8 { + self.source.read_exact(&mut buf)?; + } + + if buf[0] != '\n' as u8 { + c += 1; + } else { + break; + } + } + + Ok(c) + } +} + +/* XXX: Use u8.is_ascii_whitespace() once out of nightlies. */ +fn is_ascii_whitespace(c: u8) -> bool { + c == ' ' as u8 || c == '\n' as u8 || c == '\r' as u8 || c == '\t' as u8 +} + +/// Look for the CRC sum or the footer. +fn find_footer(buf: &[u8]) -> Option<usize> { + if buf.len() == 0 { + return None; + } + + if buf[0] == '=' as u8 || buf[0] == '-' as u8 { + return Some(0); + } + + for i in 0..buf.len() - 1 { + if is_ascii_whitespace(buf[i]) && (buf[i+1] == '=' as u8 + || buf[i+1] == '-' as u8) { + return Some(i + 1); + } + } + None +} + +impl<'a, W: Read> Read for Reader<'a, W> { + fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> { + self.initialize()?; + if self.finalized { return Ok(0) } + + /* How much did we get? */ + let mut read = 0; + + /* See if there are stashed bytes, and use them. */ + assert!(self.stash.len() < 3); + while self.stash.len() > 0 && buf.len() > read { + buf[read] = self.stash.pop().unwrap(); + read += 1; + } + + /* Try to get enough bytes to fill buf, account for bytes + * filled using the stash, round up. */ + let mut raw: Vec<u8> = vec![0; (buf.len() - read + 2) / 3 * 4]; + let got = self.source.read(&mut raw)?; + raw.truncate(got); + + /* Check if we see the footer. If so, we're almost done. */ + let decoded = if let Some(n) = find_footer(&raw) { + self.finalize(&raw[n..])?; + match base64::decode_config(&raw[..n], base64::MIME) { + Ok(d) => d, + Err(e) => return Err(Error::new(ErrorKind::InvalidInput, e)), + } + } else { + /* We may have to get some more until we have a multiple + * of four non-whitespace ASCII characters. */ + loop { + let n = &raw.iter().filter(|c| ! is_ascii_whitespace(**c)).count(); + if n % 4 == 0 { break } + + /* Get some more bytes. */ + let mut m: Vec<u8> = vec![0; 4 - n % 4]; + let got = self.source.read(&mut m)?; + if got == 0 { + /* Tough. This will fail in the decoder. */ + break; + } + m.truncate(got); + raw.append(&mut m); + } + + match base64::decode_config(&raw, base64::MIME) { + Ok(d) => d, + Err(e) => return Err(Error::new(ErrorKind::InvalidInput, e)), + } + }; + + self.crc.update(&decoded); + + /* Check how much we got vs how much was requested. */ + if decoded.len() <= (buf.len() - read) { + &mut buf[read..read + decoded.len()].copy_from_slice(&decoded); + read += decoded.len(); + } else { + /* We got more than we wanted, spill the surplus into our + * stash. */ + let spill = decoded.len() - (buf.len() - read); + assert!(spill < 3); + + &mut buf[read..read + decoded.len() - spill].copy_from_slice( + &decoded[..decoded.len() - spill]); + + for c in &decoded[decoded.len() - spill..] { + self.stash.push(*c); + } + assert!(self.stash.len() < 3); + self.stash.reverse(); + read += decoded.len() - spill; + } + + /* If we are finalized, we may have found a crc sum. */ + if let Some(crc) = self.expect_crc { + if self.crc.finalize() != crc { + return Err(Error::new(ErrorKind::InvalidInput, "Bad CRC sum.")); + } + } + Ok(read) + } +} + +const CRC24_INIT: u32 = 0xB704CE; +const CRC24_POLY: u32 = 0x1864CFB; + +struct CRC { + n: u32, +} + +/// Computes the CRC-24, (see [RFC 4880, section +/// 6.1](https://tools.ietf.org/html/rfc4880#section-6.1)). +impl CRC { + fn new() -> Self { + CRC { n: CRC24_INIT } + } + + fn update(&mut self, buf: &[u8]) -> &Self { + for octet in buf { + self.n ^= (*octet as u32) << 16; + for _ in 0..8 { + self.n <<= 1; + if self.n & 0x1000000 > 0 { + self.n ^= CRC24_POLY; + } + } + } + self + } + + fn finalize(&self) -> u32 { + self.n & 0xFFFFFF + } +} + +#[cfg(test)] +mod test { + use std::io::Write; + use super::CRC; + use super::Kind; + use super::Writer; + + #[test] + fn crc() { + let b = b"foobarbaz"; + let crcs = [ + 0xb704ce, + 0x6d2804, + 0xa2d10d, + 0x4fc255, + 0x7aafca, + 0xc79c46, + 0x7334de, + 0x77dc72, + 0x000f65, + 0xf40d86, + ]; + + for len in 0..b.len() + 1 { + assert_eq!(CRC::new().update(&b[..len]).finalize(), crcs[len]); + } + } + + use std::fs::File; + use std::io::prelude::*; + + const TEST_VECTORS: [u8; 9] = [0, 1, 2, 3, 47, 48, 49, 50, 51]; + + #[test] + fn enarmor() { + for len in TEST_VECTORS.iter() { + let mut file = File::open(format!("tests/data/armor/test-{}.bin", len)).unwrap(); + let mut bin = Vec::<u8>::new(); + file.read_to_end(&mut bin).unwrap(); + + let mut file = File::open(format!("tests/data/armor/test-{}.asc", len)).unwrap(); + let mut asc = Vec::<u8>::new(); + file.read_to_end(&mut asc).unwrap(); + + let mut buf = Vec::new(); + { + let mut w = Writer::new(&mut buf, Kind::File); + w.write_all(&bin).unwrap(); + } + assert_eq!(String::from_utf8_lossy(&buf), + String::from_utf8_lossy(&asc)); + } + } + + #[test] + fn enarmor_bytewise() { + for len in TEST_VECTORS.iter() { + let mut file = File::open(format!("tests/data/armor/test-{}.bin", len)).unwrap(); + let mut bin = Vec::<u8>::new(); + file.read_to_end(&mut bin).unwrap(); + + let mut file = File::open(format!("tests/data/armor/test-{}.asc", len)).unwrap(); + let mut asc = Vec::<u8>::new(); + file.read_to_end(&mut asc).unwrap(); + + let mut buf = Vec::new(); + { + let mut w = Writer::new(&mut buf, Kind::File); + for (i, _) in bin.iter().enumerate() { + w.write(&bin[i..i+1]).unwrap(); + } + } + assert_eq!(String::from_utf8_lossy(&buf), + String::from_utf8_lossy(&asc)); + } + } + + use super::Reader; + + #[test] + fn dearmor_binary() { + for len in TEST_VECTORS.iter() { + let mut file = File::open(format!("tests/data/armor/test-{}.bin", len)).unwrap(); + let mut r = Reader::new(&mut file, Kind::Message); + let mut buf = [0; 5]; + let e = r.read(&mut buf); + assert!(e.is_err()); + } + } + + #[test] + fn dearmor_wrong_kind() { + let mut file = File::open("tests/data/armor/test-0.asc").unwrap(); + let mut r = Reader::new(&mut file, Kind::Message); + let mut buf = [0; 5]; + let e = r.read(&mut buf); + assert!(e.is_err()); + } + + #[test] + fn dearmor_wrong_crc() { + let mut file = File::open("tests/data/armor/test-0.bad-crc.asc").unwrap(); + let mut r = Reader::new(&mut file, Kind::File); + let mut buf = [0; 5]; + let e = r.read(&mut buf); + assert!(e.is_err()); + } + + #[test] + fn dearmor_wrong_footer() { + let mut file = File::open("tests/data/armor/test-2.bad-footer.asc").unwrap(); + let mut r = Reader::new(&mut file, Kind::File); + let mut buf = [0; 5]; + let e = r.read(&mut buf); + assert!(e.is_err()); + } + + #[test] + fn dearmor_no_crc() { + let mut file = File::open("tests/data/armor/test-1.no-crc.asc").unwrap(); + let mut r = Reader::new(&mut file, Kind::File); + let mut buf = [0; 5]; + let e = r.read(&mut buf); + assert!(e.unwrap() == 1 && buf[0] == 0xde); + } + + #[test] + fn dearmor_with_header() { + let mut file = File::open("tests/data/armor/test-3.with-headers.asc").unwrap(); + let mut r = Reader::new(&mut file, Kind::File); + let mut buf = [0; 5]; + let e = r.read(&mut buf); + assert!(e.is_ok()); + } + + #[test] + fn dearmor_any() { + let mut file = File::open("tests/data/armor/test-3.with-headers.asc").unwrap(); + let mut r = Reader::new(&mut file, Kind::Any); + let mut buf = [0; 5]; + let e = r.read(&mut buf); + assert!(r.kind() == Kind::File); + assert!(e.is_ok()); + } + + #[test] + fn dearmor() { + for len in TEST_VECTORS.iter() { + let mut file = File::open(format!("tests/data/armor/test-{}.bin", len)).unwrap(); + let mut bin = Vec::<u8>::new(); + file.read_to_end(&mut bin).unwrap(); + + let mut file = File::open(format!("tests/data/armor/test-{}.asc", len)).unwrap(); + let mut r = Reader::new(&mut file, Kind::File); + let mut dearmored = Vec::<u8>::new(); + r.read_to_end(&mut dearmored).unwrap(); + + assert_eq!(&bin, &dearmored); + } + } + + #[test] + fn dearmor_bytewise() { + for len in TEST_VECTORS.iter() { + let mut file = File::open(format!("tests/data/armor/test-{}.bin", len)).unwrap(); + let mut bin = Vec::<u8>::new(); + file.read_to_end(&mut bin).unwrap(); + + let mut file = File::open(format!("tests/data/armor/test-{}.asc", len)).unwrap(); + let r = Reader::new(&mut file, Kind::File); + let mut dearmored = Vec::<u8>::new(); + for c in r.bytes() { + dearmored.push(c.unwrap()); + } + + assert_eq!(&bin, &dearmored); + } + } +} diff --git a/src/openpgp/mod.rs b/src/openpgp/mod.rs index 683b8448..46081fbe 100644 --- a/src/openpgp/mod.rs +++ b/src/openpgp/mod.rs @@ -3,6 +3,7 @@ // an included file confuses rust (it looks for the module in the // wrong place). Hence, that here as well. +pub mod armor; pub mod parse; pub mod types; |