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>,
body: Vec<&'a [u8]>,
}
impl<'a> AgeStanza<'a> {
pub fn body(&self) -> Vec<u8> {
let (partial_chunk, full_chunks) = self.body.split_last().unwrap();
let mut data = vec![0; full_chunks.len() * 64 + partial_chunk.len()];
for (i, chunk) in full_chunks.iter().enumerate() {
data[i * 64..(i + 1) * 64].copy_from_slice(chunk);
}
data[full_chunks.len() * 64..].copy_from_slice(partial_chunk);
base64::decode_config(&data, base64::STANDARD_NO_PAD).unwrap()
}
}
#[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 {
let body = stanza.body();
Stanza {
tag: stanza.tag.to_string(),
args: stanza.args.into_iter().map(|s| s.to_string()).collect(),
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_while1, take_while_m_n},
character::streaming::newline,
combinator::{map, map_opt, opt},
multi::{many_till, separated_list1},
sequence::{pair, preceded, terminated},
IResult,
};
use super::{AgeStanza, STANZA_TAG};
fn is_base64_char(c: u8) -> bool {
matches!(
c,
65..=90 | 97..=122 | 48..=57 | 43 | 47,
)
}
pub fn arbitrary_string(input: &[u8]) -> IResult<&[u8], &str> {
map(take_while1(|c| (33..=126).contains(&c)), |bytes| {
unsafe { std::str::from_utf8_unchecked(bytes) }
})(input)
}
fn wrapped_encoded_data(input: &[u8]) -> IResult<&[u8], Vec<&[u8]>> {
map(
many_till(
terminated(take_while_m_n(64, 64, is_base64_char), newline),
terminated(take_while_m_n(0, 63, is_base64_char), newline),
),
|(full_chunks, partial_chunk): (Vec<&[u8]>, &[u8])| {
let mut chunks = full_chunks;
chunks.push(partial_chunk);
chunks
},
)(input)
}
fn legacy_wrapped_encoded_data(input: &[u8]) -> IResult<&[u8], Vec<&[u8]>> {
map_opt(
separated_list1(newline, take_while1(is_base64_char)),
|chunks: Vec<&[u8]>| {
let (partial_chunk, full_chunks) = chunks.split_last().unwrap();
if full_chunks.iter().any(|s| s.len() != 64) || partial_chunk.len() > 64 {
None
} else {
Some(chunks)
}
},
)(input)
}
pub fn age_stanza(input: &[u8]) -> IResult<&[u8], AgeStanza<'_>> {
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(input: &[u8]) -> IResult<&[u8], AgeStanza<'_>> {
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_else(|| vec![&[]]),
}
},
)(input)
}
pub fn legacy_age_stanza(input: &[u8]) -> IResult<&[u8], AgeStanza<'_>> {
alt((age_stanza, legacy_age_stanza_inner))(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_padding_rejected() {
assert!(wrapped_encoded_data(b"Tm8gcGFkZGluZyE\n").is_ok());
assert!(wrapped_encoded_data(b"Tm8gcGFkZGluZyE=\n").is_err());
assert!(wrapped_encoded_data(b"SW50ZXJuYWwUGFk\n").is_ok());
assert!(wrapped_encoded_data(b"SW50ZXJuYWw=UGFk\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());
}
#[test]
fn age_stanza_with_legacy_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
--- header end
";
assert!(read::age_stanza(test_stanza.as_bytes()).is_err());
let (_, stanza) = read::legacy_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);
}
}