offline_protocol_leaf/device.rs
1//! The device: what it does with a frame, and what it hands back.
2
3use alloc::{
4 format,
5 string::{String, ToString},
6 vec,
7 vec::Vec,
8};
9use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
10use mls_rs::client_builder::MlsConfig;
11use mls_rs::group::ReceivedMessage;
12use mls_rs::{Client, Group, MlsMessage};
13use offline_protocol_core::{Address, AppId, Message, MessagePriority};
14use offline_protocol_sealed::{
15 prefixes, EncryptedMessage, GroupId, KeyPackagePayload, MlsMessageType, WelcomeMessage,
16 MLS_ENVELOPE_COMPACT_V1,
17};
18
19use crate::adapters::{KeyPackageAdapter, PeerRecord, PRIOR_EPOCH_RETENTION};
20use crate::error::{LeafError, Result};
21use crate::frames;
22use crate::identity::{build_client, Identity};
23use crate::keypkg;
24use crate::shared::{Arc, SharedStore};
25use crate::store::{
26 LeafStore, KEY_TYPE_GROUP_EPOCH, KEY_TYPE_GROUP_STATE, KEY_TYPE_KEY_PACKAGE, KEY_TYPE_PEER,
27 KEY_TYPE_PEER_INDEX,
28};
29
30/// How many peers an **inbound** frame may add records for.
31///
32/// Every peer that sends a well-formed key package gets a record and a minted
33/// package in return, both of which are writes to flash. The signature on that
34/// frame proves the sender holds the key its address derives from, which is
35/// exactly as hard as generating a key, so without a bound a stranger fills a
36/// device's storage for the cost of some signing. The phone bounds the same
37/// exchange for the same reason.
38///
39/// It bounds what arrives, not what firmware chooses:
40/// [`LeafDevice::key_package_frame`] is the integrator deciding to pair and is
41/// not held to this. The failure being prevented is a stranger spending a
42/// device's flash, not an owner spending their own.
43///
44/// Sixteen covers a household and its guests. What it costs when full is
45/// stated on [`LeafError::TooManyPeers`].
46const MAX_PEERS: usize = 16;
47
48/// How far past the retained window [`LeafDevice::unpair`] sweeps, in **both**
49/// directions.
50///
51/// [`PRIOR_EPOCH_RETENTION`] says what a healthy device holds. This is the
52/// margin for one that is not, and each side of the window needs it for a
53/// different reason. Below: a delete that failed during trimming leaves a
54/// record holding an epoch's secrets. Above: the adapter writes epoch records
55/// before the state entry that sequences them, so a cut between the two leaves
56/// a record that every anchor agrees is not there. Forgetting a peer is the
57/// moment to clear both rather than the moment to assume they are absent.
58const FORGET_EPOCH_SLACK: u64 = 16;
59
60/// Something that happened, for the firmware to act on.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum LeafEvent {
63 /// A peer advertised itself. The device now knows what it may emit.
64 PeerAdvertised {
65 /// The peer's address.
66 peer: String,
67 },
68 /// A session came up. The device can seal to this peer from here.
69 SessionEstablished {
70 /// The peer's address.
71 peer: String,
72 },
73 /// A peer discarded its session and the device followed.
74 SessionReset {
75 /// The peer's address.
76 peer: String,
77 },
78 /// An application message arrived and decrypted.
79 ///
80 /// `peer` is proven: it is the address the sealing group member's own
81 /// signature key derives to. What it is not is permission. Any address in
82 /// radio range can complete a pairing, so firmware decides what a message
83 /// from this particular peer is allowed to actuate. A lock that opens for
84 /// whatever arrives on an established session opens for anyone patient
85 /// enough to pair with it.
86 MessageReceived {
87 /// The peer's address, proven rather than claimed.
88 peer: String,
89 /// The plaintext.
90 text: String,
91 },
92 /// A commit arrived and was applied. Post-compromise security advances
93 /// here, on the cadence the peer sets, because this device never commits.
94 CommitApplied {
95 /// The peer's address.
96 peer: String,
97 /// The epoch the session is now in.
98 epoch: u64,
99 },
100 /// A frame was not for this device, or carried nothing to act on.
101 ///
102 /// Surfaced rather than swallowed so a bench can tell "refused" from
103 /// "silently dropped", which are the two states a pairing failure looks
104 /// like from the outside.
105 Ignored {
106 /// Why nothing happened.
107 reason: String,
108 },
109}
110
111/// What a frame produced.
112#[derive(Debug, Default)]
113pub struct Handled {
114 /// Frames to transmit, in order. Every one is already durable.
115 pub outbound: Vec<Message>,
116 /// What the firmware should know about.
117 pub events: Vec<LeafEvent>,
118}
119
120/// What a frame proved about the peer it names.
121///
122/// A frame's sender is a plaintext field, and proving it is the whole business
123/// of the handlers [`LeafDevice::dispatch`] routes to. Two of that function's
124/// arms prove nothing, because a frame carrying nothing this device acts on
125/// gives them nothing to work with, and those two are not failures: they
126/// return normally, with a reason.
127///
128/// So "did not fail" and "proved who sent it" are different questions, and an
129/// acknowledgement is owed only for the second. Answering on the first would
130/// hand anyone within radio range exactly what
131/// [`LeafDevice::acknowledge`]'s known-peer gate exists to withhold, because
132/// naming a paired peer costs an attacker nothing beyond having overheard one
133/// frame.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135enum Sender {
136 /// A control signature verified against the address the frame names, or
137 /// the frame opened under a key only that peer holds.
138 Proven,
139 /// Nothing was checked. The sender is whatever the last hand to touch the
140 /// frame wrote there.
141 Claimed,
142}
143
144/// A leaf node.
145///
146/// # The never-committing profile
147///
148/// The peer creates the group, adds this device, and issues every commit. The
149/// device joins, opens what arrives, answers, and persists. It never emits a
150/// Welcome, a commit or a proposal, and never any group, rich, document or
151/// relay frame. Per-commit cost here is two elliptic-curve operations;
152/// per-message cost is symmetric only.
153///
154/// Post-compromise security therefore arrives on the peer's cadence rather
155/// than this device's, and a driven rekey reaches it as a key package with
156/// `session_reset` set rather than as an unsolicited Welcome.
157///
158/// # Storage is the source of truth
159///
160/// No MLS state is cached in this value. Every operation loads the group from
161/// the store, does its work, writes back, and only then returns the frame to
162/// send. That costs a load per frame, which is the right trade for a device
163/// that handles a frame every few minutes and must survive losing power
164/// between any two instructions: there is no in-RAM state for a power cut to
165/// desynchronize from what is on flash.
166///
167/// # One writer at a time
168///
169/// Every operation that advances state takes `&mut self`, so the compiler
170/// enforces what the ratchet requires. Two seals running at once would load
171/// the same generation, encrypt under the same key and nonce, and emit both
172/// frames: the AEAD nonce reuse this crate's whole persist-before-emit rule
173/// exists to prevent, arrived at without ever losing power. The same argument
174/// covers the send counter, where a race mints one id twice and the peer's
175/// deduplicator swallows the second message.
176///
177/// A device is therefore one value, exclusively held. Two `LeafDevice`s over
178/// one [`LeafStore`] would each satisfy the borrow checker and race anyway;
179/// construct one per store.
180///
181/// # What it does not defend against
182///
183/// **Anyone pairing.** Every gate here answers "is this peer the address it
184/// claims to be", and none of them answers "did the owner mean this peer".
185/// Producing a key that derives to its own address costs nothing, so an
186/// unattended device admits whoever asks, up to the bound above. That is the
187/// same position two phones are in, where the out-of-band artifact carries
188/// first-contact trust; on a device it is firmware that decides when the radio
189/// accepts a pairing at all, and firmware that decides what an opened message
190/// may actuate. [`LeafDevice::peers`] is how it audits what accumulated.
191///
192/// A replayed control frame, **as far as this device's clock is honest**. The
193/// signed payload states when the frame was made, so one older than two days
194/// is refused outright, and a reset-flagged key package (the destructive case,
195/// since it tears down a live session) is additionally refused unless it is
196/// newer than the last reset this device acted on. One capture is therefore
197/// worth one teardown, and only inside a two-day window.
198///
199/// What that leaves is the clock. Both checks are made against the time source
200/// firmware supplies, so a device that supplies a wrong one either refuses its
201/// peer or, if the clock is far enough behind, admits frames it should have
202/// refused. The obligation is stated with the others in
203/// [the provisioning chapter](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/spec/leaf-provisioning.md),
204/// and it is why the time source is a parameter on every entry point here
205/// rather than something this crate reaches for.
206///
207/// `Debug` renders the device's address and nothing else. Everything else it
208/// holds is either secret or a handle to secrets, and a device that printed
209/// its identity key into a log would undo the part of the threat model that
210/// says the key never leaves storage.
211pub struct LeafDevice {
212 store: Arc<dyn LeafStore>,
213 identity: Identity,
214 app_id: AppId,
215}
216
217impl LeafDevice {
218 /// Generates an identity and returns a device that holds it.
219 ///
220 /// Draws from the `getrandom` backend the firmware registered. Refuses if
221 /// the store already holds an identity, because replacing one changes the
222 /// device's address and silently orphans every peer paired with it.
223 pub fn provision(store: SharedStore, app_id: &str) -> Result<Self> {
224 let identity = Identity::provision(&store)?;
225 Ok(Self {
226 store,
227 identity,
228 app_id: parse_app_id(app_id)?,
229 })
230 }
231
232 /// Loads a device that was provisioned earlier.
233 pub fn resume(store: SharedStore, app_id: &str) -> Result<Self> {
234 let identity = Identity::resume(&store)?;
235 Ok(Self {
236 store,
237 identity,
238 app_id: parse_app_id(app_id)?,
239 })
240 }
241
242 /// Loads a device, provisioning one on first boot.
243 pub fn open(store: SharedStore, app_id: &str) -> Result<Self> {
244 match Self::resume(Arc::clone(&store), app_id) {
245 Err(LeafError::NotProvisioned) => Self::provision(store, app_id),
246 other => other,
247 }
248 }
249
250 /// This device's address. Self-certifying: it is the hash of the identity
251 /// key, so a peer checks a claim rather than trusting a directory.
252 pub fn address(&self) -> &Address {
253 &self.identity.address
254 }
255
256 /// Mints a key package and wraps it in a signed frame for `peer`.
257 ///
258 /// `now_unix_secs` is the pairing time source. It is a parameter because a
259 /// device has no clock, and passing something wrong here is the difference
260 /// between pairing and being refused as expired.
261 pub fn key_package_frame(&mut self, peer: &str, now_unix_secs: u64) -> Result<Message> {
262 self.key_package_frame_inner(peer, now_unix_secs, false)
263 }
264
265 fn key_package_frame_inner(
266 &mut self,
267 peer: &str,
268 now_unix_secs: u64,
269 session_reset: bool,
270 ) -> Result<Message> {
271 let client = self.client()?;
272 let minted = keypkg::mint(&client, now_unix_secs)?;
273 let payload = keypkg::payload(
274 &self.identity.address.to_string(),
275 minted.data,
276 session_reset,
277 );
278 let body = serde_json::to_string(&payload)
279 .map_err(|e| LeafError::MalformedFrame(format!("cannot encode key package: {e}")))?;
280
281 let mut message = frames::build(
282 &self.store,
283 &self.identity,
284 &self.app_id,
285 peer,
286 format!("{}{}", prefixes::KEY_PACKAGE, body),
287 now_unix_secs,
288 MessagePriority::High,
289 )?;
290 frames::sign_control_frame(&self.identity, &mut message)?;
291
292 // Recorded before the frame is handed back, so a device that emits one
293 // and loses power does not emit a second on the next boot and leave
294 // the peer holding two init keys of which only one is ever spent.
295 //
296 // The reference is recorded here for a second reason: it is the only
297 // thing that later distinguishes this peer's Welcome from one built by
298 // whoever else heard this frame. Minting has already written the
299 // package itself, so a cut between the two leaves a package no Welcome
300 // can name, which the adapter evicts in its turn. The opposite order
301 // would leave a name pointing at nothing.
302 let mut record = self.peer_record(peer)?;
303 record.key_package_sent = true;
304 record.key_package_ref = Some(minted.reference);
305 record
306 .save(&self.store, peer)
307 .map_err(|e| LeafError::Storage(e.to_string()))?;
308
309 // Handing out a package is the moment a pairing with this peer becomes
310 // possible, so it is the moment the peer becomes something firmware
311 // should be able to see in [`LeafDevice::peers`]. Inbound exchanges
312 // reach here having already been admitted, where this is a no-op.
313 self.index_peer(peer)?;
314
315 Ok(message)
316 }
317
318 /// Seals `plaintext` to `peer`.
319 ///
320 /// The ratchet advances, the new state is persisted, and only then does
321 /// the frame exist. A store that fails produces an error and no frame,
322 /// which is the whole point: a device that emitted first and persisted
323 /// second would, after a power cut, come back and reuse an AEAD nonce.
324 pub fn seal(&mut self, peer: &str, plaintext: &str, now_unix_secs: u64) -> Result<Message> {
325 self.seal_content(peer, plaintext.as_bytes(), now_unix_secs)
326 }
327
328 fn seal_content(
329 &mut self,
330 peer: &str,
331 plaintext: &[u8],
332 now_unix_secs: u64,
333 ) -> Result<Message> {
334 let client = self.client()?;
335 let group_id = self.group_id(peer)?;
336 let mut group = self.load_group(&client, peer, &group_id)?;
337
338 // Nothing is sealed into a group that stopped being this pair. The
339 // roster is the only thing that says it did, and the commit that
340 // changed it is applied and durable by the time anything can read it,
341 // so the question has to be asked at the use and not only at the
342 // change: a refusal returned once is a value firmware may drop and a
343 // reboot certainly does, and the device would then seal every later
344 // message into a room with a member the owner never chose.
345 self.require_still_a_pair(&group, peer)?;
346
347 let sealed = group
348 .encrypt_application_message(plaintext, Vec::new())
349 .map_err(|e| LeafError::Mls(format!("cannot seal: {e:?}")))?
350 .to_bytes()
351 .map_err(|e| LeafError::Mls(format!("cannot encode sealed message: {e:?}")))?;
352 let epoch = group.current_epoch();
353
354 // Persist before emit. Everything below this line only shapes bytes
355 // that are already accounted for on flash.
356 group
357 .write_to_storage()
358 .map_err(|e| LeafError::Storage(format!("cannot persist group state: {e:?}")))?;
359
360 let envelope = EncryptedMessage {
361 group_id,
362 message_type: MlsMessageType::Application,
363 epoch,
364 ciphertext: sealed,
365 sender_id: self.identity.address.to_string(),
366 timestamp_ms: (now_unix_secs.saturating_mul(1000)),
367 };
368
369 let body = self.encode_envelope(peer, &envelope)?;
370 frames::build(
371 &self.store,
372 &self.identity,
373 &self.app_id,
374 peer,
375 format!("{}{}", prefixes::ENCRYPTED, body),
376 now_unix_secs,
377 MessagePriority::Medium,
378 )
379 }
380
381 /// Chooses the envelope encoding for this peer.
382 ///
383 /// Compact only when the peer advertised it. Otherwise the JSON floor,
384 /// which every conforming receiver parses unconditionally and which no
385 /// negotiation ever removes.
386 fn encode_envelope(&self, peer: &str, envelope: &EncryptedMessage) -> Result<String> {
387 let record = self.peer_record(peer)?;
388 if record.env_versions.contains(&MLS_ENVELOPE_COMPACT_V1) {
389 Ok(BASE64.encode(envelope.to_bytes()))
390 } else {
391 serde_json::to_string(envelope)
392 .map_err(|e| LeafError::MalformedFrame(format!("cannot encode envelope: {e}")))
393 }
394 }
395
396 /// Handles one inbound frame.
397 ///
398 /// Returns the frames to send and what happened. Everything in
399 /// [`Handled::outbound`] is already durable by the time it is returned.
400 ///
401 /// # A frame addressed elsewhere is answered by nothing
402 ///
403 /// A radio hears what it is not the recipient of, so this is the first
404 /// question asked, before a signature is verified or a prefix is read.
405 /// Nothing further down asks it again, and neither kind of frame answers
406 /// it on its own: a control frame's signature **covers** the recipient
407 /// rather than checking it, so one honestly signed for somebody else
408 /// verifies perfectly here, and a sealed frame carries no signature at
409 /// all, so its recipient is whatever the last hand to touch it wrote
410 /// there. Being able to open a frame is a different claim from having been
411 /// sent it.
412 ///
413 /// Three things follow from not asking. An overheard key package admits a
414 /// peer, spends flash on a record, mints a private init key nobody asked
415 /// this device for, and answers a phone that never addressed it. A sealed
416 /// frame this device really can open is acted on after anyone who captured
417 /// it rewrote the recipient, because that field is not inside the AEAD.
418 /// And every other prefix arrives as an identity-binding failure, so
419 /// ordinary traffic between two neighbours reaches firmware wearing the
420 /// shape of an attack, on a device whose only account of itself is that
421 /// error stream.
422 ///
423 /// What the check does not do is keep anyone else's ciphertext readable
424 /// here: the group and credential gates below hold either way.
425 ///
426 /// It is [`LeafEvent::Ignored`] rather than an error because overhearing is
427 /// what a shared radio does, and because firmware that carries frames for
428 /// its neighbours needs "not mine" to be a fact it can act on rather than a
429 /// failure it has to interpret.
430 pub fn handle(&mut self, message: &Message, now_unix_secs: u64) -> Result<Handled> {
431 if !self.is_addressed_to_me(message) {
432 return Ok(ignored("frame is addressed to another node"));
433 }
434
435 // Before the frame is opened, because opening it a second time is what
436 // fails: the ratchet has spent that generation and refuses it, and the
437 // peer is only asking again because the first answer went missing.
438 if let Some(repeat) = self.repeat_acknowledgement(message, now_unix_secs)? {
439 return Ok(repeat);
440 }
441
442 let (mut handled, sender) = self.dispatch(message, now_unix_secs)?;
443
444 // Only a frame that proved who sent it is answered, which is narrower
445 // than "was not refused". An `Err` above never reaches here, and that
446 // much is the phone's own rule and the reason for it: an
447 // acknowledgement is a receipt, and handing one to whoever just failed
448 // the signature gate tells them their frames are being processed. But
449 // a frame carrying no prefix fails nothing, so it needs the other half
450 // of the question: see [`Sender`].
451 if sender == Sender::Proven {
452 match self.acknowledge(message, now_unix_secs) {
453 Ok(Some(ack)) => handled.outbound.push(ack),
454 Ok(None) => {}
455 // Never propagated, because by here the frame is open and what
456 // it produced is already owed to firmware: the ratchet has
457 // spent that generation, so returning the error would discard
458 // an unlock this device really did receive, and the
459 // retransmission that followed would be refused as a replay
460 // with nothing remembered to answer it from. The answer is
461 // dropped instead, which costs the retry ladder that ran
462 // before any of this existed and is recovered by it.
463 Err(e) => handled.events.push(LeafEvent::Ignored {
464 reason: format!("the answer this frame is owed could not be stored: {e}"),
465 }),
466 }
467 }
468
469 Ok(handled)
470 }
471
472 /// Routes a frame to the handler for its prefix, and says whether anything
473 /// on that route proved the sender the frame names.
474 ///
475 /// Every handler below either verifies a control signature as its first
476 /// act or opens the frame under the pair's group key, on every path that
477 /// returns normally. That is what makes their success [`Sender::Proven`],
478 /// and a handler that stopped doing it would have to say so here.
479 fn dispatch(&mut self, message: &Message, now_unix_secs: u64) -> Result<(Handled, Sender)> {
480 let content = &message.content;
481
482 // Order matters: the encrypted-confirm prefix is not checked here at
483 // all, because it never travels as a frame. It is only ever found
484 // inside a decrypted plaintext, which is where this looks for it.
485 if let Some(body) = frames::strip_prefix(content, prefixes::KEY_PACKAGE) {
486 Ok((
487 self.on_key_package(message, body, now_unix_secs)?,
488 Sender::Proven,
489 ))
490 } else if let Some(body) = frames::strip_prefix(content, prefixes::WELCOME) {
491 Ok((
492 self.on_welcome(message, body, now_unix_secs)?,
493 Sender::Proven,
494 ))
495 } else if let Some(body) = frames::strip_prefix(content, prefixes::ENCRYPTED) {
496 Ok((
497 self.on_encrypted(message, body, now_unix_secs)?,
498 Sender::Proven,
499 ))
500 } else if frames::strip_prefix(content, prefixes::SESSION_CONFIRM_PROBE).is_some() {
501 Ok((self.on_probe(message, now_unix_secs)?, Sender::Proven))
502 } else if frames::strip_prefix(content, prefixes::SESSION_CONFIRM_ACK).is_some() {
503 // Never acted on, and not a frame this device accepts at all. A
504 // leaf emits an acknowledgement and never a probe, so it has none
505 // outstanding and every inbound one is unsolicited.
506 //
507 // Reading one as proof of a session is the bypass: producing a
508 // frame that derives to its own address costs an attacker nothing,
509 // so acting on it would let anyone holding a keypair tell firmware
510 // a session exists that this device would refuse to seal into. The
511 // phone gates the same frame on holding a session of its own; the
512 // profile in the spec lists this prefix under what a leaf emits and
513 // not under what it accepts.
514 Ok((
515 ignored("an acknowledgement arrived for a probe this device never sends"),
516 Sender::Claimed,
517 ))
518 } else {
519 Ok((
520 ignored("frame carries no prefix this device answers"),
521 Sender::Claimed,
522 ))
523 }
524 }
525
526 fn on_key_package(
527 &mut self,
528 message: &Message,
529 body: &str,
530 now_unix_secs: u64,
531 ) -> Result<Handled> {
532 // The one frame class that admits the older payload, because it is the
533 // frame that *teaches* a peer which payload this device accepts: a
534 // sender that has never held this device's key package signs the older
535 // one, whatever release it runs, and refusing it makes a
536 // phone-initiated pairing impossible rather than making the device
537 // safer. See [`frames`] for the full reasoning and for what it costs.
538 let signed_under =
539 frames::verify_control_frame(message, now_unix_secs, frames::UndatedPayload::Admitted)?;
540 let payload: KeyPackagePayload = serde_json::from_str(body)
541 .map_err(|e| LeafError::MalformedFrame(format!("key package body: {e}")))?;
542
543 let sender = message.sender.as_str();
544
545 // The body names its own owner, and the frame names its sender. A
546 // package that claims to belong to someone other than the peer that
547 // signed the frame is one being relayed under a borrowed name, so the
548 // two must agree before anything is stored under either.
549 if payload.user_id != sender {
550 return Err(LeafError::IdentityBinding(format!(
551 "key package claims to be '{}' but the frame is signed by '{}'",
552 payload.user_id, sender
553 )));
554 }
555
556 // Bounded before the first byte is written under this peer's name.
557 // Everything below here stores something, and a signature that only
558 // proves a key derives to its own address is not a scarce thing to
559 // produce.
560 self.admit_peer(sender)?;
561
562 let mut events = Vec::new();
563 let mut record = self.peer_record(sender)?;
564
565 // A reset carried under the older payload is refused its teardown and
566 // nothing else. That payload leaves the timestamp outside the
567 // signature, so the high-water mark below would be comparing a number
568 // an attacker rewrote at will: parked in the future once, it would
569 // deny this pair every future reset, which is a worse failure than the
570 // replay it would be defending against. The rest of the frame is still
571 // acted on, because capability advertisement is unauthenticated hint
572 // data everywhere in this protocol, and the peer re-sends a reset
573 // under the newer payload as soon as this device's key package has
574 // taught it to.
575 if payload.session_reset && signed_under != frames::ControlPayload::Fresh {
576 events.push(LeafEvent::Ignored {
577 reason: String::from(
578 "a session reset arrived on a frame that states no time, so its age \
579 cannot be judged and its teardown is refused",
580 ),
581 });
582 } else if payload.session_reset {
583 // The frame's own stamp, which `verify_control_frame` has already
584 // proved is the sender's and inside the window this device
585 // accepts. Everything below rests on both of those: an unproved
586 // stamp would let anyone park the mark past every future reset.
587 let stamp = message.timestamp.as_millis();
588 if !record.reset_is_unspent(stamp) {
589 // A reset already acted on, or older than one that was.
590 // Tearing down again is how a captured frame becomes a
591 // repeatable way to break a session that has since been
592 // rebuilt, so the teardown is what a replay loses; the record
593 // below is still refreshed, because a peer restating its
594 // capabilities is harmless and a retransmission is the
595 // ordinary reason to see this twice.
596 events.push(LeafEvent::Ignored {
597 reason: String::from("a session reset arrived that this device has spent"),
598 });
599 } else {
600 // Marked before the teardown, never after. The teardown is
601 // followed by a fresh pairing, so a power cut in between would
602 // leave this frame able to break the replacement too, and a
603 // power cut is the one event this crate assumes will happen.
604 record.remember_reset(stamp);
605 record.key_package_sent = false;
606 // Written here *and* again at the end of this function, which
607 // is deliberate rather than redundant: the second write
608 // carries the peer's refreshed capabilities and cannot be
609 // moved earlier, and this one cannot be moved later without
610 // putting a power cut between the mark and the teardown it
611 // authorizes. Two writes on a reset frame, which arrives about
612 // as often as a rekey; the ordinary frame still writes once.
613 record
614 .save(&self.store, sender)
615 .map_err(|e| LeafError::Storage(e.to_string()))?;
616 self.forget_session(sender)?;
617 events.push(LeafEvent::SessionReset {
618 peer: sender.to_string(),
619 });
620 }
621 }
622
623 record.env_versions = payload.env_versions.clone();
624 record.wire_versions = payload.wire_versions.clone();
625 record
626 .save(&self.store, sender)
627 .map_err(|e| LeafError::Storage(e.to_string()))?;
628 events.push(LeafEvent::PeerAdvertised {
629 peer: sender.to_string(),
630 });
631
632 // Answer with our own package only if this peer has not already had
633 // one. Without that guard two peers that both answer trade key
634 // packages forever, each one spending an init key.
635 let outbound = if record.key_package_sent {
636 Vec::new()
637 } else {
638 vec![self.key_package_frame_inner(sender, now_unix_secs, false)?]
639 };
640
641 Ok(Handled { outbound, events })
642 }
643
644 fn on_welcome(&mut self, message: &Message, body: &str, now_unix_secs: u64) -> Result<Handled> {
645 // The older payload is refused here and needs no accommodation: a
646 // Welcome can only follow this device's own key package reaching the
647 // peer, and that is the frame which teaches the peer to sign the
648 // freshness-bound one.
649 frames::verify_control_frame(message, now_unix_secs, frames::UndatedPayload::Refused)?;
650 let welcome: WelcomeMessage = serde_json::from_str(body)
651 .map_err(|e| LeafError::MalformedFrame(format!("welcome body: {e}")))?;
652
653 let sender = message.sender.as_str();
654
655 // The inviter named inside the body must be the peer that signed the
656 // frame, and the group must be the one this pair would build. Without
657 // the second check a peer could hand over a Welcome for a group it
658 // built with somebody else, and the device would join it and seal into
659 // a room whose membership it never checked.
660 if welcome.inviter_id != sender {
661 return Err(LeafError::IdentityBinding(format!(
662 "welcome names inviter '{}' but the frame is signed by '{}'",
663 welcome.inviter_id, sender
664 )));
665 }
666 let expected = self.group_id(sender)?;
667 if welcome.group_id != expected {
668 return Err(LeafError::IdentityBinding(format!(
669 "welcome is for group '{}', not this pair's '{}'",
670 welcome.group_id, expected
671 )));
672 }
673
674 let client = self.client()?;
675 let welcome_message = MlsMessage::from_bytes(&welcome.welcome_data)
676 .map_err(|e| LeafError::Mls(format!("welcome does not decode: {e:?}")))?;
677
678 // And the Welcome must spend the key package this device minted **for
679 // this peer**. The two checks above are about the peer and the group;
680 // this one is about the material, and nothing else in the exchange
681 // covers it, because a key package rides in a frame that is signed but
682 // not encrypted and is therefore spendable by whoever copied it off the
683 // air.
684 //
685 // Checked before the join, which is the whole point: joining spends the
686 // init key, and the failure being prevented is not a stranger reading
687 // anything (the group and credential gates hold either way) but this
688 // device's package being **burned by somebody it was not meant for**,
689 // which leaves the intended peer holding a Welcome that can no longer
690 // be joined and a pairing that only a driven reset recovers.
691 self.require_own_key_package(sender, &welcome_message)?;
692
693 // `tree_data: None` because the peer puts the ratchet tree in the
694 // Welcome. A device that needed it out of band would need a side
695 // channel it does not have.
696 let (mut group, _info) = client
697 .join_group(None, &welcome_message, None)
698 .map_err(|e| LeafError::Mls(format!("cannot join from the welcome: {e:?}")))?;
699
700 // The group checked above was the one the body *claimed*. This is the
701 // one that was actually joined, and they are separate values: the
702 // claim is a JSON field beside the Welcome, and the Welcome carries
703 // its own group id inside. A frame that puts an honest claim in front
704 // of somebody else's Welcome passes the first check and fails here.
705 //
706 // Refused before the state is written, because joining has already
707 // spent this device's init key, and persisting the group as well would
708 // leave a room it never chose sitting on flash for every later gate to
709 // keep refusing.
710 if group.group_id() != expected.as_str().as_bytes() {
711 return Err(LeafError::IdentityBinding(format!(
712 "welcome claimed group '{expected}' but joined a different one"
713 )));
714 }
715
716 group
717 .write_to_storage()
718 .map_err(|e| LeafError::Storage(format!("cannot persist group state: {e:?}")))?;
719
720 // A session exists from here, so the peer has to be in the index
721 // whatever route it took to get one: a session firmware cannot see in
722 // [`LeafDevice::peers`] is one it can neither audit nor
723 // [`LeafDevice::unpair`].
724 //
725 // Ordinarily it is already there, put there by the same mint that
726 // recorded the reference this Welcome just spent, and an eviction
727 // since then would have taken that reference with it and been refused
728 // above. What is left is the one order that separates them: the mint
729 // saves the peer record and indexes second, so an index write that
730 // failed leaves a peer holding a live reference and no slot. That peer
731 // arrives here.
732 self.index_peer(sender)?;
733
734 // The confirmation is a group-aware decrypt, sealed inside an ordinary
735 // envelope. A peer that created a session of its own confirms only on
736 // a successful decrypt, so a plaintext acknowledgement would leave it
737 // unconfirmed however many times it was sent.
738 let confirm = self.seal_content(
739 sender,
740 prefixes::SESSION_CONFIRM_ENCRYPTED.as_bytes(),
741 now_unix_secs,
742 )?;
743
744 Ok(Handled {
745 outbound: vec![confirm],
746 events: vec![LeafEvent::SessionEstablished {
747 peer: sender.to_string(),
748 }],
749 })
750 }
751
752 fn on_encrypted(&mut self, message: &Message, body: &str, _now: u64) -> Result<Handled> {
753 // Not signature-gated: this is the data plane, and MLS authenticates
754 // its own sender. A second signature on the outside would state what
755 // the AEAD already proves on the inside.
756 let envelope = parse_envelope(body)?;
757 let sender = message.sender.as_str();
758
759 // The envelope names a sender too, and it is inside nothing: it rides
760 // in the clear beside the ciphertext. Binding it to the wire sender
761 // before the group is loaded keeps a relayed frame from being
762 // attributed to whoever forwarded it.
763 if envelope.sender_id != sender {
764 return Err(LeafError::IdentityBinding(format!(
765 "envelope claims sender '{}' but the frame came from '{}'",
766 envelope.sender_id, sender
767 )));
768 }
769
770 let client = self.client()?;
771 let group_id = self.group_id(sender)?;
772 if envelope.group_id != group_id {
773 return Err(LeafError::IdentityBinding(format!(
774 "envelope is for group '{}', not this pair's '{}'",
775 envelope.group_id, group_id
776 )));
777 }
778
779 let mut group = self.load_group(&client, sender, &group_id)?;
780
781 // Asked before the frame is opened, for the reason a seal asks it
782 // before sealing: a group that stopped being this pair is one this
783 // device holds no conversation with in either direction. The commit
784 // that widens a roster is caught below instead, after it applies,
785 // because that is the only moment it can be seen at all; this is what
786 // keeps every frame after that one from arriving as ordinary traffic.
787 self.require_still_a_pair(&group, sender)?;
788
789 let inbound = MlsMessage::from_bytes(&envelope.ciphertext)
790 .map_err(|e| LeafError::Mls(format!("sealed payload does not decode: {e:?}")))?;
791
792 let received = group
793 .process_incoming_message(inbound)
794 .map_err(|e| LeafError::Mls(format!("cannot open: {e:?}")))?;
795
796 let epoch = group.current_epoch();
797 group
798 .write_to_storage()
799 .map_err(|e| LeafError::Storage(format!("cannot persist group state: {e:?}")))?;
800
801 let events = match received {
802 ReceivedMessage::ApplicationMessage(app) => {
803 // The leaf identity binding, applied here as it is on the
804 // phone: the group member that actually sealed this must be
805 // the peer the frame claims to come from. MLS proves a member
806 // sealed it; only re-deriving the address from that member's
807 // signature key proves *which* member, and in a group this
808 // device did not build, membership is not something it chose.
809 self.bind_sender_credential(&group, app.sender_index, sender)?;
810
811 let text = String::from_utf8_lossy(app.data()).to_string();
812 // Consumed and never surfaced: it exists to be a decrypt, not
813 // to be read.
814 if text == prefixes::SESSION_CONFIRM_ENCRYPTED {
815 vec![LeafEvent::SessionEstablished {
816 peer: sender.to_string(),
817 }]
818 } else {
819 vec![LeafEvent::MessageReceived {
820 peer: sender.to_string(),
821 text,
822 }]
823 }
824 }
825 ReceivedMessage::Commit(commit) => {
826 // Two questions a commit has to answer, ordered so the
827 // first failure is the informative one: is this still the
828 // pair this device joined, and did the peer whose frame
829 // carried this actually author it. The roster answers the
830 // first. Only the committer's own credential answers the
831 // second, because the frame states who sent it and a peer is
832 // free to relay a third member's work under its own name,
833 // which without this reaches firmware wearing the peer's
834 // address.
835 //
836 // Reported rather than rolled back. A member cannot skip one
837 // commit and keep decrypting the next, so by the time there
838 // is anything to read the commit is applied and durable. What
839 // these two buy is that firmware hears a group stopped being
840 // the pair it agreed to, rather than nothing at all.
841 self.require_still_a_pair(&group, sender)?;
842 self.bind_sender_credential(&group, commit.committer, sender)?;
843 vec![LeafEvent::CommitApplied {
844 peer: sender.to_string(),
845 epoch,
846 }]
847 }
848 _ => vec![LeafEvent::Ignored {
849 reason: String::from("sealed frame carried nothing this device acts on"),
850 }],
851 };
852
853 Ok(Handled {
854 outbound: Vec::new(),
855 events,
856 })
857 }
858
859 /// Answers a liveness probe, but only for a peer this device can still
860 /// talk to.
861 ///
862 /// The acknowledgement is not a pleasantry: a peer treats it as proof the
863 /// session is usable and confirms on it, then flushes everything it had
864 /// queued into that session. A device that answered after losing its store
865 /// would confirm a session it cannot decrypt a single frame of, and the
866 /// peer would have no way to find that out, because the device's silence
867 /// afterwards is indistinguishable from a quiet link. Staying quiet here
868 /// leaves the peer unconfirmed, which is a state it knows how to repair.
869 ///
870 /// This is the peer's own rule, applied on the device: it answers a probe
871 /// only while it holds a session of its own.
872 fn on_probe(&mut self, message: &Message, now_unix_secs: u64) -> Result<Handled> {
873 // The older payload is refused here too, and for the same reason it is
874 // refused on a Welcome: a probe only ever follows an established
875 // session, which this device's own key package is what starts.
876 frames::verify_control_frame(message, now_unix_secs, frames::UndatedPayload::Refused)?;
877 let sender = message.sender.as_str();
878
879 // Loaded rather than counted. "State is on flash" and "this device can
880 // decrypt" are different claims, and an acknowledgement asserts the
881 // second: a device that answered on the strength of bytes it cannot
882 // load would confirm a session it cannot open one frame of, which is
883 // the failure staying quiet exists to avoid. A state that is present
884 // and unloadable is also not silence, it is a store handing back what
885 // this device did not write, and it propagates for the reason
886 // [`LeafDevice::load_group`] separates the two.
887 let client = self.client()?;
888 let group_id = self.group_id(sender)?;
889 let group = match self.load_group(&client, sender, &group_id) {
890 Ok(group) => group,
891 Err(LeafError::NoSession(_)) => {
892 return Ok(ignored(
893 "a confirmation probe arrived for a peer this device has no session with",
894 ))
895 }
896 Err(e) => return Err(e),
897 };
898
899 // And it must still be this pair. An acknowledgement is a promise to
900 // use the session rather than a report that bytes exist, and a device
901 // that will refuse to seal into this group has no such promise to
902 // make: the peer would confirm, flush everything it had queued, and
903 // hear nothing further. Refused rather than ignored, because unlike an
904 // absent session this is a state firmware needs to hear about.
905 self.require_still_a_pair(&group, sender)?;
906
907 let mut ack = frames::build(
908 &self.store,
909 &self.identity,
910 &self.app_id,
911 sender,
912 String::from(prefixes::SESSION_CONFIRM_ACK),
913 now_unix_secs,
914 MessagePriority::High,
915 )?;
916 frames::sign_control_frame(&self.identity, &mut ack)?;
917 Ok(Handled {
918 outbound: vec![ack],
919 events: Vec::new(),
920 })
921 }
922
923 /// Discards everything belonging to a session with `peer`.
924 ///
925 /// Called when a peer says it has discarded its own. A device that kept
926 /// the old session would hold one the peer has already thrown away, and
927 /// every later frame from it would decrypt to nothing.
928 ///
929 /// # The prior epochs go too
930 ///
931 /// A session is the group state, the marker, **and** the prior-epoch
932 /// records, and the last of those is the part that is easy to leave
933 /// behind. Each holds an epoch's secrets, so records that outlive the
934 /// session they belong to are key material surviving an erasure the owner
935 /// asked for. They also outlive it under a name the next session answers
936 /// to: a pair's group id is derived from the two addresses, so a device
937 /// that re-pairs with the same peer rebuilds the same id and starts
938 /// writing epochs beside the last session's.
939 fn forget_session(&mut self, peer: &str) -> Result<()> {
940 let group_id = self.group_id(peer)?;
941 let key = crate::adapters::hex(group_id.as_str().as_bytes());
942
943 // The anchor is the middle of the sweep rather than its top. Trimming
944 // keeps only the newest few below it and the slack covers a delete
945 // that failed while it was doing so, but a record can also sit
946 // **above** every anchor: the adapter writes epoch records before the
947 // state entry that names them, so a cut between the two leaves an
948 // epoch on flash that the marker, the high-water record and the
949 // group's own epoch all agree is not there. Sweeping only downward
950 // would delete every record but that one and report success, which is
951 // the same erasure-that-did-not-happen the three anchors below exist
952 // to prevent. A delete of a key that is not there is not an error,
953 // which is what makes a fixed window the right shape rather than an
954 // enumeration this seam cannot offer.
955 //
956 // Three sources, tried in order, because an anchor that is missing or
957 // unreadable cannot bound anything and anchoring at zero would delete
958 // one record, return `Ok`, and leave every other epoch's secrets on
959 // flash: an erasure the owner asked for that reports success and did
960 // not happen. The marker inside the state entry is first because it is
961 // the one written atomically with the state. The separate high-water
962 // record is second and covers the case that one cannot: a state entry
963 // the part hands back as something else. The group's own epoch is
964 // last, and names the same neighbourhood.
965 let anchor = match crate::adapters::state_marker(&self.store, &key) {
966 Some(highest) => Some(highest),
967 None => self.max_epoch(&key)?,
968 };
969 let highest = anchor
970 .or_else(|| self.current_epoch(&group_id))
971 .unwrap_or(0);
972 let floor = highest.saturating_sub(PRIOR_EPOCH_RETENTION + FORGET_EPOCH_SLACK);
973 let ceiling = highest.saturating_add(FORGET_EPOCH_SLACK);
974 for epoch in floor..=ceiling {
975 self.store
976 .delete(KEY_TYPE_GROUP_EPOCH, &format!("{key}:{epoch}"))
977 .map_err(|e| LeafError::Storage(e.to_string()))?;
978 }
979
980 self.store
981 .delete(KEY_TYPE_GROUP_STATE, &key)
982 .map_err(|e| LeafError::Storage(e.to_string()))?;
983 self.store
984 .delete(KEY_TYPE_GROUP_EPOCH, &format!("{key}:max"))
985 .map_err(|e| LeafError::Storage(e.to_string()))?;
986 Ok(())
987 }
988
989 /// The highest epoch id ever written for a group, by storage key.
990 ///
991 /// `None` covers both a marker that was never written and one that does
992 /// not decode. The two are the same answer here, which is safe only
993 /// because the caller treats `None` as "no bound available" and finds
994 /// another anchor, rather than as "no epochs".
995 fn max_epoch(&self, group_key: &str) -> Result<Option<u64>> {
996 let raw = self
997 .store
998 .load(KEY_TYPE_GROUP_EPOCH, &format!("{group_key}:max"))
999 .map_err(|e| LeafError::Storage(e.to_string()))?;
1000 Ok(raw
1001 .and_then(|bytes| <[u8; 8]>::try_from(bytes.as_slice()).ok())
1002 .map(u64::from_be_bytes))
1003 }
1004
1005 /// Forgets a peer's session, its key package and what it advertised,
1006 /// leaving the index.
1007 fn forget_peer(&mut self, peer: &str) -> Result<()> {
1008 // Read before the record holding it is deleted, because that reference
1009 // is the only pointer this device has to the key package minted for
1010 // this peer, and the package is private key material. An init key is
1011 // single use, so one nobody spends is never consumed, and the only
1012 // other thing that reclaims it is four later mints pushing it out of
1013 // the ring. On a device that pairs twice a year that is years of
1014 // holding an init key for a peer the owner asked it to forget.
1015 //
1016 // Best effort, though. A record that will not decode has no reference
1017 // to give, and refusing the whole call for that would leave the owner
1018 // unable to forget a peer at all: the one escape hatch a device has,
1019 // closed by exactly the corruption it exists to recover from. An
1020 // unreclaimed package is the smaller residue, and it is the same
1021 // reading [`LeafDevice::peer_index`] gives an index that does not
1022 // parse.
1023 let minted = self
1024 .peer_record(peer)
1025 .ok()
1026 .and_then(|record| record.key_package_ref);
1027
1028 self.forget_session(peer)?;
1029
1030 // Erased before the record stops naming it, which is the ordering the
1031 // adapter's own eviction uses and for the same reason: a failed erase
1032 // leaves a pointer a retry can still follow, where the other order
1033 // leaves key material that nothing names and nothing reclaims.
1034 if let Some(reference) = minted {
1035 KeyPackageAdapter::new(Arc::clone(&self.store))
1036 .erase(&reference)
1037 .map_err(|e| LeafError::Storage(e.to_string()))?;
1038 }
1039
1040 self.store
1041 .delete(KEY_TYPE_PEER, peer)
1042 .map_err(|e| LeafError::Storage(e.to_string()))?;
1043 Ok(())
1044 }
1045
1046 /// The peers this device holds records for.
1047 ///
1048 /// An index that does not decode is read as empty rather than as an error.
1049 /// It is a bound on storage, not a security claim, and a device that
1050 /// refused to pair because a list of previous peers is unreadable would
1051 /// have turned a recoverable annoyance into a brick.
1052 fn peer_index(&self) -> Result<Vec<String>> {
1053 let Some(raw) = self
1054 .store
1055 .load(KEY_TYPE_PEER_INDEX, "peers")
1056 .map_err(|e| LeafError::Storage(e.to_string()))?
1057 else {
1058 return Ok(Vec::new());
1059 };
1060 Ok(serde_json::from_slice(&raw).unwrap_or_default())
1061 }
1062
1063 fn save_peer_index(&self, index: &[String]) -> Result<()> {
1064 let encoded = serde_json::to_vec(index)
1065 .map_err(|e| LeafError::MalformedFrame(format!("cannot encode peer index: {e}")))?;
1066 self.store
1067 .store(KEY_TYPE_PEER_INDEX, "peers", &encoded)
1068 .map_err(|e| LeafError::Storage(e.to_string()))
1069 }
1070
1071 /// Records `peer` in the index without holding it to [`MAX_PEERS`].
1072 ///
1073 /// The bound exists to stop a stranger spending a device's flash, and
1074 /// neither caller here is a stranger: one is firmware choosing to pair,
1075 /// the other is a Welcome, and a Welcome is only reached by a peer whose
1076 /// recorded key package reference it spends. That reference is written
1077 /// only by a mint, and a mint is reached only through firmware or through
1078 /// [`LeafDevice::admit_peer`], which is where the bound is applied. So
1079 /// every peer arriving here has already been counted or chosen.
1080 ///
1081 /// That is a claim about [`LeafDevice::require_own_key_package`] and would
1082 /// be false without it: a key package travels unencrypted, so before that
1083 /// gate any listener could copy one, build this pair's group around it and
1084 /// arrive here having been counted by nothing.
1085 ///
1086 /// It is the index rather than the bound that has to be complete. A peer
1087 /// missing from it holds a session nothing can audit and
1088 /// [`LeafDevice::unpair`] cannot be pointed at, and a session is exactly
1089 /// what the authorization obligation asks firmware to review, since none
1090 /// of the gates in this crate answers whether the owner meant this peer.
1091 fn index_peer(&mut self, peer: &str) -> Result<()> {
1092 let mut index = self.peer_index()?;
1093 if index.iter().any(|held| held == peer) {
1094 return Ok(());
1095 }
1096 index.push(peer.to_string());
1097 self.save_peer_index(&index)
1098 }
1099
1100 /// Makes room for `peer` in the bounded set, or refuses.
1101 ///
1102 /// A peer already held is admitted for free. A new one at capacity takes
1103 /// the place of a pairing that never completed, which is what a record
1104 /// with no session is; if every slot holds a real session, the answer is
1105 /// [`LeafError::TooManyPeers`] rather than the eviction of somebody the
1106 /// owner actually paired with. That direction is the whole point of having
1107 /// the rule: a stranger who can provoke a record must not be able to
1108 /// provoke the removal of one.
1109 fn admit_peer(&mut self, peer: &str) -> Result<()> {
1110 let mut index = self.peer_index()?;
1111 if index.iter().any(|held| held == peer) {
1112 return Ok(());
1113 }
1114
1115 if index.len() >= MAX_PEERS {
1116 // A store that fails the session check counts as holding one, so a
1117 // read error protects the peer rather than evicting it.
1118 let victim = index
1119 .iter()
1120 .find(|held| !self.has_session(held).unwrap_or(true))
1121 .cloned();
1122 let Some(victim) = victim else {
1123 return Err(LeafError::TooManyPeers);
1124 };
1125 self.forget_peer(&victim)?;
1126 index.retain(|held| held != &victim);
1127 }
1128
1129 index.push(peer.to_string());
1130 self.save_peer_index(&index)
1131 }
1132
1133 /// Requires the group to still be the pair this device agreed to.
1134 ///
1135 /// The Welcome gate is what keeps this device out of a room it never
1136 /// chose, and it runs once. Nothing repeats it afterwards, and a commit is
1137 /// free to add a member without the group id changing, so a device that
1138 /// checked only at the join would follow its peer into a room one member
1139 /// at a time and never see it happen. The never-committing profile makes
1140 /// that entirely the peer's decision, which is exactly why it is worth
1141 /// checking rather than trusting.
1142 ///
1143 /// So the roster is re-read on **every use of the group**, and not only on
1144 /// the commit that can change it: two members, and both of them
1145 /// **derived** rather than read off a credential, since a basic credential
1146 /// is a bare assertion.
1147 ///
1148 /// Checking only at the commit would be checking at the one moment whose
1149 /// answer cannot be kept. A commit is applied and durable by the time
1150 /// there is a roster to read, so the refusal is a returned error and
1151 /// nothing else: it survives no reboot, and a device that came back up
1152 /// would seal its next message into the widened room and call it an
1153 /// ordinary session. Reading the roster where the group is used puts the
1154 /// answer somewhere it cannot be lost, which is the same reason nothing
1155 /// else in this crate caches MLS state.
1156 ///
1157 /// It costs a roster read and two SHA-256 derivations per frame. Both are
1158 /// symmetric work, so the profile's promise that per-message cost is
1159 /// symmetric only still holds.
1160 ///
1161 /// Reported rather than rolled back. A member cannot skip one commit and
1162 /// keep decrypting the next, so the choice is not whether to follow the
1163 /// peer but whether firmware is told, and what it does about it is
1164 /// [`LeafDevice::unpair`] or the peer's own reset.
1165 fn require_still_a_pair(&self, group: &Group<impl MlsConfig>, peer: &str) -> Result<()> {
1166 let roster = group.roster();
1167 let members = roster.members();
1168 if members.len() != 2 {
1169 return Err(LeafError::IdentityBinding(format!(
1170 "a commit left this group holding {} members, and a leaf node's group is a pair",
1171 members.len()
1172 )));
1173 }
1174
1175 let mine = self.identity.address.to_string();
1176 for member in &members {
1177 let credential = member
1178 .signing_identity
1179 .credential
1180 .as_basic()
1181 .ok_or_else(|| {
1182 LeafError::IdentityBinding(String::from(
1183 "a group member presents no basic credential, so it names no address",
1184 ))
1185 })?;
1186 let address = crate::adapters::credential_address(credential)?;
1187 if address != mine && address != peer {
1188 return Err(LeafError::IdentityBinding(format!(
1189 "a commit left '{address}' in this group, which is neither this device nor '{peer}'"
1190 )));
1191 }
1192 frames::verify_sender_derivation(
1193 address,
1194 member.signing_identity.signature_key.as_bytes(),
1195 )?;
1196 }
1197 Ok(())
1198 }
1199
1200 /// Requires a Welcome to spend the key package minted for its sender.
1201 ///
1202 /// A key package is a bearer token: this device hands one to a peer in a
1203 /// frame addressed to that peer, and a shared radio carries it to everyone
1204 /// else as well. A listener that copies it can build a group whose id is
1205 /// the one this pair would build, sign the Welcome with its own key, and
1206 /// name itself as inviter, so the inviter check and the group check both
1207 /// pass honestly. What it cannot do is present the reference of a package
1208 /// this device minted for **it**.
1209 ///
1210 /// A peer with no recorded reference is refused for the same reason an
1211 /// unparseable identifier is elsewhere in this crate: there is nothing to
1212 /// compare, and "nothing to compare" is the bypass rather than a lenience.
1213 /// It also restores the bound: every peer reaching a session has been
1214 /// through [`LeafDevice::admit_peer`] or was chosen by firmware, which is
1215 /// what lets [`LeafDevice::index_peer`] skip [`MAX_PEERS`].
1216 fn require_own_key_package(&self, peer: &str, welcome: &MlsMessage) -> Result<()> {
1217 let Some(minted) = self.peer_record(peer)?.key_package_ref else {
1218 return Err(LeafError::UnsolicitedWelcome(format!(
1219 "no key package was ever minted for '{peer}', so nothing it sends can spend one"
1220 )));
1221 };
1222
1223 let spends_ours = welcome
1224 .welcome_key_package_references()
1225 .into_iter()
1226 .any(|reference| crate::adapters::hex(reference) == minted);
1227
1228 if !spends_ours {
1229 return Err(LeafError::UnsolicitedWelcome(format!(
1230 "welcome from '{peer}' spends a key package this device did not mint for it"
1231 )));
1232 }
1233
1234 // The reference matches, so this is the peer's own package. Whether it
1235 // is still on flash is a separate question, and it is asked here
1236 // because the answer decides which failure firmware is handed. An init
1237 // key is single use, so a package an earlier join consumed is gone,
1238 // and the ring bounding unspent packages evicts on the fourth later
1239 // mint. Left to the join, either arrives from inside MLS as a Welcome
1240 // that will not decode, which reads as a broken peer and sends a bench
1241 // to the wire when the repair is a fresh package.
1242 if self
1243 .store
1244 .load(KEY_TYPE_KEY_PACKAGE, &minted)
1245 .map_err(|e| LeafError::Storage(e.to_string()))?
1246 .is_none()
1247 {
1248 return Err(LeafError::StaleKeyPackage(format!(
1249 "the key package '{peer}' spends was minted for it and is no longer held"
1250 )));
1251 }
1252 Ok(())
1253 }
1254
1255 /// Requires the MLS member at `index` to derive to `claimed`.
1256 ///
1257 /// The refusal is deliberately the same for a member whose credential is
1258 /// not an address at all: a credential with no derivation to check is not
1259 /// one to wave through, it is the bypass. That is the rule
1260 /// [ADR 0010](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/adr/0010-unconditional-leaf-identity-binding.md)
1261 /// makes unconditional on the phone, and a device that skipped it would be
1262 /// the weaker end of a protocol whose whole claim is that both ends are
1263 /// the same.
1264 fn bind_sender_credential(
1265 &self,
1266 group: &Group<impl MlsConfig>,
1267 index: u32,
1268 claimed: &str,
1269 ) -> Result<()> {
1270 let member = group.member_at_index(index).ok_or_else(|| {
1271 LeafError::IdentityBinding(format!("no group member at index {index}"))
1272 })?;
1273
1274 let credential = member
1275 .signing_identity
1276 .credential
1277 .as_basic()
1278 .ok_or_else(|| {
1279 LeafError::IdentityBinding(String::from(
1280 "group member presents no basic credential, so it names no address",
1281 ))
1282 })?;
1283
1284 let credential_address = crate::adapters::credential_address(credential)?;
1285 if credential_address != claimed {
1286 return Err(LeafError::IdentityBinding(format!(
1287 "authored by group member '{credential_address}', but the frame claims '{claimed}'"
1288 )));
1289 }
1290
1291 // And the credential's claim is itself checked rather than trusted: a
1292 // basic credential is self-asserted, so the address in it means
1293 // something only because it is the hash of the key beside it.
1294 frames::verify_sender_derivation(
1295 credential_address,
1296 member.signing_identity.signature_key.as_bytes(),
1297 )
1298 }
1299
1300 /// Whether this frame names this device as its recipient.
1301 ///
1302 /// The comparison is between parsed addresses rather than between strings,
1303 /// which costs nothing and is the same test every other identity claim in
1304 /// this protocol gets. A recipient that is not an address at all is not
1305 /// this device: this device is named by one, and by exactly one spelling
1306 /// of it, since [`Address`] refuses anything but the canonical rendering.
1307 fn is_addressed_to_me(&self, message: &Message) -> bool {
1308 message
1309 .recipient
1310 .as_str()
1311 .parse::<Address>()
1312 .is_ok_and(|recipient| recipient == self.identity.address)
1313 }
1314
1315 fn client(&self) -> Result<Client<impl MlsConfig>> {
1316 build_client(&self.identity, &self.store)
1317 }
1318
1319 /// Loads the group for a session with `peer`, and says which failure it is.
1320 ///
1321 /// A group that will not load is two different things wearing one error.
1322 /// State that is **absent** is a device that never paired with this peer,
1323 /// or one that unpaired, and re-pairing is the repair. State that is
1324 /// **present and unloadable** is a store handing back bytes this device did
1325 /// not write, which no amount of re-pairing fixes and which a bench needs
1326 /// to be told about rather than sent chasing a pairing problem.
1327 fn load_group<C: MlsConfig>(
1328 &self,
1329 client: &Client<C>,
1330 peer: &str,
1331 group_id: &GroupId,
1332 ) -> Result<Group<C>> {
1333 match client.load_group(group_id.as_str().as_bytes()) {
1334 Ok(group) => Ok(group),
1335 Err(e) if self.state_present(group_id)? => Err(LeafError::Storage(format!(
1336 "group state for {peer} is on flash but does not load: {e:?}"
1337 ))),
1338 Err(_) => Err(LeafError::NoSession(peer.to_string())),
1339 }
1340 }
1341
1342 /// The epoch a group is in, read from its stored state.
1343 ///
1344 /// Best effort by construction: the one caller is the sweep in
1345 /// [`LeafDevice::forget_session`], which needs an anchor when the marker
1346 /// cannot give it one, and a state that will not load leaves nothing to
1347 /// read. A device with neither has nothing above the window to have
1348 /// written.
1349 fn current_epoch(&self, group_id: &GroupId) -> Option<u64> {
1350 let client = self.client().ok()?;
1351 client
1352 .load_group(group_id.as_str().as_bytes())
1353 .ok()
1354 .map(|group| group.current_epoch())
1355 }
1356
1357 fn group_id(&self, peer: &str) -> Result<GroupId> {
1358 Ok(GroupId::for_session(
1359 &self.identity.address.to_string(),
1360 peer,
1361 )?)
1362 }
1363
1364 /// Repeats an acknowledgement for a frame this device already answered.
1365 ///
1366 /// The peer only retransmits because it heard nothing, and the frame it
1367 /// sends again is the one it sent before: the same bytes and the same id,
1368 /// frozen in its outbox. Opening it a second time cannot work, because the
1369 /// ratchet spent that generation on the first copy, so before this the
1370 /// device refused it and stayed quiet and the peer went right on asking.
1371 ///
1372 /// Answering from memory is honest. The claim an acknowledgement makes is
1373 /// that the frame with this id reached its recipient, and it did.
1374 fn repeat_acknowledgement(
1375 &mut self,
1376 message: &Message,
1377 now_unix_secs: u64,
1378 ) -> Result<Option<Handled>> {
1379 if !message.requires_ack {
1380 return Ok(None);
1381 }
1382
1383 let Some(record) = self.known_peer(message.sender.as_str())? else {
1384 return Ok(None);
1385 };
1386 if !record.was_acknowledged(message.id.to_string().as_str()) {
1387 return Ok(None);
1388 }
1389
1390 Ok(Some(Handled {
1391 outbound: alloc::vec![frames::acknowledge(
1392 &self.store,
1393 &self.identity,
1394 &self.app_id,
1395 message,
1396 now_unix_secs,
1397 )?],
1398 events: alloc::vec![LeafEvent::Ignored {
1399 reason: String::from("a frame this device already answered arrived again"),
1400 }],
1401 }))
1402 }
1403
1404 /// Mints the acknowledgement owed for a frame that was accepted, and
1405 /// remembers it before the caller can send it.
1406 ///
1407 /// # Why only a peer this device already knows
1408 ///
1409 /// A record exists for a peer that got through the key package gate, which
1410 /// means a verified signature and an address that derives to the key that
1411 /// made it. Answering anyone else would hand a stranger within radio range
1412 /// two things they do not have today: a way to make the device transmit on
1413 /// demand, and an answer to "is there a node at this address", which on a
1414 /// lock is the question worth asking first. It would also let a flood of
1415 /// forged senders each write a record on a part with a few hundred
1416 /// kilobytes of flash.
1417 ///
1418 /// Nothing is lost by the restriction. The peer that sends frames worth
1419 /// acknowledging is the one this device is paired with, and an unknown
1420 /// sender gets exactly the silence it got before.
1421 ///
1422 /// # Why the record is not the whole gate
1423 ///
1424 /// It answers "has this address ever paired", and the frame in hand names
1425 /// its sender in plaintext, so on its own the record clears anyone who
1426 /// overheard the pair once. The caller asks [`Sender`] first, and only a
1427 /// frame that verified a signature or opened under the pair's key reaches
1428 /// here. The two are one gate: this one says the peer is known, that one
1429 /// says the frame is from it.
1430 fn acknowledge(&mut self, message: &Message, now_unix_secs: u64) -> Result<Option<Message>> {
1431 if !message.requires_ack {
1432 return Ok(None);
1433 }
1434
1435 let peer = message.sender.as_str();
1436 let Some(mut record) = self.known_peer(peer)? else {
1437 return Ok(None);
1438 };
1439
1440 let ack = frames::acknowledge(
1441 &self.store,
1442 &self.identity,
1443 &self.app_id,
1444 message,
1445 now_unix_secs,
1446 )?;
1447
1448 // Durable before it is emitted, which is this profile's third
1449 // obligation. A device that answered and then lost the record would
1450 // meet the retransmission with silence, which is the state this
1451 // whole mechanism exists to leave.
1452 record.remember_acknowledged(message.id.to_string().as_str());
1453 record
1454 .save(&self.store, peer)
1455 .map_err(|e| LeafError::Storage(e.to_string()))?;
1456
1457 Ok(Some(ack))
1458 }
1459
1460 /// The record for a peer this device has paired with, or `None`.
1461 ///
1462 /// Distinct from [`Self::peer_record`], which returns a default for an
1463 /// unknown peer. Here the difference between "known" and "not" is the
1464 /// decision being made, so it must not be flattened away.
1465 fn known_peer(&self, peer: &str) -> Result<Option<PeerRecord>> {
1466 PeerRecord::load(&self.store, peer).map_err(|e| LeafError::Storage(e.to_string()))
1467 }
1468
1469 fn peer_record(&self, peer: &str) -> Result<PeerRecord> {
1470 Ok(PeerRecord::load(&self.store, peer)
1471 .map_err(|e| LeafError::Storage(e.to_string()))?
1472 .unwrap_or_default())
1473 }
1474
1475 /// Whether a session with `peer` exists on flash.
1476 pub fn has_session(&self, peer: &str) -> Result<bool> {
1477 let group_id = self.group_id(peer)?;
1478 self.state_present(&group_id)
1479 }
1480
1481 /// Whether group state for this id is on flash, whatever its condition.
1482 fn state_present(&self, group_id: &GroupId) -> Result<bool> {
1483 Ok(self
1484 .store
1485 .load(
1486 KEY_TYPE_GROUP_STATE,
1487 &crate::adapters::hex(group_id.as_str().as_bytes()),
1488 )
1489 .map_err(|e| LeafError::Storage(e.to_string()))?
1490 .is_some())
1491 }
1492
1493 /// The peers this device holds records for.
1494 ///
1495 /// Firmware's way to audit what a device accumulated. A session proves who
1496 /// a peer is and never that the owner meant to have them, so a device that
1497 /// has been in a hallway for a year may hold peers nobody chose; this is
1498 /// how they are found, and [`LeafDevice::unpair`] is how they are removed.
1499 /// Not every entry holds a session: a pairing that stopped after the first
1500 /// frame leaves a record too, and those are the slots a new peer recycles.
1501 pub fn peers(&self) -> Result<Vec<String>> {
1502 self.peer_index()
1503 }
1504
1505 /// What this device recorded about a peer's capabilities.
1506 pub fn peer_env_versions(&self, peer: &str) -> Result<Vec<u8>> {
1507 Ok(self.peer_record(peer)?.env_versions)
1508 }
1509
1510 /// Forgets a peer: its session, its prior epochs, the key package minted
1511 /// for it, and what it advertised.
1512 ///
1513 /// Exposed because a device that is factory reset or unpaired must be able
1514 /// to forget, and because leaving MLS state behind for a peer the owner
1515 /// removed is the kind of residue that outlives the reason it existed.
1516 ///
1517 /// The key package is part of that and is easy to miss, because the only
1518 /// pointer to it is the reference inside the record this call deletes. It
1519 /// holds an init key, which is single use and therefore never consumed if
1520 /// the pairing it was minted for never completed.
1521 ///
1522 /// Also releases the slot it held, so unpairing is how an owner makes room
1523 /// on a device whose peer table is full.
1524 pub fn unpair(&mut self, peer: &str) -> Result<()> {
1525 self.forget_peer(peer)?;
1526
1527 let mut index = self.peer_index()?;
1528 if index.iter().any(|held| held == peer) {
1529 index.retain(|held| held != peer);
1530 self.save_peer_index(&index)?;
1531 }
1532 Ok(())
1533 }
1534}
1535
1536impl core::fmt::Debug for LeafDevice {
1537 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1538 f.debug_struct("LeafDevice")
1539 .field("address", &self.identity.address.to_string())
1540 .finish_non_exhaustive()
1541 }
1542}
1543
1544/// A frame that produced nothing, and why.
1545///
1546/// Surfaced rather than swallowed so a bench can tell "refused" from "silently
1547/// dropped", which are the two states a pairing failure looks like from
1548/// outside a device with no console.
1549fn ignored(reason: &str) -> Handled {
1550 Handled {
1551 outbound: Vec::new(),
1552 events: vec![LeafEvent::Ignored {
1553 reason: String::from(reason),
1554 }],
1555 }
1556}
1557
1558fn parse_app_id(app_id: &str) -> Result<AppId> {
1559 AppId::new(app_id).map_err(|e| LeafError::MalformedFrame(format!("app id: {e}")))
1560}
1561
1562/// Parses an inbound envelope in every form a conforming sender may emit.
1563///
1564/// Three forms, and none of them is gated on what this device advertised.
1565/// Parsing is unconditional in this protocol: a device that decoded only the
1566/// form it asked for would drop frames from a peer that legitimately believed
1567/// it capable, which is exactly what happens after a partial fleet upgrade.
1568///
1569/// The sniff works because `{` opens JSON and, read as the little-endian
1570/// length prefix the compact codec starts with, is a number far above the
1571/// codec's own string cap.
1572fn parse_envelope(body: &str) -> Result<EncryptedMessage> {
1573 if body.starts_with('{') {
1574 return serde_json::from_str(body)
1575 .map_err(|e| LeafError::MalformedFrame(format!("json envelope: {e}")));
1576 }
1577 let bytes = BASE64
1578 .decode(body)
1579 .map_err(|e| LeafError::MalformedFrame(format!("envelope is not base64: {e}")))?;
1580
1581 if let Ok(envelope) = EncryptedMessage::from_bytes(&bytes) {
1582 return Ok(envelope);
1583 }
1584 serde_json::from_slice(&bytes)
1585 .map_err(|e| LeafError::MalformedFrame(format!("base64 envelope: {e}")))
1586}