msg.rs
| 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 | |
| 14 | use crate::error::DecodeError; |
| 15 | use crate::point::{POINT_LEN, Point}; |
| 16 | |
| 17 | /// Number of nonce bytes, which is also the length of a message id. |
| 18 | pub 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. |
| 23 | pub 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. |
| 29 | pub const MAX_POINTS: usize = 40; |
| 30 | |
| 31 | const HELLO_LEN: usize = 6; |
| 32 | const CONFIG_LEN: usize = 12; |
| 33 | const CONFIG_GET_LEN: usize = 2; |
| 34 | const PING_LEN: usize = 6; |
| 35 | const PONG_LEN: usize = 6; |
| 36 | const NACK_LEN: usize = NONCE_LEN + 2; |
| 37 | const 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)] |
| 43 | pub 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 | |
| 56 | impl 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 | |
| 87 | impl 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)] |
| 107 | pub enum Direction { |
| 108 | /// Device → server, `K_up`. |
| 109 | Up, |
| 110 | /// Server → device, `K_down`. |
| 111 | Down, |
| 112 | } |
| 113 | |
| 114 | impl 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))] |
| 133 | pub struct AckFlags(pub u8); |
| 134 | |
| 135 | impl 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))] |
| 152 | pub struct Ack { |
| 153 | pub nonces: Vec<Nonce>, |
| 154 | pub flags: AckFlags, |
| 155 | } |
| 156 | |
| 157 | impl 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)] |
| 179 | pub 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 | |
| 190 | impl 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))] |
| 210 | pub 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)] |
| 225 | pub 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 | |
| 235 | impl 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))] |
| 275 | pub 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))] |
| 286 | pub struct HelloFlags(pub u8); |
| 287 | |
| 288 | impl 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))] |
| 299 | pub 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)] |
| 315 | pub enum Profile { |
| 316 | BatterySaver = 0, |
| 317 | #[default] |
| 318 | Balanced = 1, |
| 319 | HighAccuracy = 2, |
| 320 | } |
| 321 | |
| 322 | impl 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))] |
| 342 | pub struct ConfigFlags(pub u8); |
| 343 | |
| 344 | impl 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))] |
| 364 | pub 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 | |
| 377 | impl 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))] |
| 395 | pub 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))] |
| 407 | pub 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))] |
| 418 | pub 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 | )] |
| 436 | pub 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 | |
| 449 | impl 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 | |
| 673 | fn be16(b: &[u8]) -> u16 { |
| 674 | u16::from_be_bytes([b[0], b[1]]) |
| 675 | } |
| 676 | |
| 677 | fn be32(b: &[u8]) -> u32 { |
| 678 | u32::from_be_bytes([b[0], b[1], b[2], b[3]]) |
| 679 | } |
| 680 | |
| 681 | #[cfg(test)] |
| 682 | mod 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 |