use std::collections::HashSet;
use vitaminc_aead::{CipherText, Frame, Node, PassthroughFrame};
pub use vitaminc_aead::frame::{ARRAY, MAX_DEPTH, OBJECT, PASSTHROUGH};
pub const CT_SINGLE: u8 = 0x01;
pub const CT_SEQ: u8 = 0x03;
pub const CT_MAP: u8 = 0x04;
pub const CT_PASSTHROUGH: u8 = 0x05;
pub const CT_EMPTY_SEQ: u8 = 0x06;
pub const CT_EMPTY_MAP: u8 = 0x07;
#[cfg(test)]
const CT_NONE_RETIRED: u8 = 0x02;
fn encode_passthrough(
payload: &PassthroughFrame,
out: &mut Vec<u8>,
depth: usize,
) -> Result<(), CodecError> {
let (_, rest) = Frame::parse_nested(&payload.0, depth).map_err(|_| CodecError)?;
if !rest.is_empty() {
return Err(CodecError);
}
out.extend_from_slice(&payload.0);
Ok(())
}
fn decode_passthrough(
reader: &mut Reader<'_>,
depth: usize,
) -> Result<PassthroughFrame, CodecError> {
let (frame, _) = Frame::parse_nested(reader.remaining(), depth).map_err(|_| CodecError)?;
let len = frame.as_bytes().len();
Ok(PassthroughFrame(reader.take(len)?.to_vec()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CodecError;
impl core::fmt::Display for CodecError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("malformed ciphertext transport")
}
}
impl std::error::Error for CodecError {}
struct Reader<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
fn new(buf: &'a [u8]) -> Self {
Reader { buf, pos: 0 }
}
fn byte(&mut self) -> Result<u8, CodecError> {
let b = *self.buf.get(self.pos).ok_or(CodecError)?;
self.pos += 1;
Ok(b)
}
fn take(&mut self, len: usize) -> Result<&'a [u8], CodecError> {
let end = self.pos.checked_add(len).ok_or(CodecError)?;
let slice = self.buf.get(self.pos..end).ok_or(CodecError)?;
self.pos = end;
Ok(slice)
}
fn u32(&mut self) -> Result<u32, CodecError> {
let bytes: [u8; 4] = self.take(4)?.try_into().map_err(|_| CodecError)?;
Ok(u32::from_le_bytes(bytes))
}
fn remaining(&self) -> &'a [u8] {
self.buf.get(self.pos..).unwrap_or(&[])
}
fn count(&mut self) -> Result<usize, CodecError> {
let n = self.u32()? as usize;
if n > self.remaining().len() {
return Err(CodecError);
}
Ok(n)
}
fn finished(&self) -> bool {
self.pos == self.buf.len()
}
}
const MAX_EAGER_CAPACITY: usize = 1024;
fn eager_capacity(count: usize) -> usize {
count.min(MAX_EAGER_CAPACITY)
}
fn write_len(out: &mut Vec<u8>, len: usize) -> Result<(), CodecError> {
let len: u32 = len.try_into().map_err(|_| CodecError)?;
out.extend_from_slice(&len.to_le_bytes());
Ok(())
}
fn write_bytes(out: &mut Vec<u8>, bytes: &[u8]) -> Result<(), CodecError> {
write_len(out, bytes.len())?;
out.extend_from_slice(bytes);
Ok(())
}
fn decode_key(reader: &mut Reader<'_>) -> Result<String, CodecError> {
let len = reader.count()?;
let bytes = reader.take(len)?;
String::from_utf8(bytes.to_vec()).map_err(|_| CodecError)
}
pub fn encode_ciphertext<Leaf>(ct: &Node<Leaf>, out: &mut Vec<u8>) -> Result<(), CodecError>
where
Leaf: AsRef<[u8]>,
{
let start = out.len();
encode_ciphertext_inner(ct, out, 0).inspect_err(|_| out.truncate(start))
}
fn encode_ciphertext_inner<Leaf>(
ct: &Node<Leaf>,
out: &mut Vec<u8>,
depth: usize,
) -> Result<(), CodecError>
where
Leaf: AsRef<[u8]>,
{
if depth > MAX_DEPTH {
return Err(CodecError);
}
match ct {
CipherText::Single(leaf) => {
out.push(CT_SINGLE);
write_bytes(out, leaf.as_ref())?;
}
CipherText::EmptySequence(leaf) => {
out.push(CT_EMPTY_SEQ);
write_bytes(out, leaf.as_ref())?;
}
CipherText::EmptyMap(leaf) => {
out.push(CT_EMPTY_MAP);
write_bytes(out, leaf.as_ref())?;
}
CipherText::Sequence(items) => {
out.push(CT_SEQ);
write_len(out, items.len())?;
for item in items {
encode_ciphertext_inner(item, out, depth + 1)?;
}
}
CipherText::Map(entries) => {
out.push(CT_MAP);
write_len(out, entries.len())?;
let mut seen: HashSet<&str> = HashSet::with_capacity(eager_capacity(entries.len()));
for (key, value) in entries {
if !seen.insert(key.as_str()) {
return Err(CodecError);
}
write_bytes(out, key.as_bytes())?;
encode_ciphertext_inner(value, out, depth + 1)?;
}
}
CipherText::Passthrough(payload) => {
out.push(CT_PASSTHROUGH);
encode_passthrough(payload, out, depth + 1)?;
}
}
Ok(())
}
pub fn decode_ciphertext<Leaf>(bytes: &[u8]) -> Result<Node<Leaf>, CodecError>
where
Leaf: From<Vec<u8>>,
{
let mut reader = Reader::new(bytes);
let ct = decode_ciphertext_inner(&mut reader, 0)?;
if !reader.finished() {
return Err(CodecError);
}
Ok(ct)
}
fn decode_ciphertext_inner<Leaf>(
reader: &mut Reader<'_>,
depth: usize,
) -> Result<Node<Leaf>, CodecError>
where
Leaf: From<Vec<u8>>,
{
if depth > MAX_DEPTH {
return Err(CodecError);
}
match reader.byte()? {
CT_SEQ => {
let count = reader.count()?;
let mut items = Vec::with_capacity(eager_capacity(count));
for _ in 0..count {
items.push(decode_ciphertext_inner(reader, depth + 1)?);
}
Ok(CipherText::Sequence(items))
}
CT_MAP => {
let count = reader.count()?;
let mut entries = Vec::with_capacity(eager_capacity(count));
let mut seen: HashSet<String> = HashSet::with_capacity(eager_capacity(count));
for _ in 0..count {
let key = decode_key(reader)?;
if !seen.insert(key.clone()) {
return Err(CodecError);
}
entries.push((key, decode_ciphertext_inner(reader, depth + 1)?));
}
Ok(CipherText::Map(entries))
}
CT_PASSTHROUGH => {
Ok(CipherText::Passthrough(decode_passthrough(
reader,
depth + 1,
)?))
}
tag => decode_ciphertext_leaf(tag, reader),
}
}
fn decode_ciphertext_leaf<Leaf>(tag: u8, reader: &mut Reader<'_>) -> Result<Node<Leaf>, CodecError>
where
Leaf: From<Vec<u8>>,
{
let wrap: fn(Leaf) -> Node<Leaf> = match tag {
CT_SINGLE => CipherText::Single,
CT_EMPTY_SEQ => CipherText::EmptySequence,
CT_EMPTY_MAP => CipherText::EmptyMap,
_ => return Err(CodecError),
};
let len = reader.count()?;
Ok(wrap(Leaf::from(reader.take(len)?.to_vec())))
}
#[cfg(test)]
mod tests {
use super::*;
use quickcheck_macros::quickcheck;
use vitaminc_aead::tags;
type Ct = Node<Vec<u8>>;
fn leaf(byte: u8, len: usize) -> Vec<u8> {
vec![byte; len]
}
fn frame(n: usize) -> PassthroughFrame {
let mut bytes = Vec::new();
for _ in 0..n {
bytes.push(ARRAY);
bytes.extend_from_slice(&1u32.to_le_bytes());
}
bytes.push(tags::NULL);
PassthroughFrame(bytes)
}
fn encode(ct: &Ct) -> Vec<u8> {
let mut out = Vec::new();
encode_ciphertext(ct, &mut out).expect("codec");
out
}
fn decode(bytes: &[u8]) -> Result<Ct, CodecError> {
decode_ciphertext(bytes)
}
fn round_trip(ct: Ct) -> Ct {
let out = encode(&ct);
let decoded = decode(&out).expect("codec");
assert_eq!(encode(&decoded), out, "re-encode must be byte-stable");
decode(&out).expect("codec")
}
#[test]
fn a_tree_round_trips() {
let ct: Ct = CipherText::Map(vec![
("a".into(), CipherText::Single(leaf(9, 40))),
(
"b".into(),
CipherText::Sequence(vec![
CipherText::Single(leaf(1, 3)),
CipherText::Passthrough(frame(1)),
CipherText::EmptyMap(leaf(2, 3)),
]),
),
]);
let back = round_trip(ct);
let CipherText::Map(entries) = back else {
panic!("a map decodes as a map");
};
assert_eq!(entries.len(), 2);
assert!(matches!(&entries[1].1, CipherText::Sequence(items) if items.len() == 3));
}
#[test]
fn every_leaf_variant_round_trips_as_itself() {
let bytes = leaf(7, 40);
match round_trip(CipherText::Single(bytes.clone())) {
CipherText::Single(l) => assert_eq!(l, bytes),
other => panic!("expected Single, got {other:?}"),
}
match round_trip(CipherText::EmptySequence(bytes.clone())) {
CipherText::EmptySequence(l) => assert_eq!(l, bytes),
other => panic!("expected EmptySequence, got {other:?}"),
}
match round_trip(CipherText::EmptyMap(bytes.clone())) {
CipherText::EmptyMap(l) => assert_eq!(l, bytes),
other => panic!("expected EmptyMap, got {other:?}"),
}
}
#[test]
fn a_refused_tree_leaves_the_output_untouched() {
let mut out = vec![0xEE];
let nested: Ct = CipherText::Sequence(vec![
CipherText::Single(leaf(1, 2)),
CipherText::Map(vec![
("k".into(), CipherText::Single(leaf(1, 1))),
("k".into(), CipherText::Single(leaf(2, 1))),
]),
]);
assert_eq!(encode_ciphertext(&nested, &mut out), Err(CodecError));
assert_eq!(out, [0xEE], "a refused tree leaves the output untouched");
}
#[test]
fn the_retired_none_marker_is_refused_when_read() {
let mut bytes = vec![CT_NONE_RETIRED];
bytes.extend_from_slice(&4u32.to_le_bytes());
bytes.extend_from_slice(&[1; 4]);
assert!(decode(&bytes).is_err());
let mut seq = vec![CT_SEQ, 1, 0, 0, 0];
seq.extend_from_slice(&bytes);
assert!(decode(&seq).is_err());
let ok = encode(&CipherText::Sequence(vec![CipherText::Single(leaf(1, 4))]));
assert!(decode(&ok).is_ok());
}
#[test]
fn the_kinds_are_pinned() {
assert_eq!(
encode(&CipherText::Single(leaf(0xAB, 1))),
[CT_SINGLE, 1, 0, 0, 0, 0xAB]
);
assert_eq!(
encode(&CipherText::EmptySequence(leaf(0xAB, 1))),
[CT_EMPTY_SEQ, 1, 0, 0, 0, 0xAB]
);
assert_eq!(
encode(&CipherText::EmptyMap(leaf(0xAB, 1))),
[CT_EMPTY_MAP, 1, 0, 0, 0, 0xAB]
);
assert_eq!(
encode(&CipherText::Sequence(vec![CipherText::Single(leaf(
0xAB, 1
))])),
[CT_SEQ, 1, 0, 0, 0, CT_SINGLE, 1, 0, 0, 0, 0xAB]
);
assert_eq!(
encode(&CipherText::Map(vec![(
"k".into(),
CipherText::Single(leaf(0xAB, 1))
)])),
[CT_MAP, 1, 0, 0, 0, 1, 0, 0, 0, b'k', CT_SINGLE, 1, 0, 0, 0, 0xAB]
);
assert_eq!(
encode(&CipherText::Passthrough(frame(0))),
[CT_PASSTHROUGH, tags::NULL]
);
assert_eq!(
[
CT_SINGLE,
CT_SEQ,
CT_MAP,
CT_PASSTHROUGH,
CT_EMPTY_SEQ,
CT_EMPTY_MAP
],
[0x01, 0x03, 0x04, 0x05, 0x06, 0x07]
);
}
#[test]
fn duplicate_map_keys_are_refused() {
fn ct_map(keys: [&str; 2]) -> Vec<u8> {
let mut bytes = vec![CT_MAP, 2, 0, 0, 0];
for key in keys {
bytes.extend_from_slice(&(key.len() as u32).to_le_bytes());
bytes.extend_from_slice(key.as_bytes());
bytes.push(CT_SINGLE);
bytes.extend_from_slice(&1u32.to_le_bytes());
bytes.push(0xAB);
}
bytes
}
assert!(decode(&ct_map(["k", "k"])).is_err());
assert!(decode(&ct_map(["k", "l"])).is_ok());
}
#[test]
fn map_keys_are_read_per_entry() {
let ct: Ct = CipherText::Map(vec![
("a".into(), CipherText::Single(leaf(1, 4))),
("bcd".into(), CipherText::Single(leaf(2, 4))),
]);
match round_trip(ct) {
CipherText::Map(entries) => {
let keys: Vec<_> = entries.iter().map(|(k, _)| k.as_str()).collect();
assert_eq!(keys, vec!["a", "bcd"]);
}
other => panic!("expected Map, got {other:?}"),
}
let bytes = [
CT_MAP, 1, 0, 0, 0, 1, 0, 0, 0, 0xFF, CT_SINGLE, 1, 0, 0, 0, 0xAB,
];
assert!(decode(&bytes).is_err());
}
#[test]
fn counts_are_bounded_by_the_bytes_remaining() {
let bytes = [CT_SEQ, 3, 0, 0, 0, CT_SINGLE, 0, 0, 0, 0];
assert!(decode(&bytes).is_err());
let ok = [CT_SEQ, 1, 0, 0, 0, CT_SINGLE, 0, 0, 0, 0];
assert!(decode(&ok).is_ok());
assert!(decode(&[CT_SINGLE, 2, 0, 0, 0, 0xAB]).is_err());
assert!(decode(&[CT_SINGLE, 1, 0, 0, 0, 0xAB, 0xCD]).is_err());
assert!(decode(&[]).is_err());
assert_eq!(eager_capacity(MAX_EAGER_CAPACITY + 1), MAX_EAGER_CAPACITY);
assert_eq!(eager_capacity(3), 3);
}
fn nested_seq(n: usize) -> Vec<u8> {
let mut bytes = Vec::new();
for _ in 0..n {
bytes.push(CT_SEQ);
bytes.extend_from_slice(&1u32.to_le_bytes());
}
bytes.extend_from_slice(&[CT_SINGLE, 1, 0, 0, 0, 0xAB]);
bytes
}
fn nested_map(n: usize) -> Vec<u8> {
let mut bytes = Vec::new();
for _ in 0..n {
bytes.push(CT_MAP);
bytes.extend_from_slice(&1u32.to_le_bytes());
bytes.extend_from_slice(&1u32.to_le_bytes());
bytes.push(b'k');
}
bytes.extend_from_slice(&[CT_SINGLE, 1, 0, 0, 0, 0xAB]);
bytes
}
#[test]
fn depth_is_bounded_for_every_container() {
assert!(decode(&nested_seq(MAX_DEPTH)).is_ok());
assert!(decode(&nested_seq(MAX_DEPTH + 1)).is_err());
assert!(decode(&nested_map(MAX_DEPTH)).is_ok());
assert!(decode(&nested_map(MAX_DEPTH + 1)).is_err());
}
fn nested_tree(n: usize, map: bool) -> Ct {
let mut ct: Ct = CipherText::Single(leaf(0xAB, 1));
for _ in 0..n {
ct = if map {
CipherText::Map(vec![("k".into(), ct)])
} else {
CipherText::Sequence(vec![ct])
};
}
ct
}
#[test]
fn the_encoder_refuses_what_the_decoder_would() {
for map in [false, true] {
let mut out = Vec::new();
assert!(encode_ciphertext(&nested_tree(MAX_DEPTH, map), &mut out).is_ok());
assert!(decode(&out).is_ok());
let mut out = Vec::new();
assert_eq!(
encode_ciphertext(&nested_tree(MAX_DEPTH + 1, map), &mut out),
Err(CodecError)
);
assert!(out.is_empty());
}
let mut out = Vec::new();
let ok: Ct = CipherText::Passthrough(frame(MAX_DEPTH - 1));
assert!(encode_ciphertext(&ok, &mut out).is_ok());
assert!(decode(&out).is_ok());
let mut out = Vec::new();
let too_deep: Ct = CipherText::Passthrough(frame(MAX_DEPTH));
assert_eq!(encode_ciphertext(&too_deep, &mut out), Err(CodecError));
assert!(out.is_empty());
let dup: Ct = CipherText::Map(vec![
("k".into(), CipherText::Single(leaf(1, 1))),
("k".into(), CipherText::Single(leaf(2, 1))),
]);
let mut out = Vec::new();
assert_eq!(encode_ciphertext(&dup, &mut out), Err(CodecError));
assert!(out.is_empty());
}
#[test]
fn the_encoder_counts_the_containers_above_a_passthrough() {
let below_60 = |payload: PassthroughFrame| -> Ct {
let mut ct: Ct = CipherText::Passthrough(payload);
for _ in 0..60 {
ct = CipherText::Sequence(vec![ct]);
}
ct
};
let mut out = Vec::new();
assert!(encode_ciphertext(&below_60(frame(MAX_DEPTH - 61)), &mut out).is_ok());
assert!(decode(&out).is_ok());
let mut out = Vec::new();
assert_eq!(
encode_ciphertext(&below_60(frame(MAX_DEPTH - 60)), &mut out),
Err(CodecError)
);
assert!(out.is_empty());
}
#[test]
fn a_hand_built_frame_payload_is_validated_before_it_is_written() {
let two = vec![tags::NULL, tags::NULL];
let mut truncated = frame(1).0;
truncated.pop();
for payload in [Vec::new(), two, truncated] {
let ct: Ct = CipherText::Sequence(vec![
CipherText::Passthrough(PassthroughFrame(payload)),
CipherText::Single(leaf(9, 40)),
]);
let mut out = Vec::new();
assert_eq!(
encode_ciphertext(&ct, &mut out),
Err(CodecError),
"a payload that is not one frame must not reach the wire"
);
assert!(out.is_empty());
}
}
#[test]
fn a_frame_payload_shares_the_depth_budget() {
let mut ok = vec![CT_PASSTHROUGH];
ok.extend_from_slice(&frame(MAX_DEPTH - 1).0);
assert!(decode(&ok).is_ok());
let mut too_deep = vec![CT_PASSTHROUGH];
too_deep.extend_from_slice(&frame(MAX_DEPTH).0);
assert!(decode(&too_deep).is_err());
let mut bytes = nested_seq(0);
bytes.clear();
for _ in 0..60 {
bytes.push(CT_SEQ);
bytes.extend_from_slice(&1u32.to_le_bytes());
}
let mut ok = bytes.clone();
ok.push(CT_PASSTHROUGH);
ok.extend_from_slice(&frame(MAX_DEPTH - 61).0);
assert!(decode(&ok).is_ok());
bytes.push(CT_PASSTHROUGH);
bytes.extend_from_slice(&frame(MAX_DEPTH - 60).0);
assert!(decode(&bytes).is_err());
}
#[test]
fn a_frame_payload_round_trips_as_its_own_bytes() {
let payload = frame(2);
let ct: Ct = CipherText::Map(vec![
("id".into(), CipherText::Passthrough(payload.clone())),
("email".into(), CipherText::Single(leaf(9, 40))),
]);
let out = encode(&ct);
let decoded = decode(&out).expect("codec");
let CipherText::Map(entries) = &decoded else {
panic!("a map decodes as a map");
};
assert!(matches!(&entries[0].1, CipherText::Passthrough(frame) if frame.0 == payload.0));
assert_eq!(encode(&decoded), out);
}
#[test]
fn a_malformed_frame_payload_is_refused() {
assert!(decode(&[CT_PASSTHROUGH, ARRAY, 2, 0, 0, 0, tags::NULL]).is_err());
assert!(decode(&[CT_PASSTHROUGH, tags::NULL, tags::NULL]).is_err());
assert!(decode(&[CT_PASSTHROUGH]).is_err());
}
#[test]
fn an_unknown_kind_is_refused() {
for kind in [0x00u8, 0x08, 0x7F, 0xF0, 0xFF] {
assert!(decode(&[kind, 1, 0, 0, 0, 0xAB]).is_err(), "{kind:#04x}");
}
}
#[test]
fn a_length_that_does_not_fit_the_prefix_is_refused() {
assert!(write_len(&mut Vec::new(), usize::MAX).is_err());
let mut out = Vec::new();
assert!(write_len(&mut out, u32::MAX as usize).is_ok());
assert_eq!(out, [0xFF; 4]);
}
#[test]
fn the_error_message_is_fixed() {
assert_eq!(CodecError.to_string(), "malformed ciphertext transport");
let boxed: Box<dyn std::error::Error> = Box::new(CodecError);
assert!(boxed.source().is_none());
}
fn refused_or_canonical(bytes: &[u8]) -> bool {
match decode(bytes) {
Ok(ct) => encode(&ct) == bytes,
Err(_) => true,
}
}
#[quickcheck]
fn accepted_bytes_re_encode_exactly(bytes: Vec<u8>) -> bool {
refused_or_canonical(&bytes)
}
#[quickcheck]
fn a_corrupted_encoding_is_refused_or_canonical(at: usize, byte: u8) -> bool {
let mut bytes = encode(&CipherText::Map(vec![
("id".into(), CipherText::Passthrough(frame(2))),
(
"b".into(),
CipherText::Sequence(vec![
CipherText::Single(leaf(9, 4)),
CipherText::EmptySequence(leaf(1, 2)),
CipherText::EmptyMap(leaf(2, 3)),
]),
),
]));
let at = at % bytes.len();
bytes[at] = byte;
refused_or_canonical(&bytes)
}
}