use minicbor::{Decoder, Encoder};
pub const SYNC_ALPN: &[u8] = b"rag-rat/sync/2";
const FRAME_DOMAIN: &str = "rag-rat/sync-frame/1";
pub const MAX_ENTRIES_PER_PAGE: usize = 256;
pub const MAX_HELLO_HASHES: usize = 65_536;
pub const MAX_AUTH_BINDING_BYTES: usize = 512;
type Hash = [u8; 32];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Frame {
Auth { account_id: Hash, binding: Vec<u8> },
Hello { account_id: Hash, have: Vec<Hash> },
Entries { entries: Vec<Vec<u8>>, more: bool },
Done,
}
mod tag {
pub const HELLO: u8 = 0;
pub const ENTRIES: u8 = 1;
pub const DONE: u8 = 2;
pub const AUTH: u8 = 3;
}
#[derive(Debug)]
pub enum WireError {
Malformed(String),
OverCap(String),
}
impl std::fmt::Display for WireError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WireError::Malformed(m) => write!(f, "malformed sync frame: {m}"),
WireError::OverCap(m) => write!(f, "sync frame over cap: {m}"),
}
}
}
impl std::error::Error for WireError {}
impl Frame {
pub fn encode(&self) -> Vec<u8> {
let mut buf = Vec::new();
let mut enc = Encoder::new(&mut buf);
match self {
Frame::Auth { account_id, binding } => {
enc.array(4).expect(INFALLIBLE);
enc.str(FRAME_DOMAIN).expect(INFALLIBLE);
enc.u8(tag::AUTH).expect(INFALLIBLE);
enc.bytes(account_id).expect(INFALLIBLE);
enc.bytes(binding).expect(INFALLIBLE);
},
Frame::Hello { account_id, have } => {
enc.array(3).expect(INFALLIBLE);
enc.str(FRAME_DOMAIN).expect(INFALLIBLE);
enc.u8(tag::HELLO).expect(INFALLIBLE);
enc.array(2).expect(INFALLIBLE);
enc.bytes(account_id).expect(INFALLIBLE);
enc.array(have.len() as u64).expect(INFALLIBLE);
for h in have {
enc.bytes(h).expect(INFALLIBLE);
}
},
Frame::Entries { entries, more } => {
enc.array(4).expect(INFALLIBLE);
enc.str(FRAME_DOMAIN).expect(INFALLIBLE);
enc.u8(tag::ENTRIES).expect(INFALLIBLE);
enc.array(entries.len() as u64).expect(INFALLIBLE);
for e in entries {
enc.bytes(e).expect(INFALLIBLE);
}
enc.bool(*more).expect(INFALLIBLE);
},
Frame::Done => {
enc.array(2).expect(INFALLIBLE);
enc.str(FRAME_DOMAIN).expect(INFALLIBLE);
enc.u8(tag::DONE).expect(INFALLIBLE);
},
}
buf
}
pub fn decode(bytes: &[u8]) -> Result<Frame, WireError> {
let mut dec = Decoder::new(bytes);
let outer = expect_len(dec.array().map_err(m)?, "frame")?;
let domain = dec.str().map_err(m)?;
if domain != FRAME_DOMAIN {
return Err(WireError::Malformed(format!("unknown frame domain {domain:?}")));
}
let tag = dec.u8().map_err(m)?;
let expected_outer = match tag {
tag::HELLO => 3,
tag::ENTRIES => 4,
tag::DONE => 2,
tag::AUTH => 4,
other => return Err(WireError::Malformed(format!("unknown frame tag {other}"))),
};
if outer != expected_outer {
return Err(WireError::Malformed(format!(
"frame tag {tag} arity {outer}, expected {expected_outer}"
)));
}
let frame = Self::decode_body(tag, &mut dec)?;
if dec.position() != bytes.len() {
return Err(WireError::Malformed("trailing bytes after frame".into()));
}
Ok(frame)
}
fn decode_body(tag: u8, dec: &mut Decoder<'_>) -> Result<Frame, WireError> {
match tag {
tag::AUTH => {
let account_id = fixed_hash(dec.bytes().map_err(m)?)?;
let binding = dec.bytes().map_err(m)?;
if binding.len() > MAX_AUTH_BINDING_BYTES {
return Err(WireError::OverCap(format!(
"auth binding {} > {MAX_AUTH_BINDING_BYTES}",
binding.len()
)));
}
Ok(Frame::Auth { account_id, binding: binding.to_vec() })
},
tag::HELLO => {
let inner = dec.array().map_err(m)?;
if inner != Some(2) {
return Err(WireError::Malformed("hello payload arity".into()));
}
let account_id = fixed_hash(dec.bytes().map_err(m)?)?;
let n = expect_len(dec.array().map_err(m)?, "hello.have")?;
if n > MAX_HELLO_HASHES as u64 {
return Err(WireError::OverCap(format!("hello.have {n} > {MAX_HELLO_HASHES}")));
}
let mut have = Vec::with_capacity(n as usize);
for _ in 0..n {
have.push(fixed_hash(dec.bytes().map_err(m)?)?);
}
Ok(Frame::Hello { account_id, have })
},
tag::ENTRIES => {
let n = expect_len(dec.array().map_err(m)?, "entries")?;
if n > MAX_ENTRIES_PER_PAGE as u64 {
return Err(WireError::OverCap(format!(
"entries page {n} > {MAX_ENTRIES_PER_PAGE}"
)));
}
let mut entries = Vec::with_capacity(n as usize);
for _ in 0..n {
entries.push(dec.bytes().map_err(m)?.to_vec());
}
let more = dec.bool().map_err(m)?;
Ok(Frame::Entries { entries, more })
},
tag::DONE => Ok(Frame::Done),
other => Err(WireError::Malformed(format!("unknown frame tag {other}"))),
}
}
}
const INFALLIBLE: &str = "encoding into an owned Vec cannot fail";
fn m(e: minicbor::decode::Error) -> WireError {
WireError::Malformed(e.to_string())
}
fn expect_len(len: Option<u64>, field: &str) -> Result<u64, WireError> {
len.ok_or_else(|| WireError::Malformed(format!("indefinite-length {field} array")))
}
fn fixed_hash(bytes: &[u8]) -> Result<Hash, WireError> {
Hash::try_from(bytes)
.map_err(|_| WireError::Malformed(format!("expected 32-byte hash, got {}", bytes.len())))
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(frame: &Frame) {
let bytes = frame.encode();
assert_eq!(&Frame::decode(&bytes).unwrap(), frame, "encode∘decode is identity");
}
#[test]
fn every_frame_roundtrips() {
roundtrip(&Frame::Auth { account_id: [0xbb; 32], binding: vec![1, 2, 3, 4] });
roundtrip(&Frame::Auth { account_id: [0; 32], binding: vec![] });
roundtrip(&Frame::Hello { account_id: [0xaa; 32], have: vec![[1; 32], [2; 32]] });
roundtrip(&Frame::Hello { account_id: [0; 32], have: vec![] });
roundtrip(&Frame::Entries { entries: vec![vec![1, 2, 3], vec![]], more: true });
roundtrip(&Frame::Entries { entries: vec![], more: false });
roundtrip(&Frame::Done);
}
#[test]
fn an_over_cap_auth_binding_is_rejected() {
let frame =
Frame::Auth { account_id: [3; 32], binding: vec![0u8; MAX_AUTH_BINDING_BYTES + 1] };
assert!(matches!(Frame::decode(&frame.encode()), Err(WireError::OverCap(_))));
}
#[test]
fn a_foreign_domain_is_rejected() {
let mut buf = Vec::new();
let mut enc = Encoder::new(&mut buf);
enc.array(2).unwrap();
enc.str("some/other-protocol/1").unwrap();
enc.u8(0).unwrap();
assert!(matches!(Frame::decode(&buf), Err(WireError::Malformed(_))));
}
#[test]
fn an_over_cap_entries_page_is_rejected_as_hostile() {
let mut buf = Vec::new();
let mut enc = Encoder::new(&mut buf);
enc.array(4).unwrap();
enc.str(FRAME_DOMAIN).unwrap();
enc.u8(tag::ENTRIES).unwrap();
enc.array((MAX_ENTRIES_PER_PAGE + 1) as u64).unwrap();
assert!(matches!(Frame::decode(&buf), Err(WireError::OverCap(_))));
}
#[test]
fn a_truncated_frame_is_malformed_not_a_panic() {
let full = Frame::Hello { account_id: [7; 32], have: vec![[9; 32]] }.encode();
for cut in 0..full.len() {
let _ = Frame::decode(&full[..cut]);
}
}
#[test]
fn a_wrong_outer_arity_is_rejected() {
let mut buf = Vec::new();
let mut enc = Encoder::new(&mut buf);
enc.array(3).unwrap();
enc.str(FRAME_DOMAIN).unwrap();
enc.u8(tag::DONE).unwrap();
enc.u8(0).unwrap(); assert!(matches!(Frame::decode(&buf), Err(WireError::Malformed(_))));
}
#[test]
fn trailing_bytes_after_a_valid_frame_are_rejected() {
let mut buf = Frame::Done.encode();
buf.push(0xff); assert!(matches!(Frame::decode(&buf), Err(WireError::Malformed(_))));
}
#[test]
fn done_frame_bytes_are_frozen() {
let bytes = Frame::Done.encode();
assert_eq!(
bytes,
[
0x82, 0x74, b'r', b'a', b'g', b'-', b'r', b'a', b't', b'/', b's', b'y', b'n', b'c',
b'-', b'f', b'r', b'a', b'm', b'e', b'/', b'1', 0x02,
],
);
}
}