Skip to content

Commit

Permalink
Armored age encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
str4d committed Oct 26, 2019
1 parent ed9ff64 commit da92349
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 22 deletions.
6 changes: 6 additions & 0 deletions examples/generate-docs.rs
Expand Up @@ -24,6 +24,12 @@ fn rage_page() {
.long("--passphrase")
.help("Use a passphrase instead of public keys"),
)
.flag(
Flag::new()
.short("-A")
.long("--armor")
.help("Create ASCII armored output (default is age binary format)"),
)
.option(
Opt::new("input")
.short("-i")
Expand Down
5 changes: 4 additions & 1 deletion src/bin/rage/main.rs
Expand Up @@ -174,6 +174,9 @@ struct AgeOptions {

#[options(help = "use a passphrase instead of public keys")]
passphrase: bool,

#[options(help = "create ASCII armored output (default is age binary format)")]
armor: bool,
}

fn generate_new_key() {
Expand Down Expand Up @@ -229,7 +232,7 @@ fn encrypt(opts: AgeOptions) {
}
};

match encryptor.wrap_output(output) {
match encryptor.wrap_output(output, opts.armor) {
Ok(mut w) => {
if let Err(e) = io::copy(&mut input, &mut w) {
eprintln!("Error while encrypting: {}", e);
Expand Down
42 changes: 28 additions & 14 deletions src/format.rs
Expand Up @@ -4,7 +4,9 @@ use std::io::{self, Read, Write};

use crate::primitives::HmacWriter;

const V1_MAGIC: &[u8] = b"This is a file encrypted with age-tool.com, version 1";
const BINARY_MAGIC: &[u8] = b"This is a file";
const ARMORED_MAGIC: &[u8] = b"This is an armored file";
const V1_MAGIC: &[u8] = b"encrypted with age-tool.com, version 1";
const RECIPIENT_TAG: &[u8] = b"-> ";
const X25519_RECIPIENT_TAG: &[u8] = b"X25519 ";
const SCRYPT_RECIPIENT_TAG: &[u8] = b"scrypt ";
Expand Down Expand Up @@ -126,15 +128,19 @@ impl Header {
}
}

pub(crate) fn write<W: Write>(&self, mut output: W) -> io::Result<()> {
cookie_factory::gen(write::canonical_header(self), &mut output)
.map(|_| ())
.map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("failed to write header: {}", e),
)
})
pub(crate) fn write<W: Write>(&self, mut output: W, armored: bool) -> io::Result<()> {
if armored {
cookie_factory::gen(write::armored_header(self), &mut output)
} else {
cookie_factory::gen(write::canonical_header(self), &mut output)
}
.map(|_| ())
.map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("failed to write header: {}", e),
)
})
}
}

Expand Down Expand Up @@ -351,7 +357,10 @@ mod read {
}

pub(super) fn canonical_header(input: &[u8]) -> IResult<&[u8], Header> {
header(&nom::character::streaming::newline)(input)
preceded(
pair(tag(BINARY_MAGIC), tag(b" ")),
header(&nom::character::streaming::newline),
)(input)
}
}

Expand All @@ -365,6 +374,7 @@ mod write {
use std::io::Write;

use super::*;
use crate::util::LINE_ENDING;

fn encoded_data<W: Write>(data: &[u8]) -> impl SerializeFn<W> {
let encoded = base64::encode_config(data, base64::URL_SAFE_NO_PAD);
Expand Down Expand Up @@ -488,11 +498,15 @@ mod write {
pub(super) fn canonical_header_minus_mac<'a, W: 'a + Write>(
h: &'a Header,
) -> impl SerializeFn<W> + 'a {
header_minus_mac(h, "\n")
tuple((slice(BINARY_MAGIC), string(" "), header_minus_mac(h, "\n")))
}

pub(super) fn canonical_header<'a, W: 'a + Write>(h: &'a Header) -> impl SerializeFn<W> + 'a {
header(h, "\n")
tuple((slice(BINARY_MAGIC), string(" "), header(h, "\n")))
}

pub(super) fn armored_header<'a, W: 'a + Write>(h: &'a Header) -> impl SerializeFn<W> + 'a {
tuple((slice(ARMORED_MAGIC), string(" "), header(h, LINE_ENDING)))
}
}

Expand Down Expand Up @@ -523,7 +537,7 @@ fYCo_w
";
let h = Header::read(test_header.as_bytes()).unwrap();
let mut data = vec![];
h.write(&mut data).unwrap();
h.write(&mut data, false).unwrap();
assert_eq!(std::str::from_utf8(&data), Ok(test_header));
}
}
4 changes: 2 additions & 2 deletions src/lib.rs
Expand Up @@ -23,7 +23,7 @@
//! let encryptor = age::Encryptor::Keys(vec![pubkey]);
//! let mut encrypted = vec![];
//! {
//! let mut writer = encryptor.wrap_output(&mut encrypted)?;
//! let mut writer = encryptor.wrap_output(&mut encrypted, false)?;
//! writer.write_all(plaintext)?;
//! writer.flush()?;
//! };
Expand Down Expand Up @@ -51,7 +51,7 @@
//! let encryptor = age::Encryptor::Passphrase(passphrase.to_owned());
//! let mut encrypted = vec![];
//! {
//! let mut writer = encryptor.wrap_output(&mut encrypted)?;
//! let mut writer = encryptor.wrap_output(&mut encrypted, false)?;
//! writer.write_all(plaintext)?;
//! writer.flush()?;
//! };
Expand Down
14 changes: 9 additions & 5 deletions src/protocol.rs
Expand Up @@ -11,6 +11,7 @@ use crate::{
aead_decrypt, aead_encrypt, hkdf, scrypt,
stream::{Stream, StreamReader},
},
util::ArmoredWriter,
};

const HEADER_KEY_LABEL: &[u8] = b"header";
Expand Down Expand Up @@ -77,22 +78,25 @@ impl Encryptor {
}
}

/// Creates a wrapper around a writer that will encrypt its input.
/// Creates a wrapper around a writer that will encrypt its input, and optionally
/// ASCII armor the output.
///
/// Returns errors from the underlying writer while writing the header.
///
/// You **MUST** call `flush()` when you are done writing, in order to finish the
/// encryption process. Failing to call `flush()` will result in a truncated message
/// that will fail to decrypt.
pub fn wrap_output<W: Write>(&self, mut output: W) -> io::Result<impl Write> {
pub fn wrap_output<W: Write>(&self, mut output: W, armored: bool) -> io::Result<impl Write> {
let mut file_key = [0; 16];
getrandom(&mut file_key).expect("Should not fail");

let header = Header::new(
self.wrap_file_key(&file_key),
hkdf(&[], HEADER_KEY_LABEL, &file_key),
);
header.write(&mut output)?;
header.write(&mut output, armored)?;

let mut output = ArmoredWriter::wrap_output(output, armored);

let mut nonce = [0; 16];
getrandom(&mut nonce).expect("Should not fail");
Expand Down Expand Up @@ -233,7 +237,7 @@ _vLg6QnGTU5UQSVs3cUJDmVMJ1Qj07oSXntDpsqi0Zw
let mut encrypted = vec![];
let e = Encryptor::Keys(vec![pk]);
{
let mut w = e.wrap_output(&mut encrypted).unwrap();
let mut w = e.wrap_output(&mut encrypted, false).unwrap();
w.write_all(test_msg).unwrap();
w.flush().unwrap();
}
Expand All @@ -257,7 +261,7 @@ _vLg6QnGTU5UQSVs3cUJDmVMJ1Qj07oSXntDpsqi0Zw
let mut encrypted = vec![];
let e = Encryptor::Keys(vec![pk]);
{
let mut w = e.wrap_output(&mut encrypted).unwrap();
let mut w = e.wrap_output(&mut encrypted, false).unwrap();
w.write_all(test_msg).unwrap();
w.flush().unwrap();
}
Expand Down
112 changes: 112 additions & 0 deletions src/util.rs
Expand Up @@ -4,6 +4,14 @@ use nom::{
multi::separated_nonempty_list,
IResult,
};
use std::io::{self, Write};

#[cfg(windows)]
pub(crate) const LINE_ENDING: &str = "\r\n";
#[cfg(not(windows))]
pub(crate) const LINE_ENDING: &str = "\n";

This comment has been minimized.

Copy link
@riking

riking Nov 3, 2019

Windows and non-windows platforms need to agree on the line-ending they use and need to be cross-compatible when parsing, especially if you're signing the encrypted file afterwards.

This comment has been minimized.

Copy link
@str4d

str4d Nov 29, 2019

Author Owner

As @FiloSottile discussed on the last livestream, the now-specified armored format is intended to allow this kind of malleability. If someone wanted to sign the encrypted file afterwards, they would need to use the non-armored format.


const ARMORED_END_MARKER: &[u8] = b"***";

/// Returns the slice of input up to (but not including) the first newline
/// character, if that slice is entirely Base64 characters
Expand Down Expand Up @@ -109,3 +117,107 @@ pub(crate) fn read_wrapped_str_while_encoded(
)(input)
}
}

pub(crate) struct ArmoredWriter<W: Write> {
inner: W,
enabled: bool,
chunk: (Option<u8>, Option<u8>, Option<u8>),
line_length: usize,
}

impl<W: Write> ArmoredWriter<W> {
pub(crate) fn wrap_output(inner: W, enabled: bool) -> Self {
ArmoredWriter {
inner,
enabled,
chunk: (None, None, None),
line_length: 0,
}
}
}

impl<W: Write> Write for ArmoredWriter<W> {
fn write(&mut self, mut buf: &[u8]) -> io::Result<usize> {
if !self.enabled {
return self.inner.write(buf);
}

let mut bytes_written = 0;

while !buf.is_empty() {
let byte = buf[0];
buf = &buf[1..];
bytes_written += 1;

match self.chunk {
(None, None, None) => self.chunk.0 = Some(byte),
(Some(_), None, None) => self.chunk.1 = Some(byte),
(Some(_), Some(_), None) => self.chunk.2 = Some(byte),
(Some(a), Some(b), Some(c)) => {
// Wrap the line if needed
if self.line_length >= 56 {
self.inner.write_all(LINE_ENDING.as_bytes())?;
self.line_length = 0;
}

// Process the bytes we already have
let mut encoded = [0; 4];
assert_eq!(
base64::encode_config_slice(
&[a, b, c],
base64::URL_SAFE_NO_PAD,
&mut encoded
),
4
);
self.inner.write_all(&encoded)?;
self.line_length += 4;

// Store the new byte
self.chunk = (Some(byte), None, None);
}
_ => unreachable!(),
}
}

Ok(bytes_written)
}

fn flush(&mut self) -> io::Result<()> {
if self.enabled {
// Wrap the line if needed
if self.line_length >= 56 {
self.inner.write_all(LINE_ENDING.as_bytes())?;
self.line_length = 0;
}

// Process the remaining bytes
let mut encoded = [0; 4];
let encoded_size = match self.chunk {
(None, None, None) => 0,
(Some(a), None, None) => {
base64::encode_config_slice(&[a], base64::URL_SAFE_NO_PAD, &mut encoded)
}
(Some(a), Some(b), None) => {
base64::encode_config_slice(&[a, b], base64::URL_SAFE_NO_PAD, &mut encoded)
}
(Some(a), Some(b), Some(c)) => {
base64::encode_config_slice(&[a, b, c], base64::URL_SAFE_NO_PAD, &mut encoded)
}
_ => unreachable!(),
};
self.inner.write_all(&encoded[0..encoded_size])?;
self.line_length += encoded_size;

// Write a line ending if there is anything on the final line
if self.line_length > 0 {
self.inner.write_all(LINE_ENDING.as_bytes())?;
}

// Write the end marker
self.inner.write_all(ARMORED_END_MARKER)?;
self.inner.write_all(LINE_ENDING.as_bytes())?;
}
self.inner.flush()
}
}

0 comments on commit da92349

Please sign in to comment.