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
5use aes_gcm::{Aes256Gcm, KeyInit, aead::Aead};
6use hmac::{Hmac, Mac};
7use minicbor::{Decode, Encode};
8use sha2::Sha256;
9use spake2::{Ed25519Group, Identity, Password, Spake2};
10use std::io::{self, Read, Write};
11
12/// The opening's version; peers must agree on its authentication and access rules.
13pub 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)]
17pub struct Version(pub u16);
18
19impl 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
29impl std::error::Error for Version {}
30/// The largest block either side reads.
31const MOST: usize = 16 << 20;
32
33/// Message kinds.
34pub 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.
73pub 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)]
106pub 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
130impl 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)]
151pub 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)]
165pub 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)]
175pub 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)]
185pub struct Guid {
186 #[cbor(n(0), with = "minicbor::bytes")]
187 pub guid: [u8; 16],
188 #[n(1)]
189 pub n: u32,
190}
191
192impl 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
201impl 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)]
213pub 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)]
221pub 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)]
237pub 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)]
249pub 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)]
262pub 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)]
276pub 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)]
286pub 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)]
319pub 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)]
347pub 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)]
359pub struct WireStamp {
360 #[cbor(n(0), with = "minicbor::bytes")]
361 pub header: Vec<u8>,
362 #[n(1)]
363 pub length: u64,
364}
365
366impl 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
375impl 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)]
393pub 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`.
406const 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
429pub 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
436pub 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)]
446struct 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.
459pub struct Sealer {
460 cipher: Aes256Gcm,
461 count: u64,
462}
463
464impl 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
530fn 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)]
538pub 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.
546pub 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
563pub(super) struct Opening {
564 pake: Spake2<Ed25519Group>,
565 side: Side,
566 room: String,
567}
568
569impl 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
620fn 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
628fn 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
640fn invalid(message: &'static str) -> io::Error {
641 io::Error::new(io::ErrorKind::InvalidData, message)
642}