use rand::{
distributions::{Distribution, Uniform},
thread_rng, RngCore,
};
use secrecy::{ExposeSecret, Secret};
const STANZA_TAG: &str = "-> ";
pub const FILE_KEY_BYTES: usize = 16;
pub struct FileKey(Secret<[u8; FILE_KEY_BYTES]>);
impl From<[u8; FILE_KEY_BYTES]> for FileKey {
fn from(file_key: [u8; FILE_KEY_BYTES]) -> Self {
FileKey(Secret::new(file_key))
}
}
impl ExposeSecret<[u8; FILE_KEY_BYTES]> for FileKey {
fn expose_secret(&self) -> &[u8; FILE_KEY_BYTES] {
self.0.expose_secret()
}
}
#[derive(Debug)]
pub struct AgeStanza<'a> {
pub tag: &'a str,
pub args: Vec<&'a str>,
pub body: Vec<u8>,
}
#[derive(Debug, PartialEq)]
pub struct Stanza {
pub tag: String,
pub args: Vec<String>,
pub body: Vec<u8>,
}
impl From<AgeStanza<'_>> for Stanza {
fn from(stanza: AgeStanza<'_>) -> Self {
Stanza {
tag: stanza.tag.to_string(),
args: stanza.args.into_iter().map(|s| s.to_string()).collect(),
body: stanza.body,
}
}
}
pub fn grease_the_joint() -> Stanza {
fn gen_arbitrary_string<R: RngCore>(rng: &mut R) -> String {
let length = Uniform::from(1..9).sample(rng);
Uniform::from(33..=126)
.sample_iter(rng)
.map(char::from)
.take(length)
.collect()
}
let mut rng = thread_rng();
let tag = format!("{}-grease", gen_arbitrary_string(&mut rng));
let args = (0..Uniform::from(0..5).sample(&mut rng))
.map(|_| gen_arbitrary_string(&mut rng))
.collect();
let mut body = vec![0; Uniform::from(0..100).sample(&mut rng)];
rng.fill_bytes(&mut body);
Stanza { tag, args, body }
}
pub mod read {
use nom::{
branch::alt,
bytes::streaming::{tag, take_while, take_while1},
character::streaming::newline,
combinator::{map, map_opt, opt, verify},
multi::{many0, separated_list1},
sequence::{pair, preceded, terminated},
IResult,
};
use super::{AgeStanza, STANZA_TAG};
pub fn arbitrary_string(input: &[u8]) -> IResult<&[u8], &str> {
map(take_while1(|c| c >= 33 && c <= 126), |bytes| {
std::str::from_utf8(bytes).expect("ASCII is valid UTF-8")
})(input)
}
fn take_b64_line(input: &[u8]) -> IResult<&[u8], &[u8]> {
verify(take_while(|c| c != b'\n'), |bytes: &[u8]| {
base64::decode_config(bytes, base64::STANDARD_NO_PAD).is_ok() && !bytes.contains(&b'=')
})(input)
}
fn take_b64_line1(input: &[u8]) -> IResult<&[u8], &[u8]> {
verify(take_while1(|c| c != b'\n'), |bytes: &[u8]| {
base64::decode_config(bytes, base64::STANDARD_NO_PAD).is_ok() && !bytes.contains(&b'=')
})(input)
}
fn wrapped_encoded_data(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
map_opt(
pair(
many0(map_opt(terminated(take_b64_line, newline), |chunk| {
if chunk.len() != 64 {
None
} else {
Some(chunk)
}
})),
map_opt(terminated(take_b64_line, newline), |chunk| {
if chunk.len() < 64 {
Some(chunk)
} else {
None
}
}),
),
|(full_chunks, partial_chunk)| {
let data: Vec<u8> = full_chunks
.into_iter()
.chain(Some(partial_chunk))
.flatten()
.cloned()
.collect();
base64::decode_config(&data, base64::STANDARD_NO_PAD).ok()
},
)(input)
}
fn legacy_wrapped_encoded_data(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
map_opt(separated_list1(newline, take_b64_line1), |chunks| {
if chunks.iter().rev().skip(1).any(|s| s.len() != 64)
|| chunks.last().map(|s| s.len() > 64) == Some(true)
{
None
} else {
let data: Vec<u8> = chunks.into_iter().flatten().cloned().collect();
base64::decode_config(&data, base64::STANDARD_NO_PAD).ok()
}
})(input)
}
pub fn age_stanza<'a>(input: &'a [u8]) -> IResult<&'a [u8], AgeStanza<'a>> {
map(
pair(
preceded(
tag(STANZA_TAG),
terminated(separated_list1(tag(" "), arbitrary_string), newline),
),
wrapped_encoded_data,
),
|(mut args, body)| {
let tag = args.remove(0);
AgeStanza { tag, args, body }
},
)(input)
}
fn legacy_age_stanza_inner<'a>(input: &'a [u8]) -> IResult<&'a [u8], AgeStanza<'a>> {
map(
pair(
preceded(tag(STANZA_TAG), separated_list1(tag(" "), arbitrary_string)),
terminated(opt(preceded(newline, legacy_wrapped_encoded_data)), newline),
),
|(mut args, body)| {
let tag = args.remove(0);
AgeStanza {
tag,
args,
body: body.unwrap_or_default(),
}
},
)(input)
}
pub fn legacy_age_stanza<'a>(input: &'a [u8]) -> IResult<&'a [u8], AgeStanza<'a>> {
alt((age_stanza, legacy_age_stanza_inner))(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_padding_rejected() {
assert!(take_b64_line(b"Tm8gcGFkZGluZyE\n").is_ok());
assert!(take_b64_line(b"Tm8gcGFkZGluZyE=\n").is_err());
}
}
}
pub mod write {
use cookie_factory::{
combinator::string,
multi::separated_list,
sequence::{pair, tuple},
SerializeFn, WriteContext,
};
use std::io::Write;
use std::iter;
use super::STANZA_TAG;
fn wrapped_encoded_data<'a, W: 'a + Write>(data: &[u8]) -> impl SerializeFn<W> + 'a {
let encoded = base64::encode_config(data, base64::STANDARD_NO_PAD);
move |mut w: WriteContext<W>| {
let mut s = encoded.as_str();
while s.len() >= 64 {
let (l, r) = s.split_at(64);
w = pair(string(l), string("\n"))(w)?;
s = r;
}
pair(string(s), string("\n"))(w)
}
}
pub fn age_stanza<'a, W: 'a + Write, S: AsRef<str>>(
tag: &'a str,
args: &'a [S],
body: &'a [u8],
) -> impl SerializeFn<W> + 'a {
pair(
tuple((
string(STANZA_TAG),
separated_list(
string(" "),
iter::once(tag)
.chain(args.iter().map(|s| s.as_ref()))
.map(string),
),
string("\n"),
)),
wrapped_encoded_data(body),
)
}
}
#[cfg(test)]
mod tests {
use super::{read, write};
#[test]
fn parse_age_stanza() {
let test_tag = "X25519";
let test_args = &["CJM36AHmTbdHSuOQL+NESqyVQE75f2e610iRdLPEN20"];
let test_body = base64::decode_config(
"C3ZAeY64NXS4QFrksLm3EGz+uPRyI0eQsWw7LWbbYig",
base64::STANDARD_NO_PAD,
)
.unwrap();
let test_stanza = "-> X25519 CJM36AHmTbdHSuOQL+NESqyVQE75f2e610iRdLPEN20
C3ZAeY64NXS4QFrksLm3EGz+uPRyI0eQsWw7LWbbYig
";
let (_, stanza) = read::age_stanza(test_stanza.as_bytes()).unwrap();
assert_eq!(stanza.tag, test_tag);
assert_eq!(stanza.args, test_args);
assert_eq!(stanza.body, test_body);
let mut buf = vec![];
cookie_factory::gen_simple(write::age_stanza(test_tag, test_args, &test_body), &mut buf)
.unwrap();
assert_eq!(buf, test_stanza.as_bytes());
}
#[test]
fn age_stanza_with_empty_body() {
let test_tag = "empty-body";
let test_args = &["some", "arguments"];
let test_body = &[];
let test_stanza = "-> empty-body some arguments
";
let (_, stanza) = read::age_stanza(test_stanza.as_bytes()).unwrap();
assert_eq!(stanza.tag, test_tag);
assert_eq!(stanza.args, test_args);
assert_eq!(stanza.body, test_body);
let mut buf = vec![];
cookie_factory::gen_simple(write::age_stanza(test_tag, test_args, test_body), &mut buf)
.unwrap();
assert_eq!(buf, test_stanza.as_bytes());
}
#[test]
fn age_stanza_with_full_body() {
let test_tag = "full-body";
let test_args = &["some", "arguments"];
let test_body = base64::decode_config(
"xD7o4VEOu1t7KZQ1gDgq2FPzBEeSRqbnqvQEXdLRYy143BxR6oFxsUUJCRB0ErXA",
base64::STANDARD_NO_PAD,
)
.unwrap();
let test_stanza = "-> full-body some arguments
xD7o4VEOu1t7KZQ1gDgq2FPzBEeSRqbnqvQEXdLRYy143BxR6oFxsUUJCRB0ErXA
";
let (_, stanza) = read::age_stanza(test_stanza.as_bytes()).unwrap();
assert_eq!(stanza.tag, test_tag);
assert_eq!(stanza.args, test_args);
assert_eq!(stanza.body, test_body);
let mut buf = vec![];
cookie_factory::gen_simple(write::age_stanza(test_tag, test_args, &test_body), &mut buf)
.unwrap();
assert_eq!(buf, test_stanza.as_bytes());
}
}