summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--openpgp/examples/wrap-literal.rs40
-rw-r--r--openpgp/src/armor.rs236
-rw-r--r--openpgp/tests/data/armor/README16
-rw-r--r--openpgp/tests/data/armor/literal-0-no-header-with-chksum.asc3
-rw-r--r--openpgp/tests/data/armor/literal-0-no-header.asc2
-rw-r--r--openpgp/tests/data/armor/literal-0-no-newlines.asc1
-rw-r--r--openpgp/tests/data/armor/literal-0.asc5
-rw-r--r--openpgp/tests/data/armor/literal-0.binbin0 -> 8 bytes
-rw-r--r--openpgp/tests/data/armor/literal-1-no-header-with-chksum.asc3
-rw-r--r--openpgp/tests/data/armor/literal-1-no-header.asc2
-rw-r--r--openpgp/tests/data/armor/literal-1-no-newlines.asc1
-rw-r--r--openpgp/tests/data/armor/literal-1.asc5
-rw-r--r--openpgp/tests/data/armor/literal-1.binbin0 -> 9 bytes
-rw-r--r--openpgp/tests/data/armor/literal-2-no-header-with-chksum.asc3
-rw-r--r--openpgp/tests/data/armor/literal-2-no-header.asc2
-rw-r--r--openpgp/tests/data/armor/literal-2-no-newlines.asc1
-rw-r--r--openpgp/tests/data/armor/literal-2.asc5
-rw-r--r--openpgp/tests/data/armor/literal-2.binbin0 -> 10 bytes
-rw-r--r--openpgp/tests/data/armor/literal-3-no-header-with-chksum.asc3
-rw-r--r--openpgp/tests/data/armor/literal-3-no-header.asc2
-rw-r--r--openpgp/tests/data/armor/literal-3-no-newlines.asc1
-rw-r--r--openpgp/tests/data/armor/literal-3.asc5
-rw-r--r--openpgp/tests/data/armor/literal-3.binbin0 -> 11 bytes
-rw-r--r--openpgp/tests/data/armor/literal-47-no-header-with-chksum.asc4
-rw-r--r--openpgp/tests/data/armor/literal-47-no-header.asc3
-rw-r--r--openpgp/tests/data/armor/literal-47-no-newlines.asc1
-rw-r--r--openpgp/tests/data/armor/literal-47.asc6
-rw-r--r--openpgp/tests/data/armor/literal-47.binbin0 -> 55 bytes
-rw-r--r--openpgp/tests/data/armor/literal-48-no-header-with-chksum.asc4
-rw-r--r--openpgp/tests/data/armor/literal-48-no-header.asc3
-rw-r--r--openpgp/tests/data/armor/literal-48-no-newlines.asc1
-rw-r--r--openpgp/tests/data/armor/literal-48.asc6
-rw-r--r--openpgp/tests/data/armor/literal-48.binbin0 -> 56 bytes
-rw-r--r--openpgp/tests/data/armor/literal-49-no-header-with-chksum.asc4
-rw-r--r--openpgp/tests/data/armor/literal-49-no-header.asc3
-rw-r--r--openpgp/tests/data/armor/literal-49-no-newlines.asc1
-rw-r--r--openpgp/tests/data/armor/literal-49.asc6
-rw-r--r--openpgp/tests/data/armor/literal-49.binbin0 -> 57 bytes
-rw-r--r--openpgp/tests/data/armor/literal-50-no-header-with-chksum.asc4
-rw-r--r--openpgp/tests/data/armor/literal-50-no-header.asc3
-rw-r--r--openpgp/tests/data/armor/literal-50-no-newlines.asc1
-rw-r--r--openpgp/tests/data/armor/literal-50.asc6
-rw-r--r--openpgp/tests/data/armor/literal-50.binbin0 -> 58 bytes
-rw-r--r--openpgp/tests/data/armor/literal-51-no-header-with-chksum.asc4
-rw-r--r--openpgp/tests/data/armor/literal-51-no-header.asc3
-rw-r--r--openpgp/tests/data/armor/literal-51-no-newlines.asc1
-rw-r--r--openpgp/tests/data/armor/literal-51.asc6
-rw-r--r--openpgp/tests/data/armor/literal-51.binbin0 -> 59 bytes
48 files changed, 395 insertions, 11 deletions
diff --git a/openpgp/examples/wrap-literal.rs b/openpgp/examples/wrap-literal.rs
new file mode 100644
index 00000000..5d57d118
--- /dev/null
+++ b/openpgp/examples/wrap-literal.rs
@@ -0,0 +1,40 @@
+/// This program demonstrates how to wrap a stream into a literal data
+/// packet.
+///
+/// It is also used to generate test vectors for the armor subsystem.
+
+use std::env;
+use std::io;
+
+extern crate openpgp;
+use openpgp::armor;
+use openpgp::constants::DataFormat;
+use openpgp::serialize::stream::{wrap, LiteralWriter};
+
+fn main() {
+ let args: Vec<String> = env::args().collect();
+ if args.len() != 1 {
+ panic!("A simple filter wrapping data into a literal data packet.\n\n\
+ Usage: {} <input >output\n", args[0]);
+ }
+
+ // Compose a writer stack corresponding to the output format and
+ // packet structure we want. First, we want the output to be
+ // ASCII armored.
+ let sink = armor::Writer::new(io::stdout(), armor::Kind::Message, &[][..])
+ .expect("Failed to create armored writer.");
+
+ // Then, create a literal writer to wrap the data in a literal
+ // message packet.
+ let mut literal = LiteralWriter::new(wrap(sink), DataFormat::Binary,
+ None, None)
+ .expect("Failed to create literal writer");
+
+ // Finally, just copy all the data.
+ io::copy(&mut io::stdin(), &mut literal)
+ .expect("Failed to sign data");
+
+ // Teardown the stack to ensure all the data is written.
+ literal.finalize()
+ .expect("Failed to write data");
+}
diff --git a/openpgp/src/armor.rs b/openpgp/src/armor.rs
index c33e30b1..6a09cc2b 100644
--- a/openpgp/src/armor.rs
+++ b/openpgp/src/armor.rs
@@ -36,6 +36,8 @@ use std::cmp::min;
use std::str;
use quickcheck::{Arbitrary, Gen};
+use packet::Header;
+
/// 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
@@ -317,12 +319,10 @@ impl<W: Write> Drop for Writer<W> {
}
/// A filter that strips ASCII Armor from a stream of data.
-///
-/// The reader ignores any data in front of the armored data, as long
-/// as the line the header is in is only prefixed by whitespace.
pub struct Reader<'a> {
source: Box<'a + BufferedReader<()>>,
kind: Option<Kind>,
+ strict: bool,
buffer: Vec<u8>,
crc: CRC,
expect_crc: Option<u32>,
@@ -334,11 +334,52 @@ pub struct Reader<'a> {
impl<'a> Reader<'a> {
/// Constructs a new filter for the given type of data.
///
+ /// [ASCII Armor], designed to protect OpenPGP data in transit,
+ /// has been a source of problems if the armor structure is
+ /// damaged. For example, copying data manually from one program
+ /// to another might introduce or drop newlines.
+ ///
+ /// By default, the reader operates in robust mode. It will
+ /// extract the first armored OpenPGP data block it can find, even
+ /// if the armor frame is damaged, or missing.
+ ///
+ /// To select strict mode, specify a kind argument. In strict
+ /// mode, the reader will match on the armor frame. The reader
+ /// ignores any data in front of the Armor Header Line, as long as
+ /// the line the header is in is only prefixed by whitespace.
+ ///
+ /// [ASCII Armor]: https://tools.ietf.org/html/rfc4880#section-6.2
+ ///
/// # Example
///
/// ```
/// # use std::io::Read;
/// # extern crate openpgp;
+ /// # use openpgp::{Result, Message};
+ /// # use openpgp::armor::Reader;
+ /// # use std::io;
+ /// # fn main() { f().unwrap(); }
+ /// # fn f() -> Result<()> {
+ /// let data = "yxJiAAAAAABIZWxsbyB3b3JsZCE="; // base64 over literal data packet
+ ///
+ /// let mut cursor = io::Cursor::new(&data);
+ /// let mut reader = Reader::new(&mut cursor, None);
+ ///
+ /// let mut buf = Vec::new();
+ /// reader.read_to_end(&mut buf)?;
+ ///
+ /// let message = Message::from_bytes(&buf)?;
+ /// assert_eq!(message.body().unwrap().common.body.as_ref().unwrap(),
+ /// b"Hello world!");
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// Or, in strict mode:
+ ///
+ /// ```
+ /// # use std::io::Read;
+ /// # extern crate openpgp;
/// # use openpgp::armor::{Reader, Kind};
/// # use std::io::{self, Result};
/// # fn main() { f().unwrap(); }
@@ -351,7 +392,7 @@ impl<'a> Reader<'a> {
/// -----END PGP ARMORED FILE-----";
///
/// let mut cursor = io::Cursor::new(&data);
- /// let mut reader = Reader::new(&mut cursor, None);
+ /// let mut reader = Reader::new(&mut cursor, Some(Kind::File));
///
/// let mut content = String::new();
/// reader.read_to_string(&mut content)?;
@@ -398,6 +439,7 @@ impl<'a> Reader<'a> {
Reader {
source: Box::new(BufferedReaderGeneric::new(inner, None)),
kind: kind,
+ strict: kind.is_some(),
buffer: Vec::<u8>::with_capacity(1024),
crc: CRC::new(),
expect_crc: None,
@@ -435,6 +477,7 @@ impl<'a> Reader<'a> {
// Look for the Armor Header Line, skipping any garbage in the
// process.
let mut n = 0;
+ let mut found_blob = false;
'search: loop {
self.source.consume(n);
@@ -446,6 +489,39 @@ impl<'a> Reader<'a> {
"Reached EOF looking for Armor Header Line"));
}
+ // If the user did not specify what kind of data we want,
+ // we aggressively try to decode any data, even if we do
+ // not see a valid header.
+ if ! self.strict {
+ // Try the whole string, as well as substrings
+ // starting at each whitespace sequence.
+ let mut offset = 0;
+ loop {
+ if is_armored_pgp_blob(&line[offset..]) {
+ // Consume anything up to this point.
+ n = offset;
+ found_blob = true;
+ break 'search;
+ }
+
+ if let Some(o) = &line[offset..].iter()
+ .position(|c| c.is_ascii_whitespace())
+ {
+ offset += *o;
+
+ // Skip whitespaces.
+ while offset < line.len()
+ && line[offset].is_ascii_whitespace()
+ {
+ offset += 1;
+ }
+ } else {
+ // No armored blob found in this line.
+ break;
+ }
+ }
+ }
+
if line.len() < 27 {
// Line is too short to contain the shortest header.
continue;
@@ -473,6 +549,12 @@ impl<'a> Reader<'a> {
}
self.source.consume(n);
+ if found_blob {
+ // Skip the rest of the initialization.
+ self.initialized = true;
+ return Ok(());
+ }
+
// Read the headers.
let mut n = 0;
loop {
@@ -565,6 +647,77 @@ impl<'a> Reader<'a> {
}
}
+/// Checks whether the given bytes contain armored OpenPGP data.
+fn is_armored_pgp_blob(bytes: &[u8]) -> bool {
+ use Tag::*;
+
+ let bytes = if let Some(msg) = get_base64_prefix(bytes) {
+ msg
+ } else {
+ return false;
+ };
+
+ // We may need to drop some characters at the end.
+ let mut end = bytes.len();
+ loop {
+ match base64::decode_config(&bytes[..end], base64::MIME) {
+ Ok(d) => {
+ let mut br = BufferedReaderMemory::new(&d);
+ let header = Header::parse(&mut br);
+ break match header {
+ Ok(h) => match h.ctb.tag {
+ // Might be a message?
+ PKESK | SKESK | OnePassSig | CompressedData | Literal =>
+ true,
+ // Might be a key?
+ SecretKey | PublicKey =>
+ true,
+ // Might be a detached signature?
+ Signature =>
+ true,
+ // ... otherwise, looks like garbage.
+ _ =>
+ false,
+ },
+ Err(_) => false,
+ }
+ },
+ Err(_) =>
+ if end == 0 {
+ break false;
+ } else {
+ end -= 1;
+ },
+ }
+ }
+}
+
+/// Gets a slice containing the largest valid base64 prefix.
+fn get_base64_prefix(bytes: &[u8]) -> Option<&[u8]> {
+ let mut seen_padding = false;
+ for (i, c) in bytes.iter().enumerate() {
+ if c.is_ascii_whitespace() {
+ continue;
+ }
+
+ if seen_padding && *c != '=' as u8 {
+ return Some(&bytes[..i]);
+ }
+
+ if *c == '=' as u8 {
+ seen_padding = true;
+ } else if ! is_base64_char(c) {
+ if i == 0 {
+ return None;
+ } else {
+ return Some(&bytes[..i]);
+ }
+ }
+ }
+
+ return Some(bytes);
+}
+
/// Checks whether the given byte is in the base64 character set.
fn is_base64_char(b: &u8) -> bool {
b.is_ascii_alphanumeric() || *b == '+' as u8 || *b == '/' as u8
@@ -654,11 +807,38 @@ impl<'a> Read for Reader<'a> {
// hitting EOF.
let mut got = 0;
- loop {
+ 'readloop: loop {
let raw = self.source.data(want)?;
if raw.len() == got {
- return Err(Error::new(ErrorKind::UnexpectedEof,
- "Armor footer is missing"));
+ // EOF. Decide how to proceed.
+
+ if self.strict {
+ // If we are here, we should have seen an
+ // footer by now.
+ return Err(Error::new(ErrorKind::UnexpectedEof,
+ "Armor footer is missing"));
+ } else {
+ // Otherwise, we may have found only the blob,
+ // or the footer is damaged, or missing. Try
+ // to decode what we have got, then we are
+ // done.
+
+ // We need to try to discard garbage at the end.
+ let mut end = min(raw.len(), want);
+ loop {
+ match base64::decode_config(&raw[..end],
+ base64::MIME) {
+ Ok(d) => break 'readloop (end, d),
+ Err(_) =>
+ if end == 0 {
+ // No more valid data.
+ break 'readloop (raw.len(), vec![]);
+ } else {
+ end -= 1;
+ },
+ }
+ }
+ }
} else {
got = raw.len();
}
@@ -676,14 +856,26 @@ impl<'a> Read for Reader<'a> {
}
}
- // See how many non-whitespace characters we got.
+ // See how many valid characters we got.
let n = &raw.iter().filter(
|c| ! (**c).is_ascii_whitespace()).count();
if n % 4 == 0 {
// Enough! Try to decode them.
- match base64::decode_config(&raw, base64::MIME) {
- Ok(d) => break (raw.len(), d),
- Err(e) => return Err(Error::new(ErrorKind::InvalidInput, e)),
+
+ // We need to try to discard garbage at the end.
+ let mut end = raw.len();
+ loop {
+ match base64::decode_config(&raw[..end],
+ base64::MIME) {
+ Ok(d) => break 'readloop (end, d),
+ Err(_) =>
+ if end == 0 {
+ // No more valid data.
+ break 'readloop (raw.len(), vec![]);
+ } else {
+ end -= 1;
+ },
+ }
}
}
@@ -868,6 +1060,28 @@ mod test {
use super::Reader;
#[test]
+ fn dearmor_robust() {
+ for len in TEST_VECTORS.iter() {
+ let mut file = File::open(format!("tests/data/armor/literal-{}.bin",
+ len)).unwrap();
+ let mut reference = Vec::<u8>::new();
+ file.read_to_end(&mut reference).unwrap();
+
+ for test in &["", "-no-header-with-chksum", "-no-header",
+ "-no-newlines"] {
+ let filename = format!("tests/data/armor/literal-{}{}.asc",
+ len, test);
+ let mut file = File::open(filename).unwrap();
+ let mut r = Reader::new(&mut file, None);
+ let mut dearmored = Vec::<u8>::new();
+ r.read_to_end(&mut dearmored).unwrap();
+
+ assert_eq!(&reference, &dearmored);
+ }
+ }
+ }
+
+ #[test]
fn dearmor_binary() {
for len in TEST_VECTORS.iter() {
let mut file = File::open(format!("tests/data/armor/test-{}.bin", len)).unwrap();
diff --git a/openpgp/tests/data/armor/README b/openpgp/tests/data/armor/README
new file mode 100644
index 00000000..4f28345a
--- /dev/null
+++ b/openpgp/tests/data/armor/README
@@ -0,0 +1,16 @@
+This directory contains test vectors for the armor module.
+
+# literal-$n.*
+
+These files are generated using:
+
+ (cd ../../../.. && cargo build -p openpgp --example wrap-literal)
+ for n in 0 1 2 3 47 48 49 50 51
+ do
+ dd if=/dev/urandom bs=1 count=$n \
+ | ../../../../target/debug/examples/wrap-literal > literal-$n.asc
+ sq dearmor -o literal-$n.bin literal-$n.asc
+ grep -v - literal-$n.asc > literal-$n-no-header-with-chksum.asc
+ grep -v - literal-$n.asc | head -n -1 > literal-$n-no-header.asc
+ tr "\n" " " <literal-$n.asc > literal-$n-no-newlines.asc
+ done
diff --git a/openpgp/tests/data/armor/literal-0-no-header-with-chksum.asc b/openpgp/tests/data/armor/literal-0-no-header-with-chksum.asc
new file mode 100644
index 00000000..d700041d
--- /dev/null
+++ b/openpgp/tests/data/armor/literal-0-no-header-with-chksum.asc
@@ -0,0 +1,3 @@
+
+ywZiAAAAAAA=
+=JgGx