Merge pull request #341 from str4d/testkit-armor-fixes

Testkit armor fixes
This commit is contained in:
str4d 2022-09-11 00:46:53 +01:00 committed by GitHub
commit d3aa905a61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 2004 additions and 59 deletions

1
Cargo.lock generated
View file

@ -77,6 +77,7 @@ dependencies = [
"i18n-embed",
"i18n-embed-fl",
"lazy_static",
"memchr",
"nom",
"num-traits",
"pin-project",

View file

@ -17,6 +17,18 @@ to 1.0.0 are beta releases.
- `age::Decryptor` now rejects invalid or non-canonical `scrypt` recipient
stanzas (instead of ignoring or accepting them respectively), matching the
[age specification](https://c2sp.org/age#scrypt-recipient-stanza).
- `age::armor::ArmoredReader`:
- It now correctly implements strict parsing as defined in
[RFC 7468](https://www.rfc-editor.org/rfc/rfc7468.html#section-3), and
rejects armored files with non-canonical final lines (where padding bytes
are omitted).
- It now rejects armored files with non-whitespace characters after the end
marker.
- It now accepts armored files with no newline after the end marker.
Previously these were rejected by the synchronous API, and would cause the
async API to hang.
- The async API now correctly rejects some classes of invalid armoring that
previously would cause it to hang.
## [0.8.1] - 2022-06-18
### Security

View file

@ -68,6 +68,7 @@ zeroize = "1"
# Async I/O
futures = { version = "0.3", optional = true }
memchr = { version = "2.5", optional = true }
pin-project = "1"
# Localization
@ -108,7 +109,7 @@ criterion-cycles-per-byte = "0.1"
[features]
default = []
armor = []
async = ["futures"]
async = ["futures", "memchr"]
cli-common = ["atty", "console", "pinentry", "rpassword"]
plugin = ["age-core/plugin", "which", "wsl"]
ssh = [
@ -131,7 +132,7 @@ required-features = ["ssh"]
[[test]]
name = "testkit"
required-features = ["async"]
required-features = ["armor", "async"]
[[bench]]
name = "parser"

View file

@ -16,7 +16,11 @@ use futures::{
task::{Context, Poll},
};
#[cfg(feature = "async")]
use std::mem;
#[cfg(feature = "async")]
use std::pin::Pin;
#[cfg(feature = "async")]
use std::str;
const ARMORED_COLUMNS_PER_LINE: usize = 64;
const ARMORED_BYTES_PER_LINE: usize = ARMORED_COLUMNS_PER_LINE / 4 * 3;
@ -525,17 +529,14 @@ pub enum ArmoredReadError {
InvalidUtf8,
/// A line of the armor contains a `\r` character.
LineContainsCr,
/// A line of the armor is missing a line ending.
///
/// In practice, this only enforces a newline after the end marker, because the parser
/// splits the input on newlines, so a missing line ending internally would instead be
/// interpreted as either `ArmoredReadError::LineContainsCr` or
/// `ArmoredReadError::NotWrappedAt64Chars`.
MissingLineEnding,
/// The final Base64 line is non-canonical.
MissingPadding,
/// The armor is not wrapped at 64 characters.
NotWrappedAt64Chars,
/// There is a short line in the middle of the armor (only the final line may be short).
ShortLineInMiddle,
/// There are trailing non-whitespace characters after the end marker.
TrailingGarbage,
}
impl fmt::Display for ArmoredReadError {
@ -545,13 +546,21 @@ impl fmt::Display for ArmoredReadError {
ArmoredReadError::InvalidBeginMarker => write!(f, "invalid armor begin marker"),
ArmoredReadError::InvalidUtf8 => write!(f, "stream did not contain valid UTF-8"),
ArmoredReadError::LineContainsCr => write!(f, "line contains CR"),
ArmoredReadError::MissingLineEnding => write!(f, "missing line ending"),
ArmoredReadError::MissingPadding => {
write!(f, "invalid armor (last line is missing padding)")
}
ArmoredReadError::NotWrappedAt64Chars => {
write!(f, "invalid armor (not wrapped at 64 characters)")
}
ArmoredReadError::ShortLineInMiddle => {
write!(f, "invalid armor (short line in middle of encoding)")
}
ArmoredReadError::TrailingGarbage => {
write!(
f,
"invalid armor (non-whitespace characters after end marker)"
)
}
}
}
}
@ -737,10 +746,11 @@ impl<R> ArmoredReader<R> {
} else if self.line_buf.ends_with('\n') {
self.line_buf.trim_end_matches('\n')
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
ArmoredReadError::MissingLineEnding,
));
// If the line does not end in a `\n`, then it must be the final line in the
// file, because we parse the file into lines by splitting on `\n`. This will
// either be an invalid line (and be caught as a different error), or the end
// marker (which we allow to omit a trailing `\n`).
&self.line_buf
};
if line.contains('\r') {
return Err(io::Error::new(
@ -757,6 +767,15 @@ impl<R> ArmoredReader<R> {
} else {
match (self.found_short_line, line.len()) {
(false, ARMORED_COLUMNS_PER_LINE) => (),
(false, n) if n % 4 != 0 => {
// The `base64` crate does not (yet) support canonical decoding.
// Handle this ourselves until the upstream issue is closed:
// https://github.com/marshallpierce/rust-base64/issues/182
return Err(io::Error::new(
io::ErrorKind::InvalidData,
ArmoredReadError::MissingPadding,
));
}
(false, n) if n < ARMORED_COLUMNS_PER_LINE => {
// The format may contain a single short line at the end.
self.found_short_line = true;
@ -833,7 +852,22 @@ impl<R: BufRead> Read for ArmoredReader<R> {
// Parse the line into bytes
if self.parse_armor_line()? {
// This was the last line!
// This was the last line! Check for trailing garbage.
loop {
let amt = match self.inner.fill_buf()? {
&[] => break,
buf => {
if buf.iter().any(|b| !b.is_ascii_whitespace()) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
ArmoredReadError::TrailingGarbage,
));
}
buf.len()
}
};
self.inner.consume(amt);
}
break;
}
@ -847,6 +881,61 @@ impl<R: BufRead> Read for ArmoredReader<R> {
}
}
/// Copied from `futures_util::io::read_until::read_until_internal`.
#[cfg(feature = "async")]
fn read_until_internal<R: AsyncBufRead + ?Sized>(
mut reader: Pin<&mut R>,
cx: &mut Context<'_>,
byte: u8,
buf: &mut Vec<u8>,
read: &mut usize,
) -> Poll<io::Result<usize>> {
loop {
let (done, used) = {
let available = ready!(reader.as_mut().poll_fill_buf(cx))?;
if let Some(i) = memchr::memchr(byte, available) {
buf.extend_from_slice(&available[..=i]);
(true, i + 1)
} else {
buf.extend_from_slice(available);
(false, available.len())
}
};
reader.as_mut().consume(used);
*read += used;
if done || used == 0 {
return Poll::Ready(Ok(mem::replace(read, 0)));
}
}
}
/// Adapted from `futures_util::io::read_line::read_line_internal`.
#[cfg(feature = "async")]
fn read_line_internal<R: AsyncBufRead + ?Sized>(
reader: Pin<&mut R>,
cx: &mut Context<'_>,
buf: &mut String,
bytes: &mut Vec<u8>,
read: &mut usize,
) -> Poll<io::Result<usize>> {
let ret = ready!(read_until_internal(reader, cx, b'\n', bytes, read));
match String::from_utf8(mem::take(bytes)) {
Err(_) => Poll::Ready(ret.and_then(|_| {
Err(io::Error::new(
io::ErrorKind::InvalidData,
ArmoredReadError::InvalidUtf8,
))
})),
Ok(mut line) => {
debug_assert!(buf.is_empty());
debug_assert_eq!(*read, 0);
// Safety: `bytes` is a valid UTF-8 because `str::from_utf8` returned `Ok`.
mem::swap(buf, &mut line);
Poll::Ready(ret)
}
}
}
#[cfg(feature = "async")]
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
impl<R: AsyncBufRead + Unpin> AsyncRead for ArmoredReader<R> {
@ -891,34 +980,40 @@ impl<R: AsyncBufRead + Unpin> AsyncRead for ArmoredReader<R> {
// Read the next line
{
// Emulates `AsyncBufReadExt::read_line`.
let mut this = self.as_mut().project();
let available = loop {
let buf = ready!(this.inner.as_mut().poll_fill_buf(cx))?;
if buf.contains(&b'\n') {
break buf;
}
};
let pos = available
.iter()
.position(|c| *c == b'\n')
.expect("contains LF byte")
+ 1;
this.line_buf
.push_str(std::str::from_utf8(&available[..pos]).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
ArmoredReadError::InvalidUtf8,
)
})?);
this.inner.as_mut().consume(pos);
self.count_reader_bytes(pos);
let buf: &mut String = &mut this.line_buf;
let mut bytes = mem::take(buf).into_bytes();
let mut read = 0;
ready!(read_line_internal(
this.inner.as_mut(),
cx,
buf,
&mut bytes,
&mut read,
))
}
.map(|read| self.count_reader_bytes(read))?;
// Parse the line into bytes.
let read = if self.parse_armor_line()? {
// This was the last line!
// This was the last line! Check for trailing garbage.
let mut this = self.as_mut().project();
loop {
let amt = match ready!(this.inner.as_mut().poll_fill_buf(cx))? {
&[] => break,
buf => {
if buf.iter().any(|b| !b.is_ascii_whitespace()) {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidData,
ArmoredReadError::TrailingGarbage,
)));
}
buf.len()
}
};
this.inner.as_mut().consume(amt);
}
0
} else {
// Output as much as we can of this line.

View file

@ -198,10 +198,7 @@ impl Stream {
assert!(chunk.len() <= ENCRYPTED_CHUNK_SIZE);
self.nonce.set_last(last).map_err(|_| {
io::Error::new(
io::ErrorKind::UnexpectedEof,
"last chunk has been processed",
)
io::Error::new(io::ErrorKind::InvalidData, "last chunk has been processed")
})?;
let decrypted = self
@ -711,11 +708,11 @@ mod tests {
// Further calls return an error
match s.decrypt_chunk(&encrypted, false) {
Err(e) => assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof),
Err(e) => assert_eq!(e.kind(), io::ErrorKind::InvalidData),
_ => panic!("Expected error"),
}
match s.decrypt_chunk(&encrypted, true) {
Err(e) => assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof),
Err(e) => assert_eq!(e.kind(), io::ErrorKind::InvalidData),
_ => panic!("Expected error"),
}

13
age/tests/testdata/testkit/armor vendored Normal file
View file

@ -0,0 +1,13 @@
expect: success
payload: 013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

14
age/tests/testdata/testkit/armor_crlf vendored Normal file
View file

@ -0,0 +1,14 @@
expect: success
payload: 013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
comment: CRLF is allowed as a end of line for armored files
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,13 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,13 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,13 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW2ewwwqo
mNlxYv6gMOKyDNzgiw=
=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,13 @@
expect: success
payload: 724a112a2cac139a4fca3ea0f799f2e5ccd1d0db46af654dee40567bff16ee33
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW3bj4iHS
YS3WWUtZB5wJqKgEe8kpsp0iOnD2CNG4DVKBC0Z7SAcCFb8xdwV9CRavSEE7OU1c
-----END AGE ENCRYPTED FILE-----

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
garbage
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,13 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----
garbage

View file

@ -0,0 +1,13 @@
expect: header failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0
armored: yes
comment: lines in the header end with CRLF instead of LF
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxDQotPiBYMjU1MTkgVEVpRjB5cHFyK2JwdmNx
WE55Q1ZKcEw3T3V3UGRWd1BMN0tRRWJGRE9DYw0KaGphYkdYd1NMUTljM1M2THcy
aStTMlR1MmZpd1FISHNsYkJONkI0MUZMRQ0KLS0tIDJLSUdiN3llMzJNV3RVdUVW
V2tPM01QNnFDREx6T3ZUOXdGMDZsZWxCU0kNCu7PYsfOkbQzJ05o1PL5E0y3TFv+
976qUsjwvA6ZLB6DMftm
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,15 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
Headers: are
Not: allowed
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,12 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdl*WVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,12 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
*PC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,8 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FYTnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lWK0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkzZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpSyPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,12 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN age ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END age ENCRYPTED FILE-----

View file

@ -0,0 +1,11 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=

14
age/tests/testdata/testkit/armor_no_eol vendored Normal file
View file

@ -0,0 +1,14 @@
expect: success
payload: 013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
comment: there is no end of line at the end of the file
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,12 @@
expect: no match
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-143WN7DCXU4G8R5AXQSSYD9AEPYDNT3HXSLWSPK36CDU6E8M59SSSAGZ3KG
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhanRxQXZERWtWTnIyQjd6
VU90cTJtQVFYRFNCbE5yVkF1TS9kS2I1c1Q0CkhVS3R6MFIyajVCbDJFUjdIaEFa
clVSaWtDRnBpSWpOYTBLakhjamJBR1UKLS0tIHJycFRsdktFS3JLM0VxaG9PUEpl
UDFLRThPMWQyYXJyUmV6Nzdtd2VrUmMK3d9y0G+8q1ffPQ0xJJatIYzX/W+AeLv4
gS3YeUcVXre9Xog=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,13 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
comment: missing base64 padding
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,13 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
comment: base64 is not canonical
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Z=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,14 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
=J2ub
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,12 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRp
b24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FYTnlDVkpwTDdPdXdQ
ZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lWK0h1MHIrRThSNzdE
ZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkzZjFzcUhqbHUvejFM
Q1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpSyPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,12 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
----- BEGIN AGE ENCRYPTED FILE -----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,12 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
----- END AGE ENCRYPTED FILE -----

View file

@ -0,0 +1,12 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,12 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,12 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,18 @@
expect: success
payload: 013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
comment: whitespace is allowed before and after armored files
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----

View file

@ -0,0 +1,12 @@
expect: armor failure
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
armored: yes
-----BEGIN AGE ENCRYPTED MESSAGE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED MESSAGE-----

View file

@ -4,11 +4,44 @@ use std::{
str::FromStr,
};
use age::{secrecy::SecretString, x25519, DecryptError, Decryptor, Identity};
use age::{
armor::{ArmoredReadError, ArmoredReader},
secrecy::SecretString,
x25519, DecryptError, Decryptor, Identity,
};
use futures::AsyncReadExt;
use sha2::{Digest, Sha256};
use test_case::test_case;
#[test_case("armor")]
#[test_case("armor_crlf")]
#[test_case("armor_empty_line_begin")]
#[test_case("armor_empty_line_end")]
#[test_case("armor_eol_between_padding")]
#[test_case("armor_full_last_line")]
#[test_case("armor_garbage_encoded")]
#[test_case("armor_garbage_leading")]
#[test_case("armor_garbage_trailing")]
#[test_case("armor_header_crlf")]
#[test_case("armor_headers")]
#[test_case("armor_invalid_character_header")]
#[test_case("armor_invalid_character_payload")]
#[test_case("armor_long_line")]
#[test_case("armor_lowercase")]
#[test_case("armor_no_end_line")]
#[test_case("armor_no_eol")]
#[test_case("armor_no_match")]
#[test_case("armor_no_padding")]
#[test_case("armor_not_canonical")]
#[test_case("armor_pgp_checksum")]
#[test_case("armor_short_line")]
#[test_case("armor_whitespace_begin")]
#[test_case("armor_whitespace_end")]
#[test_case("armor_whitespace_eol")]
#[test_case("armor_whitespace_last_line")]
#[test_case("armor_whitespace_line_start")]
#[test_case("armor_whitespace_outside")]
#[test_case("armor_wrong_type")]
#[test_case("header_crlf")]
#[test_case("hmac_bad")]
#[test_case("hmac_extra_space")]
@ -98,7 +131,7 @@ fn testkit(filename: &str) {
let testfile = TestFile::parse(filename);
let comment = format_testkit_comment(&testfile);
match Decryptor::new(&testfile.age_file[..]).and_then(|d| match d {
match Decryptor::new(ArmoredReader::new(&testfile.age_file[..])).and_then(|d| match d {
Decryptor::Recipients(d) => {
let identities = get_testkit_identities(filename, &testfile);
d.decrypt(identities.iter().map(|i| i as &dyn Identity))
@ -110,13 +143,42 @@ fn testkit(filename: &str) {
}) {
Ok(mut r) => {
let mut payload = vec![];
let res = io::Read::read_to_end(&mut r, &mut payload);
let res = r.read_to_end(&mut payload);
check_decrypt_success(filename, testfile, &comment, res, &payload);
}
Err(e) => check_decrypt_error(testfile, e),
Err(e) => check_decrypt_error(filename, testfile, e),
}
}
#[test_case("armor")]
#[test_case("armor_crlf")]
#[test_case("armor_empty_line_begin")]
#[test_case("armor_empty_line_end")]
#[test_case("armor_eol_between_padding")]
#[test_case("armor_full_last_line")]
#[test_case("armor_garbage_encoded")]
#[test_case("armor_garbage_leading")]
#[test_case("armor_garbage_trailing")]
#[test_case("armor_header_crlf")]
#[test_case("armor_headers")]
#[test_case("armor_invalid_character_header")]
#[test_case("armor_invalid_character_payload")]
#[test_case("armor_long_line")]
#[test_case("armor_lowercase")]
#[test_case("armor_no_end_line")]
#[test_case("armor_no_eol")]
#[test_case("armor_no_match")]
#[test_case("armor_no_padding")]
#[test_case("armor_not_canonical")]
#[test_case("armor_pgp_checksum")]
#[test_case("armor_short_line")]
#[test_case("armor_whitespace_begin")]
#[test_case("armor_whitespace_end")]
#[test_case("armor_whitespace_eol")]
#[test_case("armor_whitespace_last_line")]
#[test_case("armor_whitespace_line_start")]
#[test_case("armor_whitespace_outside")]
#[test_case("armor_wrong_type")]
#[test_case("header_crlf")]
#[test_case("hmac_bad")]
#[test_case("hmac_extra_space")]
@ -207,7 +269,7 @@ async fn testkit_async(filename: &str) {
let testfile = TestFile::parse(filename);
let comment = format_testkit_comment(&testfile);
match Decryptor::new_async(&testfile.age_file[..])
match Decryptor::new_async(ArmoredReader::from_async_reader(&testfile.age_file[..]))
.await
.and_then(|d| match d {
Decryptor::Recipients(d) => {
@ -221,10 +283,10 @@ async fn testkit_async(filename: &str) {
}) {
Ok(mut r) => {
let mut payload = vec![];
let res = AsyncReadExt::read_to_end(&mut r, &mut payload).await;
let res = r.read_to_end(&mut payload).await;
check_decrypt_success(filename, testfile, &comment, res, &payload);
}
Err(e) => check_decrypt_error(testfile, e),
Err(e) => check_decrypt_error(filename, testfile, e),
}
}
@ -277,6 +339,13 @@ fn check_decrypt_success(
// parsing legacy age stanzas without an explicit short final line.
(Ok(_), Expect::HeaderFailure)
if ["stanza_missing_body", "stanza_missing_final_line"].contains(&filename) => {}
(Err(e), Expect::ArmorFailure) => {
assert_eq!(e.kind(), io::ErrorKind::InvalidData);
assert_eq!(
e.into_inner().map(|inner| inner.is::<ArmoredReadError>()),
Some(true)
);
}
(Err(e), Expect::PayloadFailure { payload_sha256 }) => {
assert_eq!(
e.kind(),
@ -284,9 +353,6 @@ fn check_decrypt_success(
"stream_no_chunks",
"stream_no_final_full",
"stream_no_final_two_chunks_full",
"stream_trailing_garbage_long",
"stream_trailing_garbage_short",
"stream_two_final_chunks",
]
.contains(&filename)
{
@ -312,12 +378,44 @@ fn check_decrypt_success(
}
}
fn check_decrypt_error(testfile: TestFile, e: DecryptError) {
fn check_decrypt_error(filename: &str, testfile: TestFile, e: DecryptError) {
match e {
DecryptError::ExcessiveWork { .. }
| DecryptError::InvalidHeader
| DecryptError::Io(_)
| DecryptError::UnknownFormat => {
DecryptError::InvalidHeader => {
// `ArmoredReader` is a transparent wrapper around an `io::Read` and
// only runs de-armoring if it detects the expected begin marker.
// This leaves a hole in error detection: if the begin marker is invalid,
// then the test case will be rejected by the inner age header parsing
// with `DecryptError::InvalidHeader`. However, we can't simply treat
// these as "armor failed" because there are test cases where the armor is
// valid but the contained age file is invalid. We hard-code the list of
// test cases with invalid begin markers to cover this hole.
if testfile.armored
&& [
"armor_garbage_leading",
"armor_lowercase",
"armor_whitespace_begin",
"armor_wrong_type",
]
.contains(&filename)
{
assert_eq!(testfile.expect, Expect::ArmorFailure);
} else if testfile.armored && ["armor_whitespace_outside"].contains(&filename) {
// This decryption error is expected, because we do not support parsing
// armored files with leading whitespace (due to how we detect armoring).
} else {
assert_eq!(testfile.expect, Expect::HeaderFailure);
}
}
DecryptError::Io(e) => {
let kind = e.kind();
if e.into_inner().map(|inner| inner.is::<ArmoredReadError>()) == Some(true) {
assert_eq!(kind, io::ErrorKind::InvalidData);
assert_eq!(testfile.expect, Expect::ArmorFailure);
} else {
assert_eq!(testfile.expect, Expect::HeaderFailure);
}
}
DecryptError::ExcessiveWork { .. } | DecryptError::UnknownFormat => {
assert_eq!(testfile.expect, Expect::HeaderFailure)
}
DecryptError::InvalidMac => assert_eq!(testfile.expect, Expect::HmacFailure),
@ -335,6 +433,7 @@ fn check_decrypt_error(testfile: TestFile, e: DecryptError) {
#[derive(Debug, PartialEq, Eq)]
enum Expect {
Success { payload_sha256: [u8; 32] },
ArmorFailure,
HeaderFailure,
HmacFailure,
NoMatch,
@ -345,6 +444,7 @@ struct TestFile {
expect: Expect,
identities: Vec<String>,
passphrases: Vec<String>,
armored: bool,
comment: Option<String>,
age_file: Vec<u8>,
}
@ -370,6 +470,7 @@ impl TestFile {
payload_sha256: hex::decode(payload).unwrap().try_into().unwrap(),
}
}
"armor failure" => Expect::ArmorFailure,
"header failure" => Expect::HeaderFailure,
"payload failure" => {
line.clear();
@ -393,6 +494,7 @@ impl TestFile {
let mut identities = vec![];
let mut passphrases = vec![];
let mut armored = false;
let mut comment = None;
loop {
line.clear();
@ -405,6 +507,7 @@ impl TestFile {
match prefix {
"identity" => identities.push(data.to_owned()),
"passphrase" => passphrases.push(data.to_owned()),
"armored" => armored = data == "yes",
"comment" => comment = Some(data.to_owned()),
_ => panic!("Unknown testkit metadata '{}'", prefix),
}
@ -417,6 +520,7 @@ impl TestFile {
expect,
identities,
passphrases,
armored,
comment,
age_file,
}