offline_protocol_leaf/adapters.rs
1//! Adapters that carry mls-rs's storage traits onto [`LeafStore`].
2//!
3//! mls-rs asks for two storage providers, and neither shape belongs in a
4//! device integrator's lap: they carry mls-rs's own types, they are versioned
5//! with a dependency [ADR 0021](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/adr/0021-a-leaf-node-speaks-mls.md)
6//! calls monitored rather than settled, and getting the write ordering wrong
7//! is a confidentiality bug rather than a lost message. So firmware implements
8//! one blob store and these adapters do the rest.
9
10use alloc::{format, string::String, sync::Arc, vec::Vec};
11use mls_rs_core::{
12 error::IntoAnyError,
13 group::{EpochRecord, GroupState, GroupStateStorage},
14 key_package::{KeyPackageData, KeyPackageStorage},
15};
16use mls_rs_core::{
17 identity::BasicCredential,
18 mls_rs_codec::{MlsDecode, MlsEncode},
19};
20use zeroize::Zeroizing;
21
22use crate::store::{LeafStore, StoreError, KEY_TYPE_GROUP_EPOCH, KEY_TYPE_GROUP_STATE};
23use crate::store::{KEY_TYPE_KEY_PACKAGE, KEY_TYPE_PEER};
24
25impl IntoAnyError for StoreError {}
26
27/// How many prior-epoch records a group keeps.
28///
29/// mls-rs delegates this to the storage provider rather than applying it
30/// itself: its own in-memory provider trims to three on every write, and a
31/// provider that never trims keeps every epoch a group has ever left. That is
32/// two failures rather than one. The records accumulate on a part whose flash
33/// is measured in hundreds of kilobytes, and each one holds that epoch's
34/// secrets, so retaining them all turns "how far out of order a message may
35/// arrive" into "how far back a stolen device decrypts". Three is the window
36/// a phone-driven commit cadence needs on a lossy radio, and it is the bound
37/// on both.
38///
39/// This is leaf-side storage policy, not a number the two ends must match:
40/// the phone's provider keeps its own history and neither reads the other's.
41/// It is therefore declared here rather than in `offline-protocol-sealed`,
42/// which is for values a peer would disagree with us about.
43pub(crate) const PRIOR_EPOCH_RETENTION: u64 = 3;
44
45/// Renders bytes as lowercase hex, so an arbitrary group id can be a key id.
46///
47/// Group ids are chosen by whoever created the group and are not required to
48/// be printable. Hex is the shortest thing that cannot collide with the `:`
49/// this module uses as a separator.
50pub(crate) fn hex(bytes: &[u8]) -> String {
51 let mut out = String::with_capacity(bytes.len() * 2);
52 for b in bytes {
53 // `write!` would need `core::fmt::Write` in scope and can fail; a
54 // table lookup cannot.
55 const DIGITS: &[u8; 16] = b"0123456789abcdef";
56 out.push(DIGITS[(b >> 4) as usize] as char);
57 out.push(DIGITS[(b & 0x0f) as usize] as char);
58 }
59 out
60}
61
62/// Where a group's current state is kept.
63fn state_key(group_id: &[u8]) -> String {
64 hex(group_id)
65}
66
67/// Where one prior epoch is kept.
68fn epoch_key(group_id: &[u8], epoch_id: u64) -> String {
69 format!("{}:{}", hex(group_id), epoch_id)
70}
71
72/// Where the highest stored epoch id is kept.
73fn max_epoch_key(group_id: &[u8]) -> String {
74 format!("{}:max", hex(group_id))
75}
76
77/// How many bytes of a state entry carry the sequencing marker: a presence
78/// flag and a big-endian epoch id.
79const STATE_MARKER_LEN: usize = 9;
80
81/// Puts the sequencing marker in front of the state mls-rs handed us.
82///
83/// # Why the marker rides inside the state entry
84///
85/// mls-rs sequences every epoch insert against
86/// [`GroupStateStorage::max_epoch_id`]: an inserted record's id must be
87/// exactly one above what that returns, or the operation is refused. Nothing
88/// in this crate caches either value, so both are read from flash on every
89/// operation, and the two therefore have to move together or not at all.
90///
91/// Held as separate entries they cannot. This seam is atomic per entry rather
92/// than across a set, so a cut or a failed write between a marker record and
93/// the state leaves the marker one ahead of the state it describes, and from
94/// there **every later commit is refused, permanently**: the retry inserts the
95/// epoch id the marker has already counted. Reversing the order does not fix
96/// it, it moves the same wedge into the other window. What that costs is not a
97/// lost frame: the device stops opening anything its peer sends until the
98/// peer's own recovery drives a full reset, which on a door lock is an owner
99/// standing outside it.
100///
101/// One entry closes the window. The marker a validation reads is a slice of
102/// the same bytes as the state it validates against, so there is no moment in
103/// which the two disagree.
104fn encode_state_entry(marker: Option<u64>, state: &[u8]) -> Vec<u8> {
105 let mut out = Vec::with_capacity(STATE_MARKER_LEN + state.len());
106 match marker {
107 Some(epoch) => {
108 out.push(1);
109 out.extend_from_slice(&epoch.to_be_bytes());
110 }
111 // A group that has left no epoch behind yet. mls-rs skips the
112 // sequencing check entirely for `None`, which is what a group that was
113 // just joined needs.
114 None => out.extend_from_slice(&[0u8; STATE_MARKER_LEN]),
115 }
116 out.extend_from_slice(state);
117 out
118}
119
120/// Splits a state entry into its marker and the state mls-rs stored.
121fn decode_state_entry(raw: &[u8]) -> Result<(Option<u64>, &[u8]), StoreError> {
122 if raw.len() < STATE_MARKER_LEN {
123 return Err(StoreError::Corrupt(format!(
124 "group state entry is {} bytes, too short to carry its epoch marker",
125 raw.len()
126 )));
127 }
128 let (header, state) = raw.split_at(STATE_MARKER_LEN);
129 let marker = match header[0] {
130 0 => None,
131 1 => {
132 let mut epoch = [0u8; 8];
133 epoch.copy_from_slice(&header[1..STATE_MARKER_LEN]);
134 Some(u64::from_be_bytes(epoch))
135 }
136 other => {
137 return Err(StoreError::Corrupt(format!(
138 "group state entry carries epoch marker flag {other}, expected 0 or 1"
139 )))
140 }
141 };
142 Ok((marker, state))
143}
144
145/// The sequencing marker in a group's state entry, if it can be read.
146///
147/// For the erasure sweep in [`LeafDevice::unpair`](crate::LeafDevice::unpair),
148/// which needs an anchor rather than a guarantee: every failure here is the
149/// same answer, "this record cannot bound anything", and the caller looks
150/// elsewhere.
151pub(crate) fn state_marker(store: &Arc<dyn LeafStore>, group_key: &str) -> Option<u64> {
152 let raw = store.load(KEY_TYPE_GROUP_STATE, group_key).ok()??;
153 decode_state_entry(&raw).ok().and_then(|(marker, _)| marker)
154}
155
156/// Carries [`GroupStateStorage`] onto the device's blob store.
157#[derive(Clone)]
158pub(crate) struct GroupStateAdapter {
159 store: Arc<dyn LeafStore>,
160}
161
162impl GroupStateAdapter {
163 pub(crate) fn new(store: Arc<dyn LeafStore>) -> Self {
164 Self { store }
165 }
166}
167
168impl GroupStateStorage for GroupStateAdapter {
169 type Error = StoreError;
170
171 fn state(&self, group_id: &[u8]) -> Result<Option<Zeroizing<Vec<u8>>>, Self::Error> {
172 let Some(raw) = self
173 .store
174 .load(KEY_TYPE_GROUP_STATE, &state_key(group_id))?
175 else {
176 return Ok(None);
177 };
178 let raw = Zeroizing::new(raw);
179 let (_, state) = decode_state_entry(&raw)?;
180 Ok(Some(Zeroizing::new(state.to_vec())))
181 }
182
183 fn epoch(
184 &self,
185 group_id: &[u8],
186 epoch_id: u64,
187 ) -> Result<Option<Zeroizing<Vec<u8>>>, Self::Error> {
188 Ok(self
189 .store
190 .load(KEY_TYPE_GROUP_EPOCH, &epoch_key(group_id, epoch_id))?
191 .map(Zeroizing::new))
192 }
193
194 /// Writes the epoch records first and the group state last.
195 ///
196 /// mls-rs asks for one atomic transaction, and this seam offers atomicity
197 /// per entry rather than across a set, so the ordering has to carry what
198 /// the transaction would have. Prior-epoch records are additive: they let
199 /// a message that arrives late still decrypt. Writing them before the
200 /// state means a power cut mid-write leaves the **old** state alongside
201 /// records it does not yet reference, which costs nothing. The reverse
202 /// order would leave a new state whose prior epochs were never written, so
203 /// the device would come back having lost exactly the out-of-order
204 /// tolerance a lossy radio needs.
205 ///
206 /// The state and the marker that sequences it go down as **one entry**,
207 /// because ordering cannot make those two safe in either direction. See
208 /// [`encode_state_entry`] for what a torn write between them costs.
209 ///
210 /// The caller does not emit anything until this returns `Ok`, so a failure
211 /// here is a frame that was never sent rather than state that fell behind
212 /// one that was.
213 ///
214 /// The high-water record and the expired-record deletes both happen
215 /// **after** the state write, and neither propagates a failure. Both
216 /// follow from the same rule: once the state write has returned, the write
217 /// the caller is waiting on is durable, so reporting a failure in
218 /// housekeeping would suppress a frame whose state is already on flash.
219 /// A record that survives a failed delete is swept by
220 /// [`LeafDevice::unpair`](crate::LeafDevice::unpair).
221 fn write(
222 &mut self,
223 state: GroupState,
224 epoch_inserts: Vec<EpochRecord>,
225 epoch_updates: Vec<EpochRecord>,
226 ) -> Result<(), Self::Error> {
227 let previous = self.max_epoch_id(&state.id)?;
228 let mut marker = previous;
229
230 for record in epoch_inserts.iter().chain(epoch_updates.iter()) {
231 self.store.store(
232 KEY_TYPE_GROUP_EPOCH,
233 &epoch_key(&state.id, record.id),
234 &record.data,
235 )?;
236 marker = Some(marker.map_or(record.id, |held: u64| held.max(record.id)));
237 }
238
239 self.store.store(
240 KEY_TYPE_GROUP_STATE,
241 &state_key(&state.id),
242 &encode_state_entry(marker, &state.data),
243 )?;
244
245 // The erasure high-water mark, which is a different job from the
246 // marker above and is why it is a different record rather than the
247 // same one read twice.
248 //
249 // The marker inside the state entry answers "what may be inserted
250 // next", and it goes away with the state it lives in. This record
251 // answers "how far up did this group ever write", which
252 // [`LeafDevice::unpair`](crate::LeafDevice::unpair) needs in order to
253 // bound a sweep over records it cannot enumerate, and it has to be
254 // readable in the one case the other is not: a state entry the part
255 // hands back as something else. It is deliberately never lowered by
256 // trimming, so it stays above every record that could still be there.
257 let advanced_to = if marker == previous { None } else { marker };
258 if let Some(high) = advanced_to {
259 let _ = self.store.store(
260 KEY_TYPE_GROUP_EPOCH,
261 &max_epoch_key(&state.id),
262 &high.to_be_bytes(),
263 );
264 }
265
266 // One delete per record that entered, which is all the window can
267 // lose: mls-rs requires each inserted epoch id to be exactly one above
268 // the highest stored, so the window advances by the number of inserts
269 // and never skips. Updates rewrite records already inside it.
270 for record in epoch_inserts.iter() {
271 if let Some(expired) = record.id.checked_sub(PRIOR_EPOCH_RETENTION) {
272 let _ = self
273 .store
274 .delete(KEY_TYPE_GROUP_EPOCH, &epoch_key(&state.id, expired));
275 }
276 }
277
278 Ok(())
279 }
280
281 /// The highest epoch id this group has left behind.
282 ///
283 /// Read from the same entry as the state it belongs to, because mls-rs
284 /// sequences every epoch insert against this value and a marker that got
285 /// ahead of its state wedges the group permanently. See
286 /// [`encode_state_entry`].
287 ///
288 /// A group with no state has no marker, and that `None` is what makes
289 /// mls-rs skip the sequencing check for a group that was just joined.
290 fn max_epoch_id(&self, group_id: &[u8]) -> Result<Option<u64>, Self::Error> {
291 let Some(raw) = self
292 .store
293 .load(KEY_TYPE_GROUP_STATE, &state_key(group_id))?
294 else {
295 return Ok(None);
296 };
297 let raw = Zeroizing::new(raw);
298 let (marker, _) = decode_state_entry(&raw)?;
299 Ok(marker)
300 }
301}
302
303/// How many minted-but-unspent key packages a device keeps.
304///
305/// mls-rs deletes an entry when a join consumes it, so the only entries that
306/// accumulate are packages nobody ever spent: a pairing that was abandoned, or
307/// one a stranger provoked. [`LeafDevice::unpair`](crate::LeafDevice::unpair)
308/// reclaims the one a peer record still names, which needs an owner who knows
309/// the peer is there; a stranger provoking mints leaves records nobody is
310/// watching. Without a bound each of those is private key material written to
311/// flash and never reclaimed, and provoking a mint costs an attacker one
312/// signed frame.
313///
314/// Evicting the oldest is the trade this makes, and it is not free: a peer
315/// holding an evicted package can no longer complete a join with it, so a
316/// flood turns into a pairing failure rather than a full flash. Four is enough
317/// for a household pairing its phones in sequence, and small enough that the
318/// residue is bounded.
319const MAX_UNSPENT_KEY_PACKAGES: usize = 4;
320
321/// Where the list of unspent key package ids is kept.
322///
323/// Held under the same key type as the packages themselves. It cannot collide
324/// with one: every other id there is [`hex`] output, and `_` is not a hex
325/// digit.
326const KEY_PACKAGE_INDEX: &str = "__index__";
327
328/// Carries [`KeyPackageStorage`] onto the device's blob store.
329///
330/// The values here hold the init and leaf-node private keys of a key package
331/// the device minted and has not yet spent. mls-rs deletes an entry when the
332/// package is consumed by a join, which is why an init key is single use and
333/// why a static pairing artifact must never carry one. What it does not do is
334/// bound the ones never consumed, so this adapter does: see
335/// [`MAX_UNSPENT_KEY_PACKAGES`].
336#[derive(Clone)]
337pub(crate) struct KeyPackageAdapter {
338 store: Arc<dyn LeafStore>,
339}
340
341impl KeyPackageAdapter {
342 pub(crate) fn new(store: Arc<dyn LeafStore>) -> Self {
343 Self { store }
344 }
345
346 /// The ids of unspent packages, oldest first.
347 ///
348 /// A corrupt index is treated as an empty one rather than an error. It is
349 /// housekeeping state, and refusing to mint a key package because a list
350 /// of previous ones does not parse would turn a recoverable annoyance into
351 /// a device that cannot pair.
352 fn index(&self) -> Result<Vec<String>, StoreError> {
353 let Some(raw) = self.store.load(KEY_TYPE_KEY_PACKAGE, KEY_PACKAGE_INDEX)? else {
354 return Ok(Vec::new());
355 };
356 Ok(serde_json::from_slice(&raw).unwrap_or_default())
357 }
358
359 fn save_index(&self, index: &[String]) -> Result<(), StoreError> {
360 let encoded = serde_json::to_vec(index)
361 .map_err(|e| StoreError::Store(format!("cannot encode key package index: {e}")))?;
362 self.store
363 .store(KEY_TYPE_KEY_PACKAGE, KEY_PACKAGE_INDEX, &encoded)
364 }
365
366 /// Erases the package filed under `reference` and stops the index naming
367 /// it.
368 ///
369 /// Two callers, and the second is why this is reachable by reference
370 /// rather than only by the id mls-rs holds. mls-rs erases a package a join
371 /// consumed. [`LeafDevice::unpair`](crate::LeafDevice::unpair) erases one
372 /// minted for a peer the owner has removed, and nothing else ever will:
373 /// an init key is single use, so a package nobody spends is never
374 /// consumed, and the ring only evicts it once four later mints have
375 /// happened.
376 ///
377 /// The erase is what propagates and dropping the name is best effort. By
378 /// then the material is gone, which is what was asked for, and a stale
379 /// index entry costs one slot and is evicted in its turn. Reporting it
380 /// would turn a deletion that did happen into one the caller believes did
381 /// not: mls-rs calls this after persisting the group state that consumed
382 /// the package, so the caller would withhold a frame whose state is
383 /// already on flash.
384 pub(crate) fn erase(&self, reference: &str) -> Result<(), StoreError> {
385 self.store.delete(KEY_TYPE_KEY_PACKAGE, reference)?;
386
387 if let Ok(mut index) = self.index() {
388 if let Some(at) = index.iter().position(|held| held == reference) {
389 index.remove(at);
390 let _ = self.save_index(&index);
391 }
392 }
393 Ok(())
394 }
395}
396
397impl KeyPackageStorage for KeyPackageAdapter {
398 type Error = StoreError;
399
400 /// Erases the package a join consumed, so it does not hold a slot against
401 /// the ones still outstanding. See [`KeyPackageAdapter::erase`].
402 fn delete(&mut self, id: &[u8]) -> Result<(), Self::Error> {
403 self.erase(&hex(id))
404 }
405
406 fn insert(&mut self, id: Vec<u8>, pkg: KeyPackageData) -> Result<(), Self::Error> {
407 let encoded = pkg
408 .mls_encode_to_vec()
409 .map_err(|e| StoreError::Store(format!("cannot encode key package data: {e:?}")))?;
410 let key = hex(&id);
411
412 let mut index = self.index()?;
413 if !index.iter().any(|held| held == &key) {
414 index.push(key.clone());
415 }
416 let evicted: Vec<String> = if index.len() > MAX_UNSPENT_KEY_PACKAGES {
417 index
418 .drain(..index.len() - MAX_UNSPENT_KEY_PACKAGES)
419 .collect()
420 } else {
421 Vec::new()
422 };
423
424 // Two writes, ordered so the same thing survives either failure: an
425 // index entry naming a package that is not there. That costs one slot
426 // and is evicted in its turn. The opposite residue is private key
427 // material the index has stopped naming, and **nothing reclaims that**:
428 // [`LeafDevice::unpair`](crate::LeafDevice::unpair) erases the package
429 // a peer record still names and sweeps epoch records, neither of which
430 // reaches one already evicted, this key type has no sweep of its own,
431 // and an eviction the index has forgotten is never attempted again.
432 //
433 // So the evicted packages are erased before the index stops naming
434 // them, and the index is written before the package it names. A delete
435 // that fails takes the whole mint down with it, which is a key package
436 // this device does not hand out rather than one it cannot account for.
437 for stale in evicted {
438 self.store.delete(KEY_TYPE_KEY_PACKAGE, &stale)?;
439 }
440 self.save_index(&index)?;
441
442 self.store.store(KEY_TYPE_KEY_PACKAGE, &key, &encoded)
443 }
444
445 fn get(&self, id: &[u8]) -> Result<Option<KeyPackageData>, Self::Error> {
446 let Some(raw) = self.store.load(KEY_TYPE_KEY_PACKAGE, &hex(id))? else {
447 return Ok(None);
448 };
449 let decoded = KeyPackageData::mls_decode(&mut &raw[..])
450 .map_err(|e| StoreError::Corrupt(format!("key package data does not decode: {e:?}")))?;
451 Ok(Some(decoded))
452 }
453}
454
455/// What a peer told us it can parse, and what we therefore may emit to it.
456///
457/// Persisted because these are end-to-end capabilities: they describe what the
458/// recipient parses after any number of relay hops, so a device that forgot
459/// them across a power cycle would silently downgrade every established peer
460/// until the next key package exchange.
461#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
462pub(crate) struct PeerRecord {
463 /// Envelope forms the peer parses. `[1]` means the compact envelope.
464 #[serde(default)]
465 pub(crate) env_versions: Vec<u8>,
466 /// Frame encodings the peer decodes on the next hop.
467 #[serde(default)]
468 pub(crate) wire_versions: Vec<u8>,
469 /// Whether this device has already given this peer a key package.
470 ///
471 /// Persisted rather than held in RAM because an init key is single use:
472 /// a device that forgot across a power cycle would mint a second package
473 /// on the next exchange and leave the peer holding two, of which only one
474 /// is ever spent. Cleared when a peer resets the session, which is the
475 /// one moment a fresh package is required rather than wasteful.
476 #[serde(default)]
477 pub(crate) key_package_sent: bool,
478
479 /// The reference of the key package this device last minted for this peer.
480 ///
481 /// A key package is a **bearer token**: it travels in a frame that is
482 /// signed but not encrypted, so a copy taken off the air is as spendable
483 /// as the original, and a Welcome built around one passes every other gate
484 /// honestly. Recording which package went to which peer is what makes the
485 /// difference checkable, and
486 /// [`LeafDevice::handle`](crate::LeafDevice::handle) checks it before the
487 /// join rather than after, so a refused Welcome leaves the package
488 /// **unspent** and the peer it was minted for can still use it.
489 ///
490 /// One reference rather than a history: this device mints one package per
491 /// peer and re-mints only on a reset, so the newest is the one a peer was
492 /// told to use. A peer holding an older package that a re-mint replaced is
493 /// refused and re-pairs, which is the same path every other stale-pairing
494 /// failure takes.
495 #[serde(default)]
496 pub(crate) key_package_ref: Option<String>,
497
498 /// The signed timestamp of the most recent session reset acted on from
499 /// this peer, or `0` if none.
500 ///
501 /// A reset tears down a live session, so a frame carrying one is worth
502 /// capturing and sending again, and a reset is honoured only when it is
503 /// **strictly newer** than this.
504 ///
505 /// This replaced a ring of the last few frame ids, which was what a device
506 /// could do while nothing in the signed payload stated freshness: it
507 /// denied a repeat of a *remembered* frame and left a capture older than
508 /// the ring spendable once. A timestamp inside the signature makes the
509 /// stronger statement affordable — every older reset is refused, including
510 /// ones this device has no memory of — and it costs one integer per peer
511 /// instead of a list that grows with traffic, which on a device with a few
512 /// hundred kilobytes of flash is the difference that decides it.
513 ///
514 /// Sound only because the stamp is inside the signature. On a payload that
515 /// left it outside, an attacker would rewrite it, park this at `i64::MAX`
516 /// and deny the peer every future reset, which is worse than the replay.
517 /// A leaf verifies no such payload; see
518 /// [`frames`](crate::frames) for why it has no legacy peer to accommodate.
519 #[serde(default)]
520 pub(crate) last_reset_ms: i64,
521
522 /// The ids of the last few frames from this peer that were acknowledged.
523 ///
524 /// An acknowledgement is the frame most likely to be the one that goes
525 /// missing: it is the last in the exchange and nothing answers it, so
526 /// nothing retries it. When one is lost the peer retransmits what it
527 /// already delivered, the ratchet refuses the replay, and without this the
528 /// device stays silent and the full retry ladder runs anyway, which is the
529 /// case issue 402 exists to end rather than to narrow.
530 ///
531 /// Answering from memory closes it: a frame whose id is here was received
532 /// once, so saying so again is true, costs one small frame, and stops the
533 /// remaining nine.
534 ///
535 /// # Why a list here, where a reset needed only an integer
536 ///
537 /// [`Self::last_reset_ms`] replaced exactly this shape, and the difference
538 /// is where the number being compared comes from. A reset's timestamp is
539 /// inside the signature, so a high-water mark over it is a statement an
540 /// attacker cannot move. A sealed frame carries no signature at all, and
541 /// its id and timestamp sit outside the AEAD, so a watermark over either
542 /// could be parked in the future by anyone within radio range and would
543 /// deny the peer every future acknowledgement.
544 ///
545 /// A list has no such failure. Nothing reaches it that this device did not
546 /// itself acknowledge, and its worst case is re-answering a frame it
547 /// already answered, which is one frame rather than a durably broken pair.
548 #[serde(default)]
549 pub(crate) acknowledged: Vec<String>,
550}
551
552/// How many acknowledged frame ids a device remembers per peer.
553///
554/// Four, because the peer's own ladder is what sets the number: it waits ten
555/// seconds for an answer before retransmitting, so the ids at risk are the ones
556/// still in flight in that window, and a phone does not have dozens of frames
557/// outstanding to a lock. It is a bound on flash, not a bound on correctness:
558/// forgetting an id costs the retry ladder that was running before this
559/// existed.
560pub(crate) const REMEMBERED_ACKS: usize = 4;
561
562impl PeerRecord {
563 /// Whether a frame from this peer has already been acknowledged, so the
564 /// answer can be repeated without opening it a second time.
565 pub(crate) fn was_acknowledged(&self, id: &str) -> bool {
566 self.acknowledged.iter().any(|seen| seen == id)
567 }
568
569 /// Records a frame as acknowledged, evicting the oldest when full.
570 pub(crate) fn remember_acknowledged(&mut self, id: &str) {
571 if self.was_acknowledged(id) {
572 return;
573 }
574 if self.acknowledged.len() >= REMEMBERED_ACKS {
575 self.acknowledged.remove(0);
576 }
577 self.acknowledged.push(String::from(id));
578 }
579
580 /// Records a reset stamped `signed_ms` as acted on.
581 pub(crate) fn remember_reset(&mut self, signed_ms: i64) {
582 self.last_reset_ms = signed_ms;
583 }
584
585 /// Whether a reset stamped `signed_ms` is one this device has not acted on.
586 ///
587 /// Strictly newer, because the stamp is what identifies the frame here:
588 /// admitting an equal one admits the frame itself a second time. Two
589 /// genuine resets inside one millisecond would need the phone's rekey
590 /// ladder to run far faster than its own floor allows, and if it ever
591 /// happened the pair still converges, because a refused reset shows up as
592 /// the next frame that will not open and drives another.
593 pub(crate) fn reset_is_unspent(&self, signed_ms: i64) -> bool {
594 signed_ms > self.last_reset_ms
595 }
596
597 pub(crate) fn load(store: &Arc<dyn LeafStore>, peer: &str) -> Result<Option<Self>, StoreError> {
598 let Some(raw) = store.load(KEY_TYPE_PEER, peer)? else {
599 return Ok(None);
600 };
601 serde_json::from_slice(&raw)
602 .map(Some)
603 .map_err(|e| StoreError::Corrupt(format!("peer record does not decode: {e}")))
604 }
605
606 pub(crate) fn save(&self, store: &Arc<dyn LeafStore>, peer: &str) -> Result<(), StoreError> {
607 let encoded = serde_json::to_vec(self)
608 .map_err(|e| StoreError::Store(format!("cannot encode peer record: {e}")))?;
609 store.store(KEY_TYPE_PEER, peer, &encoded)
610 }
611}
612
613/// Reads the address out of a basic credential.
614///
615/// The credential's whole content is the peer's address in its canonical text
616/// form. Anything else is a credential this protocol did not mint.
617pub(crate) fn credential_address(credential: &BasicCredential) -> Result<&str, crate::LeafError> {
618 core::str::from_utf8(&credential.identifier).map_err(|_| {
619 crate::LeafError::IdentityBinding(String::from(
620 "credential identifier is not valid UTF-8, so it names no address",
621 ))
622 })
623}