mirror of
https://github.com/str4d/rage.git
synced 2025-04-07 04:47:42 +03:00
Armored age encryption
This commit is contained in:
parent
ed9ff6421b
commit
da92349774
6 changed files with 161 additions and 22 deletions
|
@ -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")
|
||||
|
|
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
|
|
@ -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 ";
|
||||
|
@ -126,8 +128,12 @@ impl Header {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write<W: Write>(&self, mut output: W) -> io::Result<()> {
|
||||
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(
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()?;
|
||||
//! };
|
||||
|
@ -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()?;
|
||||
//! };
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::{
|
|||
aead_decrypt, aead_encrypt, hkdf, scrypt,
|
||||
stream::{Stream, StreamReader},
|
||||
},
|
||||
util::ArmoredWriter,
|
||||
};
|
||||
|
||||
const HEADER_KEY_LABEL: &[u8] = b"header";
|
||||
|
@ -77,14 +78,15 @@ 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");
|
||||
|
||||
|
@ -92,7 +94,9 @@ impl Encryptor {
|
|||
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");
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
|
112
src/util.rs
112
src/util.rs
|
@ -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";
|
||||
|
||||
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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue