| 1 | //! What peers say to each other: an opening in the clear that meets through the secret both |
| 2 | //! hold (SPAKE2), then frames sealed under the keys it agreed, each a message kind and a CBOR |
| 3 | //! map. Readers skip unknown kinds and fields within the same opening version. |
| 4 | |
| 5 | use aes_gcm::{Aes256Gcm, KeyInit, aead::Aead}; |
| 6 | use hmac::{Hmac, Mac}; |
| 7 | use minicbor::{Decode, Encode}; |
| 8 | use sha2::Sha256; |
| 9 | use spake2::{Ed25519Group, Identity, Password, Spake2}; |
| 10 | use std::io::{self, Read, Write}; |
| 11 | |
| 12 | /// The opening's version; peers must agree on its authentication and access rules. |
| 13 | pub const VERSION: u16 = 3; |
| 14 | |
| 15 | /// The version of the opening a peer of another version sent, as `open` fails with it. |
| 16 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 17 | pub struct Version(pub u16); |
| 18 | |
| 19 | impl std::fmt::Display for Version { |
| 20 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
| 21 | write!( |
| 22 | f, |
| 23 | "The peer speaks Live Share version {}, this end {VERSION}", |
| 24 | self.0 |
| 25 | ) |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | impl std::error::Error for Version {} |
| 30 | /// The largest block either side reads. |
| 31 | const MOST: usize = 16 << 20; |
| 32 | |
| 33 | /// Message kinds. |
| 34 | pub mod kind { |
| 35 | pub const HELLO: u16 = 1; |
| 36 | /// Keeps a quiet connection open; it says nothing else. |
| 37 | pub const PING: u16 = 2; |
| 38 | pub const BYE: u16 = 3; |
| 39 | /// A `Hello` in answer to one heard in a notebook's room's group. |
| 40 | pub const HELLO_BACK: u16 = 4; |
| 41 | pub const PRESENCE: u16 = 16; |
| 42 | /// A host's files changed: `Touched`. |
| 43 | pub const TOUCHED: u16 = 18; |
| 44 | /// A section's bytes as a commit changed them: `Delta`. |
| 45 | pub const DELTA: u16 = 19; |
| 46 | pub const WELCOME: u16 = 32; |
| 47 | pub const APPROVAL: u16 = 33; |
| 48 | /// Storage requests to a host, each a `Request` answered by a `Reply`. |
| 49 | pub const LIST: u16 = 257; |
| 50 | pub const STAMP: u16 = 258; |
| 51 | /// A section's consistent image, a chunk at a time. |
| 52 | pub const READ: u16 = 259; |
| 53 | pub const COMMIT: u16 = 260; |
| 54 | pub const CONFIRM: u16 = 261; |
| 55 | pub const CREATE: u16 = 262; |
| 56 | pub const CREATE_DIRECTORY: u16 = 263; |
| 57 | pub const HIDE: u16 = 264; |
| 58 | pub const RENAME: u16 = 265; |
| 59 | pub const REPLACE: u16 = 266; |
| 60 | pub const DELETE: u16 = 267; |
| 61 | pub const PLACE: u16 = 268; |
| 62 | pub const SUPERSEDE: u16 = 269; |
| 63 | /// Any other file as it stands, a chunk at a time. |
| 64 | pub const READ_FILE: u16 = 270; |
| 65 | pub const EXISTS: u16 = 271; |
| 66 | /// A chunk of the bytes a later request carries. |
| 67 | pub const PUT: u16 = 272; |
| 68 | pub const EDITS: u16 = 273; |
| 69 | pub const REPLY: u16 = 511; |
| 70 | } |
| 71 | |
| 72 | /// The kinds this version reads, as `Hello::kinds` lists them. |
| 73 | pub const KNOWN: &[u16] = &[ |
| 74 | kind::HELLO, |
| 75 | kind::PING, |
| 76 | kind::BYE, |
| 77 | kind::HELLO_BACK, |
| 78 | kind::PRESENCE, |
| 79 | kind::TOUCHED, |
| 80 | kind::DELTA, |
| 81 | kind::WELCOME, |
| 82 | kind::APPROVAL, |
| 83 | kind::LIST, |
| 84 | kind::STAMP, |
| 85 | kind::READ, |
| 86 | kind::COMMIT, |
| 87 | kind::CONFIRM, |
| 88 | kind::CREATE, |
| 89 | kind::CREATE_DIRECTORY, |
| 90 | kind::HIDE, |
| 91 | kind::RENAME, |
| 92 | kind::REPLACE, |
| 93 | kind::DELETE, |
| 94 | kind::PLACE, |
| 95 | kind::SUPERSEDE, |
| 96 | kind::READ_FILE, |
| 97 | kind::EXISTS, |
| 98 | kind::PUT, |
| 99 | kind::EDITS, |
| 100 | kind::REPLY, |
| 101 | ]; |
| 102 | |
| 103 | /// The first message each way, and the only one with a name and a picture. |
| 104 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] |
| 105 | #[cbor(map)] |
| 106 | pub struct Hello { |
| 107 | /// Random for each run of the app. |
| 108 | #[cbor(n(0), with = "minicbor::bytes")] |
| 109 | pub peer: [u8; 16], |
| 110 | #[n(1)] |
| 111 | pub name: String, |
| 112 | /// A PNG of the person, at most 96 pixels a side. |
| 113 | #[cbor(n(2), with = "minicbor::bytes")] |
| 114 | pub picture: Option<Vec<u8>>, |
| 115 | /// The app and its version, for diagnostics. |
| 116 | #[n(3)] |
| 117 | pub app: String, |
| 118 | /// The message kinds the sender reads; a request waits for its kind to be listed. |
| 119 | #[n(4)] |
| 120 | pub kinds: Vec<u16>, |
| 121 | /// The share this peer hosts, whose storage requests it answers. |
| 122 | #[cbor(n(5), with = "minicbor::bytes")] |
| 123 | pub serves: Option<[u8; 16]>, |
| 124 | #[n(6)] |
| 125 | pub ops: Option<u16>, |
| 126 | #[n(7)] |
| 127 | pub device: Option<String>, |
| 128 | } |
| 129 | |
| 130 | impl Hello { |
| 131 | /// A hello from a new peer, named `name`, reading the kinds this version reads. |
| 132 | pub fn new(name: String, picture: Option<Vec<u8>>) -> io::Result<Self> { |
| 133 | let mut peer = [0; 16]; |
| 134 | getrandom::fill(&mut peer).map_err(|_| io::Error::other("System random source failed"))?; |
| 135 | Ok(Self { |
| 136 | peer, |
| 137 | name, |
| 138 | picture, |
| 139 | app: format!("Snowbound {}", env!("CARGO_PKG_VERSION")), |
| 140 | kinds: KNOWN.to_vec(), |
| 141 | serves: None, |
| 142 | ops: Some(1), |
| 143 | device: None, |
| 144 | }) |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | /// Where someone is: the section and page they have open and their caret on it. |
| 149 | #[derive(Clone, Debug, Default, PartialEq, Encode, Decode)] |
| 150 | #[cbor(map)] |
| 151 | pub struct Presence { |
| 152 | /// The section file's identity (its header's guidFile). |
| 153 | #[cbor(n(0), with = "minicbor::bytes")] |
| 154 | pub section: Option<[u8; 16]>, |
| 155 | /// The page's object space. |
| 156 | #[n(1)] |
| 157 | pub page: Option<Guid>, |
| 158 | #[n(2)] |
| 159 | pub caret: Option<Caret>, |
| 160 | } |
| 161 | |
| 162 | /// A selection, collapsed where `anchor` is `focus`. |
| 163 | #[derive(Clone, Copy, Debug, PartialEq, Encode, Decode)] |
| 164 | #[cbor(map)] |
| 165 | pub struct Caret { |
| 166 | #[n(0)] |
| 167 | pub anchor: Spot, |
| 168 | #[n(1)] |
| 169 | pub focus: Spot, |
| 170 | } |
| 171 | |
| 172 | /// A place in a text object, in UTF-16 code units, as ops address text. |
| 173 | #[derive(Clone, Copy, Debug, PartialEq, Encode, Decode)] |
| 174 | #[cbor(map)] |
| 175 | pub struct Spot { |
| 176 | #[n(0)] |
| 177 | pub text: Guid, |
| 178 | #[n(1)] |
| 179 | pub offset: u32, |
| 180 | } |
| 181 | |
| 182 | /// An `ExGuid`. |
| 183 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Encode, Decode)] |
| 184 | #[cbor(array)] |
| 185 | pub struct Guid { |
| 186 | #[cbor(n(0), with = "minicbor::bytes")] |
| 187 | pub guid: [u8; 16], |
| 188 | #[n(1)] |
| 189 | pub n: u32, |
| 190 | } |
| 191 | |
| 192 | impl From<onestore::ExGuid> for Guid { |
| 193 | fn from(id: onestore::ExGuid) -> Self { |
| 194 | Self { |
| 195 | guid: id.guid, |
| 196 | n: id.n, |
| 197 | } |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | impl From<Guid> for onestore::ExGuid { |
| 202 | fn from(id: Guid) -> Self { |
| 203 | Self { |
| 204 | guid: id.guid, |
| 205 | n: id.n, |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | /// Why a peer leaves: `left`, or `stopped` for a host that stopped sharing. |
| 211 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] |
| 212 | #[cbor(map)] |
| 213 | pub struct Bye { |
| 214 | #[n(0)] |
| 215 | pub reason: String, |
| 216 | } |
| 217 | |
| 218 | /// A device's access credential, the current presence room, and the notebook's names. |
| 219 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] |
| 220 | #[cbor(map)] |
| 221 | pub struct Welcome { |
| 222 | #[cbor(n(0), with = "minicbor::bytes")] |
| 223 | pub share: [u8; 16], |
| 224 | #[cbor(n(1), with = "minicbor::bytes")] |
| 225 | pub secret: [u8; 16], |
| 226 | #[n(2)] |
| 227 | pub notebook: String, |
| 228 | /// The host's name for itself, as `Hello::name`. |
| 229 | #[n(3)] |
| 230 | pub host: String, |
| 231 | #[cbor(n(4), with = "minicbor::bytes")] |
| 232 | pub room: [u8; 16], |
| 233 | } |
| 234 | |
| 235 | #[derive(Clone, Debug, Encode, Decode)] |
| 236 | #[cbor(index_only)] |
| 237 | pub enum Approval { |
| 238 | #[n(0)] |
| 239 | Pending, |
| 240 | #[n(1)] |
| 241 | Declined, |
| 242 | #[n(2)] |
| 243 | Failed, |
| 244 | } |
| 245 | |
| 246 | /// Paths a host's files changed at, by catalog path; `""` is the notebook's folder. |
| 247 | #[derive(Clone, Debug, Default, PartialEq, Encode, Decode)] |
| 248 | #[cbor(map)] |
| 249 | pub struct Touched { |
| 250 | #[n(0)] |
| 251 | pub paths: Vec<String>, |
| 252 | /// Each path's stamp now, as `share::digest` hashes it, where it has one: a guest holding |
| 253 | /// that image knows the stamp without asking. |
| 254 | #[n(1)] |
| 255 | pub stamps: Vec<Option<u64>>, |
| 256 | } |
| 257 | |
| 258 | /// What a commit changed in a section a host serves: from the image with stamp `base`, the |
| 259 | /// image `length` long with `writes` in place. A guest holding the base needs read nothing. |
| 260 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] |
| 261 | #[cbor(map)] |
| 262 | pub struct Delta { |
| 263 | #[n(0)] |
| 264 | pub path: String, |
| 265 | #[n(1)] |
| 266 | pub base: WireStamp, |
| 267 | #[n(2)] |
| 268 | pub length: u64, |
| 269 | #[n(3)] |
| 270 | pub writes: Vec<Written>, |
| 271 | } |
| 272 | |
| 273 | /// Bytes at an offset. |
| 274 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] |
| 275 | #[cbor(map)] |
| 276 | pub struct Written { |
| 277 | #[n(0)] |
| 278 | pub offset: u64, |
| 279 | #[cbor(n(1), with = "minicbor::bytes")] |
| 280 | pub bytes: Vec<u8>, |
| 281 | } |
| 282 | |
| 283 | /// A storage request; its kind names the verb, and the verb what it carries. |
| 284 | #[derive(Clone, Debug, Default, PartialEq, Encode, Decode)] |
| 285 | #[cbor(map)] |
| 286 | pub struct Request { |
| 287 | /// Names the reply; a `PUT`'s names the bytes a later request carries. |
| 288 | #[n(0)] |
| 289 | pub id: u64, |
| 290 | /// A catalog path. |
| 291 | #[n(1)] |
| 292 | pub path: String, |
| 293 | /// A rename's or replacement's target, or the file a supersession puts in place. |
| 294 | #[n(2)] |
| 295 | pub to: Option<String>, |
| 296 | #[n(3)] |
| 297 | pub offset: Option<u64>, |
| 298 | #[n(4)] |
| 299 | pub limit: Option<u64>, |
| 300 | #[cbor(n(5), with = "minicbor::bytes")] |
| 301 | pub bytes: Option<Vec<u8>>, |
| 302 | /// A confirmation's or supersession's base; for a section's read, the image the guest |
| 303 | /// holds, which the reply may give the changes to. |
| 304 | #[n(6)] |
| 305 | pub stamp: Option<WireStamp>, |
| 306 | #[cbor(n(7), with = "minicbor::bytes")] |
| 307 | pub ancestor: Option<[u8; 16]>, |
| 308 | #[n(8)] |
| 309 | pub name: Option<String>, |
| 310 | /// The `PUT`s, by id, whose bytes this request carries in place of `bytes`; a read's |
| 311 | /// snapshot after its first chunk. |
| 312 | #[n(9)] |
| 313 | pub handle: Option<u64>, |
| 314 | } |
| 315 | |
| 316 | /// A request's answer: a failure, or what the verb gives. |
| 317 | #[derive(Clone, Debug, Default, PartialEq, Encode, Decode)] |
| 318 | #[cbor(map)] |
| 319 | pub struct Reply { |
| 320 | #[n(0)] |
| 321 | pub id: u64, |
| 322 | #[n(1)] |
| 323 | pub failure: Option<Failure>, |
| 324 | #[cbor(n(2), with = "minicbor::bytes")] |
| 325 | pub bytes: Option<Vec<u8>>, |
| 326 | /// A read's whole length. |
| 327 | #[n(3)] |
| 328 | pub length: Option<u64>, |
| 329 | #[n(4)] |
| 330 | pub stamp: Option<WireStamp>, |
| 331 | #[n(5)] |
| 332 | pub entries: Option<Vec<WireEntry>>, |
| 333 | #[n(6)] |
| 334 | pub exists: Option<bool>, |
| 335 | /// The snapshot a read's later chunks come from. |
| 336 | #[n(7)] |
| 337 | pub handle: Option<u64>, |
| 338 | /// A read's changes to the image with the request's stamp, in place of its bytes. |
| 339 | #[n(8)] |
| 340 | pub writes: Option<Vec<Written>>, |
| 341 | } |
| 342 | |
| 343 | /// Why a request failed: an `io::ErrorKind` as `error_kind` numbers it, and for a commit, |
| 344 | /// how far it got (`commit_state`). |
| 345 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] |
| 346 | #[cbor(map)] |
| 347 | pub struct Failure { |
| 348 | #[n(0)] |
| 349 | pub kind: u16, |
| 350 | #[n(1)] |
| 351 | pub message: String, |
| 352 | #[n(2)] |
| 353 | pub state: Option<u8>, |
| 354 | } |
| 355 | |
| 356 | /// An `onestore::Stamp`. |
| 357 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] |
| 358 | #[cbor(map)] |
| 359 | pub struct WireStamp { |
| 360 | #[cbor(n(0), with = "minicbor::bytes")] |
| 361 | pub header: Vec<u8>, |
| 362 | #[n(1)] |
| 363 | pub length: u64, |
| 364 | } |
| 365 | |
| 366 | impl From<&onestore::Stamp> for WireStamp { |
| 367 | fn from(stamp: &onestore::Stamp) -> Self { |
| 368 | Self { |
| 369 | header: stamp.header.to_vec(), |
| 370 | length: stamp.length, |
| 371 | } |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | impl TryFrom<&WireStamp> for onestore::Stamp { |
| 376 | type Error = io::Error; |
| 377 | |
| 378 | fn try_from(stamp: &WireStamp) -> io::Result<Self> { |
| 379 | Ok(Self { |
| 380 | header: stamp |
| 381 | .header |
| 382 | .as_slice() |
| 383 | .try_into() |
| 384 | .map_err(|_| invalid("A stamp's header is 1024 bytes"))?, |
| 385 | length: stamp.length, |
| 386 | }) |
| 387 | } |
| 388 | } |
| 389 | |
| 390 | /// A folder's entry, as `discover::Entry`. |
| 391 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] |
| 392 | #[cbor(map)] |
| 393 | pub struct WireEntry { |
| 394 | #[n(0)] |
| 395 | pub name: String, |
| 396 | /// 0 a file, 1 a folder, 2 anything else, 3 a file kept elsewhere. |
| 397 | #[n(1)] |
| 398 | pub kind: u8, |
| 399 | #[n(2)] |
| 400 | pub size: u64, |
| 401 | #[n(3)] |
| 402 | pub modified: u64, |
| 403 | } |
| 404 | |
| 405 | /// The `io::ErrorKind`s a failure names, by number; any other is `Other`. |
| 406 | const ERROR_KINDS: [io::ErrorKind; 20] = [ |
| 407 | io::ErrorKind::Other, |
| 408 | io::ErrorKind::NotFound, |
| 409 | io::ErrorKind::PermissionDenied, |
| 410 | io::ErrorKind::AlreadyExists, |
| 411 | io::ErrorKind::InvalidInput, |
| 412 | io::ErrorKind::InvalidData, |
| 413 | io::ErrorKind::TimedOut, |
| 414 | io::ErrorKind::WouldBlock, |
| 415 | io::ErrorKind::ResourceBusy, |
| 416 | io::ErrorKind::Unsupported, |
| 417 | io::ErrorKind::FileTooLarge, |
| 418 | io::ErrorKind::NotConnected, |
| 419 | io::ErrorKind::ReadOnlyFilesystem, |
| 420 | io::ErrorKind::DirectoryNotEmpty, |
| 421 | io::ErrorKind::NotADirectory, |
| 422 | io::ErrorKind::IsADirectory, |
| 423 | io::ErrorKind::StorageFull, |
| 424 | io::ErrorKind::UnexpectedEof, |
| 425 | io::ErrorKind::Interrupted, |
| 426 | io::ErrorKind::BrokenPipe, |
| 427 | ]; |
| 428 | |
| 429 | pub fn error_number(kind: io::ErrorKind) -> u16 { |
| 430 | ERROR_KINDS |
| 431 | .iter() |
| 432 | .position(|known| *known == kind) |
| 433 | .unwrap_or(0) as u16 |
| 434 | } |
| 435 | |
| 436 | pub fn error_kind(number: u16) -> io::ErrorKind { |
| 437 | ERROR_KINDS |
| 438 | .get(usize::from(number)) |
| 439 | .copied() |
| 440 | .unwrap_or(io::ErrorKind::Other) |
| 441 | } |
| 442 | |
| 443 | /// The opening, sent in the clear by the side that connected and answered by the other. |
| 444 | #[derive(Encode, Decode)] |
| 445 | #[cbor(map)] |
| 446 | struct Open { |
| 447 | #[n(0)] |
| 448 | version: u16, |
| 449 | /// The room it means, as discovery names it. |
| 450 | #[n(1)] |
| 451 | room: String, |
| 452 | #[cbor(n(2), with = "minicbor::bytes")] |
| 453 | pake: Vec<u8>, |
| 454 | } |
| 455 | |
| 456 | /// One direction's AEAD key and the count of frames sealed under it. Each frame carries its |
| 457 | /// number, which is also its nonce, so a frame lost, repeated, reordered or forged on the way |
| 458 | /// is caught before anything in it or after it is read. |
| 459 | pub struct Sealer { |
| 460 | cipher: Aes256Gcm, |
| 461 | count: u64, |
| 462 | } |
| 463 | |
| 464 | impl Sealer { |
| 465 | fn new(key: &[u8], purpose: &[u8]) -> Self { |
| 466 | let mut mac = <Hmac<Sha256> as hmac::KeyInit>::new_from_slice(key).expect("any key length"); |
| 467 | mac.update(purpose); |
| 468 | Self { |
| 469 | cipher: Aes256Gcm::new_from_slice(&mac.finalize().into_bytes()).expect("a 32-byte key"), |
| 470 | count: 0, |
| 471 | } |
| 472 | } |
| 473 | |
| 474 | /// Writes message `kind` holding `body`. |
| 475 | pub fn send( |
| 476 | &mut self, |
| 477 | to: &mut impl Write, |
| 478 | kind: u16, |
| 479 | body: &impl Encode<()>, |
| 480 | ) -> io::Result<()> { |
| 481 | let body = minicbor::to_vec(body).map_err(io::Error::other)?; |
| 482 | self.send_encoded(to, kind, &body) |
| 483 | } |
| 484 | |
| 485 | /// `send` for a body already encoded. |
| 486 | pub fn send_encoded(&mut self, to: &mut impl Write, kind: u16, body: &[u8]) -> io::Result<()> { |
| 487 | let clear = [&kind.to_be_bytes()[..], body].concat(); |
| 488 | let number = self.count.to_be_bytes(); |
| 489 | let sealed = self |
| 490 | .cipher |
| 491 | .encrypt(&nonce(number).into(), clear.as_slice()) |
| 492 | .map_err(|_| io::Error::other("A frame could not be sealed"))?; |
| 493 | self.count += 1; |
| 494 | write_block(to, &[&number[..], &sealed].concat()) |
| 495 | } |
| 496 | |
| 497 | /// Reads the next message: its kind and body. An error of kind `InvalidData` means the |
| 498 | /// stream broke, and nothing more on it can be trusted. |
| 499 | pub fn receive(&mut self, from: &mut impl Read) -> io::Result<(u16, Vec<u8>)> { |
| 500 | let block = read_block(from)?; |
| 501 | let (number, sealed) = block |
| 502 | .split_first_chunk::<8>() |
| 503 | .ok_or_else(|| invalid("A frame without its number"))?; |
| 504 | let due = self.count; |
| 505 | if u64::from_be_bytes(*number) != due { |
| 506 | return Err(io::Error::new( |
| 507 | io::ErrorKind::InvalidData, |
| 508 | format!( |
| 509 | "Frame {} came where frame {due} was due", |
| 510 | u64::from_be_bytes(*number) |
| 511 | ), |
| 512 | )); |
| 513 | } |
| 514 | let mut clear = self |
| 515 | .cipher |
| 516 | .decrypt(&nonce(*number).into(), sealed) |
| 517 | .map_err(|_| { |
| 518 | io::Error::new( |
| 519 | io::ErrorKind::InvalidData, |
| 520 | format!("Frame {due} does not open under the agreed key"), |
| 521 | ) |
| 522 | })?; |
| 523 | self.count += 1; |
| 524 | let body = clear.split_off(2.min(clear.len())); |
| 525 | let kind = u16::from_be_bytes(clear.try_into().map_err(|_| invalid("An empty frame"))?); |
| 526 | Ok((kind, body)) |
| 527 | } |
| 528 | } |
| 529 | |
| 530 | fn nonce(number: [u8; 8]) -> [u8; 12] { |
| 531 | let mut nonce = [0; 12]; |
| 532 | nonce[4..].copy_from_slice(&number); |
| 533 | nonce |
| 534 | } |
| 535 | |
| 536 | /// Which end of the connection this is. |
| 537 | #[derive(Clone, Copy, PartialEq, Eq)] |
| 538 | pub enum Side { |
| 539 | Initiator, |
| 540 | Responder, |
| 541 | } |
| 542 | |
| 543 | /// Meets the peer at the other end of `stream` in `room` through `secret`: the sealers to |
| 544 | /// send and to receive with. A peer holding another secret goes unnoticed here; its first |
| 545 | /// frame then fails to open. |
| 546 | pub fn open( |
| 547 | stream: &mut (impl Read + Write), |
| 548 | side: Side, |
| 549 | room: &str, |
| 550 | secret: &[u8], |
| 551 | ) -> io::Result<(Sealer, Sealer)> { |
| 552 | let (opening, ours) = Opening::new(side, room, secret)?; |
| 553 | if side == Side::Initiator { |
| 554 | write_block(stream, &ours)?; |
| 555 | } |
| 556 | let theirs = read_block(stream)?; |
| 557 | if side == Side::Responder { |
| 558 | write_block(stream, &ours)?; |
| 559 | } |
| 560 | opening.finish(&theirs) |
| 561 | } |
| 562 | |
| 563 | pub(super) struct Opening { |
| 564 | pake: Spake2<Ed25519Group>, |
| 565 | side: Side, |
| 566 | room: String, |
| 567 | } |
| 568 | |
| 569 | impl Opening { |
| 570 | pub(super) fn new(side: Side, room: &str, secret: &[u8]) -> io::Result<(Self, Vec<u8>)> { |
| 571 | let password = Password::new(secret); |
| 572 | let [initiator, responder] = [b"initiator", b"responder"] |
| 573 | .map(|role| Identity::new(&[&role[..], room.as_bytes()].concat())); |
| 574 | let (pake, message) = match side { |
| 575 | Side::Initiator => Spake2::<Ed25519Group>::start_a(&password, &initiator, &responder), |
| 576 | Side::Responder => Spake2::<Ed25519Group>::start_b(&password, &initiator, &responder), |
| 577 | }; |
| 578 | let ours = Open { |
| 579 | version: VERSION, |
| 580 | room: room.into(), |
| 581 | pake: message, |
| 582 | }; |
| 583 | let message = minicbor::to_vec(&ours).map_err(io::Error::other)?; |
| 584 | Ok(( |
| 585 | Self { |
| 586 | pake, |
| 587 | side, |
| 588 | room: room.to_owned(), |
| 589 | }, |
| 590 | message, |
| 591 | )) |
| 592 | } |
| 593 | |
| 594 | pub(super) fn finish(self, bytes: &[u8]) -> io::Result<(Sealer, Sealer)> { |
| 595 | let theirs: Open = minicbor::decode(bytes).map_err(|_| invalid("A malformed opening"))?; |
| 596 | if theirs.version != VERSION { |
| 597 | return Err(io::Error::new( |
| 598 | io::ErrorKind::Unsupported, |
| 599 | Version(theirs.version), |
| 600 | )); |
| 601 | } |
| 602 | if theirs.room != self.room { |
| 603 | return Err(invalid("The peer means another room")); |
| 604 | } |
| 605 | let key = self |
| 606 | .pake |
| 607 | .finish(&theirs.pake) |
| 608 | .map_err(|_| invalid("A malformed key exchange"))?; |
| 609 | let [from_initiator, from_responder] = [ |
| 610 | Sealer::new(&key, b"Snowbound live v1 initiator"), |
| 611 | Sealer::new(&key, b"Snowbound live v1 responder"), |
| 612 | ]; |
| 613 | Ok(match self.side { |
| 614 | Side::Initiator => (from_initiator, from_responder), |
| 615 | Side::Responder => (from_responder, from_initiator), |
| 616 | }) |
| 617 | } |
| 618 | } |
| 619 | |
| 620 | fn write_block(to: &mut impl Write, bytes: &[u8]) -> io::Result<()> { |
| 621 | let length = u32::try_from(bytes.len()) |
| 622 | .ok() |
| 623 | .filter(|length| *length as usize <= MOST) |
| 624 | .ok_or_else(|| invalid("A frame too large to send"))?; |
| 625 | to.write_all(&[&length.to_be_bytes()[..], bytes].concat()) |
| 626 | } |
| 627 | |
| 628 | fn read_block(from: &mut impl Read) -> io::Result<Vec<u8>> { |
| 629 | let mut length = [0; 4]; |
| 630 | from.read_exact(&mut length)?; |
| 631 | let length = u32::from_be_bytes(length) as usize; |
| 632 | if length > MOST { |
| 633 | return Err(invalid("A frame too large to read")); |
| 634 | } |
| 635 | let mut bytes = vec![0; length]; |
| 636 | from.read_exact(&mut bytes)?; |
| 637 | Ok(bytes) |
| 638 | } |
| 639 | |
| 640 | fn invalid(message: &'static str) -> io::Error { |
| 641 | io::Error::new(io::ErrorKind::InvalidData, message) |
| 642 | } |