msg.rs
⎇
Raw
1//! Message types and their payload layouts.
2//!
3//! Every payload is fixed-width except `LOC` (which is a count plus that many
4//! point records) and `ACK` (a count plus that many nonces).
5//!
6//! **There is no clock anywhere in this protocol except `Point::ts`.** No message
7//! carries the server's time, nothing measures or reports clock skew, and nothing
8//! corrects a timestamp. The client's `ts` is stored and displayed exactly as
9//! sent. That is the whole of the timestamp design.
10//!
11//! Reserved bytes, where they exist, are written as zero and ignored on decode,
12//! so a later version can populate them without this build rejecting the packet.
13
14use crate::error::DecodeError;
15use crate::point::{POINT_LEN, Point};
16
17/// Number of nonce bytes, which is also the length of a message id.
18pub const NONCE_LEN: usize = 12;
19
20/// A message id: the random AEAD nonce, reused as the identifier that `ACK` and
21/// `NACK` echo. It is unique and already on the wire, so a separate id field
22/// would be pure overhead.
23pub type Nonce = [u8; NONCE_LEN];
24
25/// Most points in one `LOC`, and most nonces in one `ACK`.
26///
27/// 40 points is `21 + 1 + 40*24 + 16` = 998 bytes, comfortably inside the
28/// 1200-byte datagram budget.
29pub const MAX_POINTS: usize = 40;
30
31const HELLO_LEN: usize = 6;
32const CONFIG_LEN: usize = 12;
33const CONFIG_GET_LEN: usize = 2;
34const PING_LEN: usize = 6;
35const PONG_LEN: usize = 6;
36const NACK_LEN: usize = NONCE_LEN + 2;
37const REVOKED_LEN: usize = 1;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
42#[repr(u8)]
43pub enum MsgType {
44 Loc = 0x1,
45 Ack = 0x2,
46 Nack = 0x3,
47 Hello = 0x4,
48 Config = 0x5,
49 ConfigGet = 0x6,
50 Ping = 0x7,
51 Pong = 0x8,
52 /// Sealed under `K_rev`, not `K_down`. See [`Revoked`].
53 Revoked = 0x9,
54}
55
56impl MsgType {
57 pub const ALL: [Self; 9] = [
58 Self::Loc,
59 Self::Ack,
60 Self::Nack,
61 Self::Hello,
62 Self::Config,
63 Self::ConfigGet,
64 Self::Ping,
65 Self::Pong,
66 Self::Revoked,
67 ];
68
69 /// True for messages a device sends to the server, which are sealed under
70 /// `K_up`. The two directions have separate keys, so a captured uplink
71 /// datagram can never be replayed back as a downlink one.
72 #[must_use]
73 pub const fn is_uplink(self) -> bool {
74 matches!(self, Self::Loc | Self::Hello | Self::ConfigGet | Self::Ping)
75 }
76
77 #[must_use]
78 pub const fn direction(self) -> Direction {
79 if self.is_uplink() {
80 Direction::Up
81 } else {
82 Direction::Down
83 }
84 }
85}
86
87impl TryFrom<u8> for MsgType {
88 type Error = DecodeError;
89 fn try_from(v: u8) -> Result<Self, Self::Error> {
90 Ok(match v {
91 0x1 => Self::Loc,
92 0x2 => Self::Ack,
93 0x3 => Self::Nack,
94 0x4 => Self::Hello,
95 0x5 => Self::Config,
96 0x6 => Self::ConfigGet,
97 0x7 => Self::Ping,
98 0x8 => Self::Pong,
99 0x9 => Self::Revoked,
100 other => return Err(DecodeError::BadMsgType(other)),
101 })
102 }
103}
104
105/// Which of the two derived keys seals a message.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub enum Direction {
108 /// Device → server, `K_up`.
109 Up,
110 /// Server → device, `K_down`.
111 Down,
112}
113
114impl Direction {
115 /// HKDF info string. Distinct strings are what make the two keys
116 /// independent.
117 #[must_use]
118 pub const fn info(self) -> &'static [u8] {
119 match self {
120 Self::Up => b"otp/1/up",
121 Self::Down => b"otp/1/down",
122 }
123 }
124}
125
126// ---------------------------------------------------------------------------
127// ACK
128// ---------------------------------------------------------------------------
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
132#[cfg_attr(feature = "serde", serde(transparent))]
133pub struct AckFlags(pub u8);
134
135impl AckFlags {
136 pub const NONE: Self = Self(0);
137 /// The server has a newer config; the device should send `CONFIG_GET`.
138 /// Config delivery is pull-based so it never depends on a live NAT binding.
139 pub const CONFIG_PENDING: Self = Self(1 << 0);
140 /// The write path is saturated; back off before the next flush.
141 pub const THROTTLE: Self = Self(1 << 1);
142
143 #[must_use]
144 pub const fn contains(self, other: Self) -> bool {
145 self.0 & other.0 == other.0
146 }
147}
148
149/// Retires one or more messages. One `ACK` can cover a whole flush burst.
150#[derive(Debug, Clone, PartialEq, Eq)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
152pub struct Ack {
153 pub nonces: Vec<Nonce>,
154 pub flags: AckFlags,
155}
156
157impl Ack {
158 #[must_use]
159 pub fn single(nonce: Nonce) -> Self {
160 Self {
161 nonces: vec![nonce],
162 flags: AckFlags::NONE,
163 }
164 }
165
166 const fn payload_len_for(count: usize) -> usize {
167 1 + count * NONCE_LEN + 1
168 }
169}
170
171// ---------------------------------------------------------------------------
172// NACK
173// ---------------------------------------------------------------------------
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
176#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
177#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
178#[repr(u8)]
179pub enum NackReason {
180 /// Token unknown, revoked, or expired. The device clears local state and
181 /// shows the login screen.
182 UnknownToken = 1,
183 /// Decrypted cleanly but the payload made no sense.
184 Malformed = 2,
185 RateLimited = 3,
186 /// Per-account storage quota exhausted.
187 StorageFull = 4,
188}
189
190impl TryFrom<u8> for NackReason {
191 type Error = DecodeError;
192 fn try_from(v: u8) -> Result<Self, Self::Error> {
193 Ok(match v {
194 1 => Self::UnknownToken,
195 2 => Self::Malformed,
196 3 => Self::RateLimited,
197 4 => Self::StorageFull,
198 value => {
199 return Err(DecodeError::BadEnum {
200 field: "NackReason",
201 value,
202 });
203 }
204 })
205 }
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
210pub struct Nack {
211 pub nonce: Nonce,
212 pub reason: NackReason,
213 /// Seconds to wait before retrying. 0 means "no advice".
214 pub retry_after_s: u8,
215}
216
217// ---------------------------------------------------------------------------
218// REVOKED
219// ---------------------------------------------------------------------------
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
222#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
223#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
224#[repr(u8)]
225pub enum RevokeReason {
226 /// Explicitly revoked: "log out all other devices", or a password change.
227 Revoked = 1,
228 /// Deleted by the staleness sweep after a long silence.
229 Expired = 2,
230 /// The server has no record of this token at all. A restored backup or a
231 /// rotated server key looks like this.
232 Unknown = 3,
233}
234
235impl TryFrom<u8> for RevokeReason {
236 type Error = DecodeError;
237 fn try_from(v: u8) -> Result<Self, Self::Error> {
238 Ok(match v {
239 1 => Self::Revoked,
240 2 => Self::Expired,
241 3 => Self::Unknown,
242 value => {
243 return Err(DecodeError::BadEnum {
244 field: "RevokeReason",
245 value,
246 });
247 }
248 })
249 }
250}
251
252/// "This token is dead; log in again."
253///
254/// The one message sealed under `K_rev` rather than `K_down`, which is the whole
255/// reason it exists as a separate type. `K_down` derives from the token key, so
256/// it dies with the token's row — and the moment the server most needs to speak
257/// is exactly when that row is gone. `K_rev` is derived from a server master key
258/// and the `token_id` (see [`crate::kdf::revocation_key`]), so the server can
259/// recompute it for any id, including one it has never seen.
260///
261/// Two properties follow from deriving per `token_id` rather than sharing one
262/// server key: a third party cannot forge this message, and neither can another
263/// legitimate device — it only ever learns its own `K_rev`.
264///
265/// Replay needs no counter. `token_id` lives in the header, and the header is the
266/// AEAD's associated data, so a captured `REVOKED` names the token it was issued
267/// against. After the user logs in again the device holds a different id and the
268/// rule "act only on my current `token_id`" discards it.
269///
270/// One byte of payload, deliberately: at 38 bytes on the wire this is the
271/// smallest reply in the protocol, and the server refuses to send it in answer to
272/// anything shorter, so it can never amplify.
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
275pub struct Revoked {
276 pub reason: RevokeReason,
277}
278
279// ---------------------------------------------------------------------------
280// HELLO
281// ---------------------------------------------------------------------------
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
284#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
285#[cfg_attr(feature = "serde", serde(transparent))]
286pub struct HelloFlags(pub u8);
287
288impl HelloFlags {
289 pub const NONE: Self = Self(0);
290 /// First run after an install or reinstall.
291 pub const FIRST_LAUNCH: Self = Self(1 << 0);
292}
293
294/// Sent once per service start, so the server has app and OS versions to show
295/// alongside a token. Carries no time: the server has nothing to compare it to
296/// that it would be allowed to act on.
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
299pub struct Hello {
300 pub app_version_code: u16,
301 pub os_api_level: u8,
302 pub flags: HelloFlags,
303 /// The config version the device currently holds.
304 pub config_version: u16,
305}
306
307// ---------------------------------------------------------------------------
308// CONFIG / CONFIG_GET
309// ---------------------------------------------------------------------------
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
312#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
313#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
314#[repr(u8)]
315pub enum Profile {
316 BatterySaver = 0,
317 #[default]
318 Balanced = 1,
319 HighAccuracy = 2,
320}
321
322impl TryFrom<u8> for Profile {
323 type Error = DecodeError;
324 fn try_from(v: u8) -> Result<Self, Self::Error> {
325 Ok(match v {
326 0 => Self::BatterySaver,
327 1 => Self::Balanced,
328 2 => Self::HighAccuracy,
329 value => {
330 return Err(DecodeError::BadEnum {
331 field: "Profile",
332 value,
333 });
334 }
335 })
336 }
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
340#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
341#[cfg_attr(feature = "serde", serde(transparent))]
342pub struct ConfigFlags(pub u8);
343
344impl ConfigFlags {
345 pub const NONE: Self = Self(0);
346 /// Master switch. Clearing it stops sharing without revoking the token.
347 pub const TRACKING_ENABLED: Self = Self(1 << 0);
348 /// Server wants a fresh `HELLO` (e.g. it has no version info on record).
349 pub const REQUEST_HELLO: Self = Self(1 << 1);
350
351 #[must_use]
352 pub const fn contains(self, other: Self) -> bool {
353 self.0 & other.0 == other.0
354 }
355}
356
357/// The server's view of how this device should behave.
358///
359/// `profile` names a parameter set the *client* owns; the numeric fields are
360/// server-side overrides on top of it. That keeps the message small and means a
361/// profile can be retuned by shipping an app update, without a protocol change.
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
364pub struct Config {
365 pub config_version: u16,
366 pub profile: Profile,
367 pub flags: ConfigFlags,
368 /// Stationary heartbeat period. Clamped by the client to what inexact
369 /// alarms can actually deliver in doze (~9 min floor).
370 pub heartbeat_s: u16,
371 /// Scales the profile's intervals, in percent. 100 = profile default.
372 pub interval_scale_pct: u16,
373 pub min_distance_m: u16,
374 pub max_points_per_loc: u8,
375}
376
377impl Default for Config {
378 fn default() -> Self {
379 Self {
380 config_version: 1,
381 profile: Profile::Balanced,
382 flags: ConfigFlags::TRACKING_ENABLED,
383 heartbeat_s: 900,
384 interval_scale_pct: 100,
385 min_distance_m: 20,
386 max_points_per_loc: MAX_POINTS as u8,
387 }
388 }
389}
390
391/// Asks for the current [`Config`]. Padded to `CONFIG`'s size so the reply is
392/// never larger than the request.
393#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
394#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
395pub struct ConfigGet {
396 /// The version the device already has, so the server can skip a no-op push.
397 pub have_version: u16,
398}
399
400// ---------------------------------------------------------------------------
401// PING / PONG
402// ---------------------------------------------------------------------------
403
404/// Probes whether UDP works on the current network.
405#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
406#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
407pub struct Ping {
408 /// Opaque to the server, echoed verbatim in the `PONG`. The client puts
409 /// whatever lets it match up a reply and measure a round trip — a reading of
410 /// its own monotonic clock, typically. Deliberately *not* a wall-clock time
411 /// the server is invited to interpret.
412 pub echo: u32,
413 pub seq: u16,
414}
415
416#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
417#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
418pub struct Pong {
419 /// Copied from the `PING`, so the client needs no per-probe state.
420 pub echo: u32,
421 pub seq: u16,
422}
423
424// ---------------------------------------------------------------------------
425// Message
426// ---------------------------------------------------------------------------
427
428#[derive(Debug, Clone, PartialEq, Eq)]
429#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
430// Adjacently tagged rather than internally tagged: `Loc` carries a sequence,
431// which an internally-tagged representation cannot express.
432#[cfg_attr(
433 feature = "serde",
434 serde(tag = "type", content = "value", rename_all = "snake_case")
435)]
436pub enum Message {
437 /// One or more independent points.
438 Loc(Vec<Point>),
439 Ack(Ack),
440 Nack(Nack),
441 Hello(Hello),
442 Config(Config),
443 ConfigGet(ConfigGet),
444 Ping(Ping),
445 Pong(Pong),
446 Revoked(Revoked),
447}
448
449impl Message {
450 #[must_use]
451 pub const fn msg_type(&self) -> MsgType {
452 match self {
453 Self::Loc(_) => MsgType::Loc,
454 Self::Ack(_) => MsgType::Ack,
455 Self::Nack(_) => MsgType::Nack,
456 Self::Hello(_) => MsgType::Hello,
457 Self::Config(_) => MsgType::Config,
458 Self::ConfigGet(_) => MsgType::ConfigGet,
459 Self::Ping(_) => MsgType::Ping,
460 Self::Pong(_) => MsgType::Pong,
461 Self::Revoked(_) => MsgType::Revoked,
462 }
463 }
464
465 /// Exact payload length, without allocating.
466 #[must_use]
467 pub fn payload_len(&self) -> usize {
468 match self {
469 Self::Loc(points) => 1 + points.len() * POINT_LEN,
470 Self::Ack(ack) => Ack::payload_len_for(ack.nonces.len()),
471 Self::Nack(_) => NACK_LEN,
472 Self::Hello(_) => HELLO_LEN,
473 Self::Config(_) => CONFIG_LEN,
474 Self::ConfigGet(_) => CONFIG_GET_LEN,
475 Self::Ping(_) => PING_LEN,
476 Self::Pong(_) => PONG_LEN,
477 Self::Revoked(_) => REVOKED_LEN,
478 }
479 }
480
481 /// Append this message's payload (the part that gets encrypted).
482 ///
483 /// # Panics
484 /// If a `Loc` or `Ack` holds more than [`MAX_POINTS`] elements, or a `Loc`
485 /// holds none. Those are caller bugs, not wire conditions: nothing outside
486 /// this process can trigger them.
487 pub fn encode_payload_into(&self, out: &mut Vec<u8>) {
488 out.reserve(self.payload_len());
489 match self {
490 Self::Loc(points) => {
491 assert!(
492 (1..=MAX_POINTS).contains(&points.len()),
493 "LOC must carry 1..={MAX_POINTS} points, got {}",
494 points.len()
495 );
496 out.push(points.len() as u8);
497 for p in points {
498 out.extend_from_slice(&p.to_bytes());
499 }
500 }
501 Self::Ack(ack) => {
502 assert!(
503 (1..=MAX_POINTS).contains(&ack.nonces.len()),
504 "ACK must carry 1..={MAX_POINTS} nonces, got {}",
505 ack.nonces.len()
506 );
507 out.push(ack.nonces.len() as u8);
508 for n in &ack.nonces {
509 out.extend_from_slice(n);
510 }
511 out.push(ack.flags.0);
512 }
513 Self::Nack(nack) => {
514 out.extend_from_slice(&nack.nonce);
515 out.push(nack.reason as u8);
516 out.push(nack.retry_after_s);
517 }
518 Self::Hello(h) => {
519 out.extend_from_slice(&h.app_version_code.to_be_bytes());
520 out.push(h.os_api_level);
521 out.push(h.flags.0);
522 out.extend_from_slice(&h.config_version.to_be_bytes());
523 }
524 Self::Config(c) => {
525 out.extend_from_slice(&c.config_version.to_be_bytes());
526 out.push(c.profile as u8);
527 out.push(c.flags.0);
528 out.extend_from_slice(&c.heartbeat_s.to_be_bytes());
529 out.extend_from_slice(&c.interval_scale_pct.to_be_bytes());
530 out.extend_from_slice(&c.min_distance_m.to_be_bytes());
531 out.push(c.max_points_per_loc);
532 out.push(0); // reserved
533 }
534 Self::ConfigGet(g) => {
535 out.extend_from_slice(&g.have_version.to_be_bytes());
536 }
537 Self::Ping(p) => {
538 out.extend_from_slice(&p.echo.to_be_bytes());
539 out.extend_from_slice(&p.seq.to_be_bytes());
540 }
541 Self::Pong(p) => {
542 out.extend_from_slice(&p.echo.to_be_bytes());
543 out.extend_from_slice(&p.seq.to_be_bytes());
544 }
545 Self::Revoked(r) => out.push(r.reason as u8),
546 }
547 debug_assert_eq!(
548 out.len(),
549 self.payload_len(),
550 "payload_len disagrees with the encoder"
551 );
552 }
553
554 #[must_use]
555 pub fn encode_payload(&self) -> Vec<u8> {
556 let mut out = Vec::with_capacity(self.payload_len());
557 self.encode_payload_into(&mut out);
558 out
559 }
560
561 /// Parse a decrypted payload.
562 pub fn decode_payload(ty: MsgType, p: &[u8]) -> Result<Self, DecodeError> {
563 let exact = |what: &'static str, expected: usize| -> Result<(), DecodeError> {
564 if p.len() == expected {
565 Ok(())
566 } else {
567 Err(DecodeError::BadPayloadLen {
568 what,
569 expected,
570 actual: p.len(),
571 })
572 }
573 };
574
575 Ok(match ty {
576 MsgType::Loc => {
577 let count = *p.first().ok_or(DecodeError::BadPayloadLen {
578 what: "LOC",
579 expected: 1 + POINT_LEN,
580 actual: 0,
581 })? as usize;
582 if !(1..=MAX_POINTS).contains(&count) {
583 return Err(DecodeError::BadPointCount(count));
584 }
585 exact("LOC", 1 + count * POINT_LEN)?;
586 let points = p[1..]
587 .chunks_exact(POINT_LEN)
588 .map(|c| {
589 Point::from_bytes(c.try_into().expect("chunks_exact yields POINT_LEN"))
590 })
591 .collect();
592 Self::Loc(points)
593 }
594 MsgType::Ack => {
595 let count = *p.first().ok_or(DecodeError::BadPayloadLen {
596 what: "ACK",
597 expected: Ack::payload_len_for(1),
598 actual: 0,
599 })? as usize;
600 if !(1..=MAX_POINTS).contains(&count) {
601 return Err(DecodeError::BadNonceCount(count));
602 }
603 exact("ACK", Ack::payload_len_for(count))?;
604 let nonces = p[1..1 + count * NONCE_LEN]
605 .chunks_exact(NONCE_LEN)
606 .map(|c| -> Nonce { c.try_into().expect("chunks_exact yields NONCE_LEN") })
607 .collect();
608 let tail = 1 + count * NONCE_LEN;
609 Self::Ack(Ack {
610 nonces,
611 flags: AckFlags(p[tail]),
612 })
613 }
614 MsgType::Nack => {
615 exact("NACK", NACK_LEN)?;
616 Self::Nack(Nack {
617 nonce: p[..NONCE_LEN].try_into().expect("length checked"),
618 reason: NackReason::try_from(p[NONCE_LEN])?,
619 retry_after_s: p[NONCE_LEN + 1],
620 })
621 }
622 MsgType::Hello => {
623 exact("HELLO", HELLO_LEN)?;
624 Self::Hello(Hello {
625 app_version_code: be16(p),
626 os_api_level: p[2],
627 flags: HelloFlags(p[3]),
628 config_version: be16(&p[4..]),
629 })
630 }
631 MsgType::Config => {
632 exact("CONFIG", CONFIG_LEN)?;
633 Self::Config(Config {
634 config_version: be16(p),
635 profile: Profile::try_from(p[2])?,
636 flags: ConfigFlags(p[3]),
637 heartbeat_s: be16(&p[4..]),
638 interval_scale_pct: be16(&p[6..]),
639 min_distance_m: be16(&p[8..]),
640 max_points_per_loc: p[10],
641 })
642 }
643 MsgType::ConfigGet => {
644 exact("CONFIG_GET", CONFIG_GET_LEN)?;
645 Self::ConfigGet(ConfigGet {
646 have_version: be16(p),
647 })
648 }
649 MsgType::Ping => {
650 exact("PING", PING_LEN)?;
651 Self::Ping(Ping {
652 echo: be32(p),
653 seq: be16(&p[4..]),
654 })
655 }
656 MsgType::Pong => {
657 exact("PONG", PONG_LEN)?;
658 Self::Pong(Pong {
659 echo: be32(p),
660 seq: be16(&p[4..]),
661 })
662 }
663 MsgType::Revoked => {
664 exact("REVOKED", REVOKED_LEN)?;
665 Self::Revoked(Revoked {
666 reason: RevokeReason::try_from(p[0])?,
667 })
668 }
669 })
670 }
671}
672
673fn be16(b: &[u8]) -> u16 {
674 u16::from_be_bytes([b[0], b[1]])
675}
676
677fn be32(b: &[u8]) -> u32 {
678 u32::from_be_bytes([b[0], b[1], b[2], b[3]])
679}
680
681#[cfg(test)]
682mod tests {
683 use super::*;
684
685 fn round_trip(m: &Message) {
686 let bytes = m.encode_payload();
687 assert_eq!(bytes.len(), m.payload_len());
688 let back = Message::decode_payload(m.msg_type(), &bytes).expect("decodes");
689 assert_eq!(&back, m);
690 }
691
692 #[test]
693 fn every_type_round_trips() {
694 round_trip(&Message::Loc(vec![Point::new(
695 1_785_000_042,
696 525_200_080,
697 134_050_000,
698 )]));
699 round_trip(&Message::Loc(
700 (0..MAX_POINTS as u32)
701 .map(|i| Point::new(i, i as i32, -(i as i32)))
702 .collect(),
703 ));
704 round_trip(&Message::Ack(Ack {
705 nonces: vec![[7; NONCE_LEN], [9; NONCE_LEN]],
706 flags: AckFlags::CONFIG_PENDING,
707 }));
708 round_trip(&Message::Nack(Nack {
709 nonce: [3; NONCE_LEN],
710 reason: NackReason::RateLimited,
711 retry_after_s: 30,
712 }));
713 round_trip(&Message::Hello(Hello {
714 app_version_code: 17,
715 os_api_level: 34,
716 flags: HelloFlags::FIRST_LAUNCH,
717 config_version: 2,
718 }));
719 round_trip(&Message::Config(Config::default()));
720 round_trip(&Message::ConfigGet(ConfigGet { have_version: 2 }));
721 round_trip(&Message::Ping(Ping {
722 echo: 0xDEAD_BEEF,
723 seq: 5,
724 }));
725 round_trip(&Message::Pong(Pong {
726 echo: 0xDEAD_BEEF,
727 seq: 5,
728 }));
729 }
730
731 #[test]
732 fn loc_rejects_degenerate_counts() {
733 assert_eq!(
734 Message::decode_payload(MsgType::Loc, &[0]),
735 Err(DecodeError::BadPointCount(0))
736 );
737 let mut too_many = vec![(MAX_POINTS + 1) as u8];
738 too_many.extend(std::iter::repeat_n(0u8, (MAX_POINTS + 1) * POINT_LEN));
739 assert_eq!(
740 Message::decode_payload(MsgType::Loc, &too_many),
741 Err(DecodeError::BadPointCount(MAX_POINTS + 1))
742 );
743 }
744
745 #[test]
746 fn loc_rejects_a_count_that_disagrees_with_the_length() {
747 // Claims two points, carries one. The classic truncation bug.
748 let mut p = vec![2u8];
749 p.extend_from_slice(&Point::new(1, 2, 3).to_bytes());
750 assert!(matches!(
751 Message::decode_payload(MsgType::Loc, &p),
752 Err(DecodeError::BadPayloadLen { .. })
753 ));
754 }
755
756 #[test]
757 fn fixed_payload_sizes_are_what_the_spec_says() {
758 assert_eq!(
759 Message::Nack(Nack {
760 nonce: [0; NONCE_LEN],
761 reason: NackReason::Malformed,
762 retry_after_s: 0
763 })
764 .payload_len(),
765 14
766 );
767 assert_eq!(
768 Message::Hello(Hello {
769 app_version_code: 0,
770 os_api_level: 0,
771 flags: HelloFlags::NONE,
772 config_version: 0
773 })
774 .payload_len(),
775 6
776 );
777 assert_eq!(Message::Config(Config::default()).payload_len(), 12);
778 assert_eq!(Message::ConfigGet(ConfigGet::default()).payload_len(), 2);
779 assert_eq!(Message::Ping(Ping::default()).payload_len(), 6);
780 assert_eq!(Message::Pong(Pong::default()).payload_len(), 6);
781 assert_eq!(Message::Loc(vec![Point::new(0, 0, 0)]).payload_len(), 25);
782 }
783
784 /// CONFIG holds the only reserved byte left in the protocol. A later version
785 /// may populate it, and this build must ignore it rather than reject the
786 /// packet.
787 #[test]
788 fn reserved_bytes_do_not_break_decoding() {
789 let config = Config::default();
790 let mut p = Message::Config(config).encode_payload();
791 p[11] = 0xAB;
792 assert_eq!(
793 Message::decode_payload(MsgType::Config, &p),
794 Ok(Message::Config(config))
795 );
796 }
797
798 #[test]
799 fn unknown_discriminants_are_rejected() {
800 assert!(MsgType::try_from(0).is_err());
801 assert!(MsgType::try_from(10).is_err());
802 assert!(Profile::try_from(3).is_err());
803 assert!(NackReason::try_from(0).is_err());
804 assert!(NackReason::try_from(5).is_err());
805 assert!(RevokeReason::try_from(0).is_err());
806 assert!(RevokeReason::try_from(4).is_err());
807 }
808
809 #[test]
810 fn direction_split_matches_the_key_schedule() {
811 for ty in MsgType::ALL {
812 assert_eq!(
813 ty.direction() == Direction::Up,
814 matches!(
815 ty,
816 MsgType::Loc | MsgType::Hello | MsgType::ConfigGet | MsgType::Ping
817 )
818 );
819 }
820 assert_ne!(Direction::Up.info(), Direction::Down.info());
821 }
822}
823