1use minicbor::{Decoder, Encoder};
15
16pub const SYNC_ALPN: &[u8] = b"rag-rat/sync/2";
21
22const FRAME_DOMAIN: &str = "rag-rat/sync-frame/1";
25
26pub const MAX_ENTRIES_PER_PAGE: usize = 256;
29
30pub const MAX_HELLO_HASHES: usize = 65_536;
36
37pub const MAX_AUTH_BINDING_BYTES: usize = 512;
44
45type Hash = [u8; 32];
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum Frame {
50 Auth { account_id: Hash, binding: Vec<u8> },
55 Hello { account_id: Hash, have: Vec<Hash> },
58 Entries { entries: Vec<Vec<u8>>, more: bool },
60 Done,
63}
64
65mod tag {
66 pub const HELLO: u8 = 0;
67 pub const ENTRIES: u8 = 1;
68 pub const DONE: u8 = 2;
69 pub const AUTH: u8 = 3;
70}
71
72#[derive(Debug)]
75pub enum WireError {
76 Malformed(String),
78 OverCap(String),
80}
81
82impl std::fmt::Display for WireError {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 match self {
85 WireError::Malformed(m) => write!(f, "malformed sync frame: {m}"),
86 WireError::OverCap(m) => write!(f, "sync frame over cap: {m}"),
87 }
88 }
89}
90
91impl std::error::Error for WireError {}
92
93impl Frame {
94 pub fn encode(&self) -> Vec<u8> {
98 let mut buf = Vec::new();
99 let mut enc = Encoder::new(&mut buf);
100 match self {
102 Frame::Auth { account_id, binding } => {
103 enc.array(4).expect(INFALLIBLE);
104 enc.str(FRAME_DOMAIN).expect(INFALLIBLE);
105 enc.u8(tag::AUTH).expect(INFALLIBLE);
106 enc.bytes(account_id).expect(INFALLIBLE);
107 enc.bytes(binding).expect(INFALLIBLE);
108 },
109 Frame::Hello { account_id, have } => {
110 enc.array(3).expect(INFALLIBLE);
111 enc.str(FRAME_DOMAIN).expect(INFALLIBLE);
112 enc.u8(tag::HELLO).expect(INFALLIBLE);
113 enc.array(2).expect(INFALLIBLE);
114 enc.bytes(account_id).expect(INFALLIBLE);
115 enc.array(have.len() as u64).expect(INFALLIBLE);
116 for h in have {
117 enc.bytes(h).expect(INFALLIBLE);
118 }
119 },
120 Frame::Entries { entries, more } => {
121 enc.array(4).expect(INFALLIBLE);
122 enc.str(FRAME_DOMAIN).expect(INFALLIBLE);
123 enc.u8(tag::ENTRIES).expect(INFALLIBLE);
124 enc.array(entries.len() as u64).expect(INFALLIBLE);
125 for e in entries {
126 enc.bytes(e).expect(INFALLIBLE);
127 }
128 enc.bool(*more).expect(INFALLIBLE);
129 },
130 Frame::Done => {
131 enc.array(2).expect(INFALLIBLE);
132 enc.str(FRAME_DOMAIN).expect(INFALLIBLE);
133 enc.u8(tag::DONE).expect(INFALLIBLE);
134 },
135 }
136 buf
137 }
138
139 pub fn decode(bytes: &[u8]) -> Result<Frame, WireError> {
142 let mut dec = Decoder::new(bytes);
143 let outer = expect_len(dec.array().map_err(m)?, "frame")?;
144 let domain = dec.str().map_err(m)?;
145 if domain != FRAME_DOMAIN {
146 return Err(WireError::Malformed(format!("unknown frame domain {domain:?}")));
147 }
148 let tag = dec.u8().map_err(m)?;
149 let expected_outer = match tag {
152 tag::HELLO => 3,
153 tag::ENTRIES => 4,
154 tag::DONE => 2,
155 tag::AUTH => 4,
156 other => return Err(WireError::Malformed(format!("unknown frame tag {other}"))),
157 };
158 if outer != expected_outer {
159 return Err(WireError::Malformed(format!(
160 "frame tag {tag} arity {outer}, expected {expected_outer}"
161 )));
162 }
163 let frame = Self::decode_body(tag, &mut dec)?;
164 if dec.position() != bytes.len() {
167 return Err(WireError::Malformed("trailing bytes after frame".into()));
168 }
169 Ok(frame)
170 }
171
172 fn decode_body(tag: u8, dec: &mut Decoder<'_>) -> Result<Frame, WireError> {
173 match tag {
174 tag::AUTH => {
175 let account_id = fixed_hash(dec.bytes().map_err(m)?)?;
176 let binding = dec.bytes().map_err(m)?;
177 if binding.len() > MAX_AUTH_BINDING_BYTES {
178 return Err(WireError::OverCap(format!(
179 "auth binding {} > {MAX_AUTH_BINDING_BYTES}",
180 binding.len()
181 )));
182 }
183 Ok(Frame::Auth { account_id, binding: binding.to_vec() })
184 },
185 tag::HELLO => {
186 let inner = dec.array().map_err(m)?;
187 if inner != Some(2) {
188 return Err(WireError::Malformed("hello payload arity".into()));
189 }
190 let account_id = fixed_hash(dec.bytes().map_err(m)?)?;
191 let n = expect_len(dec.array().map_err(m)?, "hello.have")?;
192 if n > MAX_HELLO_HASHES as u64 {
193 return Err(WireError::OverCap(format!("hello.have {n} > {MAX_HELLO_HASHES}")));
194 }
195 let mut have = Vec::with_capacity(n as usize);
196 for _ in 0..n {
197 have.push(fixed_hash(dec.bytes().map_err(m)?)?);
198 }
199 Ok(Frame::Hello { account_id, have })
200 },
201 tag::ENTRIES => {
202 let n = expect_len(dec.array().map_err(m)?, "entries")?;
203 if n > MAX_ENTRIES_PER_PAGE as u64 {
204 return Err(WireError::OverCap(format!(
205 "entries page {n} > {MAX_ENTRIES_PER_PAGE}"
206 )));
207 }
208 let mut entries = Vec::with_capacity(n as usize);
209 for _ in 0..n {
210 entries.push(dec.bytes().map_err(m)?.to_vec());
211 }
212 let more = dec.bool().map_err(m)?;
213 Ok(Frame::Entries { entries, more })
214 },
215 tag::DONE => Ok(Frame::Done),
216 other => Err(WireError::Malformed(format!("unknown frame tag {other}"))),
217 }
218 }
219}
220
221const INFALLIBLE: &str = "encoding into an owned Vec cannot fail";
222
223fn m(e: minicbor::decode::Error) -> WireError {
224 WireError::Malformed(e.to_string())
225}
226
227fn expect_len(len: Option<u64>, field: &str) -> Result<u64, WireError> {
228 len.ok_or_else(|| WireError::Malformed(format!("indefinite-length {field} array")))
229}
230
231fn fixed_hash(bytes: &[u8]) -> Result<Hash, WireError> {
232 Hash::try_from(bytes)
233 .map_err(|_| WireError::Malformed(format!("expected 32-byte hash, got {}", bytes.len())))
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 fn roundtrip(frame: &Frame) {
241 let bytes = frame.encode();
242 assert_eq!(&Frame::decode(&bytes).unwrap(), frame, "encode∘decode is identity");
243 }
244
245 #[test]
246 fn every_frame_roundtrips() {
247 roundtrip(&Frame::Auth { account_id: [0xbb; 32], binding: vec![1, 2, 3, 4] });
248 roundtrip(&Frame::Auth { account_id: [0; 32], binding: vec![] });
249 roundtrip(&Frame::Hello { account_id: [0xaa; 32], have: vec![[1; 32], [2; 32]] });
250 roundtrip(&Frame::Hello { account_id: [0; 32], have: vec![] });
251 roundtrip(&Frame::Entries { entries: vec![vec![1, 2, 3], vec![]], more: true });
252 roundtrip(&Frame::Entries { entries: vec![], more: false });
253 roundtrip(&Frame::Done);
254 }
255
256 #[test]
257 fn an_over_cap_auth_binding_is_rejected() {
258 let frame =
261 Frame::Auth { account_id: [3; 32], binding: vec![0u8; MAX_AUTH_BINDING_BYTES + 1] };
262 assert!(matches!(Frame::decode(&frame.encode()), Err(WireError::OverCap(_))));
263 }
264
265 #[test]
266 fn a_foreign_domain_is_rejected() {
267 let mut buf = Vec::new();
269 let mut enc = Encoder::new(&mut buf);
270 enc.array(2).unwrap();
271 enc.str("some/other-protocol/1").unwrap();
272 enc.u8(0).unwrap();
273 assert!(matches!(Frame::decode(&buf), Err(WireError::Malformed(_))));
274 }
275
276 #[test]
277 fn an_over_cap_entries_page_is_rejected_as_hostile() {
278 let mut buf = Vec::new();
281 let mut enc = Encoder::new(&mut buf);
282 enc.array(4).unwrap();
283 enc.str(FRAME_DOMAIN).unwrap();
284 enc.u8(tag::ENTRIES).unwrap();
285 enc.array((MAX_ENTRIES_PER_PAGE + 1) as u64).unwrap();
286 assert!(matches!(Frame::decode(&buf), Err(WireError::OverCap(_))));
288 }
289
290 #[test]
291 fn a_truncated_frame_is_malformed_not_a_panic() {
292 let full = Frame::Hello { account_id: [7; 32], have: vec![[9; 32]] }.encode();
293 for cut in 0..full.len() {
294 let _ = Frame::decode(&full[..cut]);
296 }
297 }
298
299 #[test]
300 fn a_wrong_outer_arity_is_rejected() {
301 let mut buf = Vec::new();
303 let mut enc = Encoder::new(&mut buf);
304 enc.array(3).unwrap();
305 enc.str(FRAME_DOMAIN).unwrap();
306 enc.u8(tag::DONE).unwrap();
307 enc.u8(0).unwrap(); assert!(matches!(Frame::decode(&buf), Err(WireError::Malformed(_))));
309 }
310
311 #[test]
312 fn trailing_bytes_after_a_valid_frame_are_rejected() {
313 let mut buf = Frame::Done.encode();
314 buf.push(0xff); assert!(matches!(Frame::decode(&buf), Err(WireError::Malformed(_))));
316 }
317
318 #[test]
321 fn done_frame_bytes_are_frozen() {
322 let bytes = Frame::Done.encode();
323 assert_eq!(
324 bytes,
325 [
327 0x82, 0x74, b'r', b'a', b'g', b'-', b'r', b'a', b't', b'/', b's', b'y', b'n', b'c',
328 b'-', b'f', b'r', b'a', b'm', b'e', b'/', b'1', 0x02,
329 ],
330 );
331 }
332}