pub struct LeafDevice { /* private fields */ }Expand description
A leaf node.
§The never-committing profile
The peer creates the group, adds this device, and issues every commit. The device joins, opens what arrives, answers, and persists. It never emits a Welcome, a commit or a proposal, and never any group, rich, document or relay frame. Per-commit cost here is two elliptic-curve operations; per-message cost is symmetric only.
Post-compromise security therefore arrives on the peer’s cadence rather
than this device’s, and a driven rekey reaches it as a key package with
session_reset set rather than as an unsolicited Welcome.
§Storage is the source of truth
No MLS state is cached in this value. Every operation loads the group from the store, does its work, writes back, and only then returns the frame to send. That costs a load per frame, which is the right trade for a device that handles a frame every few minutes and must survive losing power between any two instructions: there is no in-RAM state for a power cut to desynchronize from what is on flash.
§One writer at a time
Every operation that advances state takes &mut self, so the compiler
enforces what the ratchet requires. Two seals running at once would load
the same generation, encrypt under the same key and nonce, and emit both
frames: the AEAD nonce reuse this crate’s whole persist-before-emit rule
exists to prevent, arrived at without ever losing power. The same argument
covers the send counter, where a race mints one id twice and the peer’s
deduplicator swallows the second message.
A device is therefore one value, exclusively held. Two LeafDevices over
one LeafStore would each satisfy the borrow checker and race anyway;
construct one per store.
§What it does not defend against
Anyone pairing. Every gate here answers “is this peer the address it
claims to be”, and none of them answers “did the owner mean this peer”.
Producing a key that derives to its own address costs nothing, so an
unattended device admits whoever asks, up to the bound above. That is the
same position two phones are in, where the out-of-band artifact carries
first-contact trust; on a device it is firmware that decides when the radio
accepts a pairing at all, and firmware that decides what an opened message
may actuate. LeafDevice::peers is how it audits what accumulated.
A replayed control frame, as far as this device’s clock is honest. The signed payload states when the frame was made, so one older than two days is refused outright, and a reset-flagged key package (the destructive case, since it tears down a live session) is additionally refused unless it is newer than the last reset this device acted on. One capture is therefore worth one teardown, and only inside a two-day window.
What that leaves is the clock. Both checks are made against the time source firmware supplies, so a device that supplies a wrong one either refuses its peer or, if the clock is far enough behind, admits frames it should have refused. The obligation is stated with the others in the provisioning chapter, and it is why the time source is a parameter on every entry point here rather than something this crate reaches for.
Debug renders the device’s address and nothing else. Everything else it
holds is either secret or a handle to secrets, and a device that printed
its identity key into a log would undo the part of the threat model that
says the key never leaves storage.
Implementations§
Source§impl LeafDevice
impl LeafDevice
Sourcepub fn provision(store: Arc<dyn LeafStore>, app_id: &str) -> Result<Self>
pub fn provision(store: Arc<dyn LeafStore>, app_id: &str) -> Result<Self>
Generates an identity and returns a device that holds it.
Draws from the getrandom backend the firmware registered. Refuses if
the store already holds an identity, because replacing one changes the
device’s address and silently orphans every peer paired with it.
Sourcepub fn resume(store: Arc<dyn LeafStore>, app_id: &str) -> Result<Self>
pub fn resume(store: Arc<dyn LeafStore>, app_id: &str) -> Result<Self>
Loads a device that was provisioned earlier.
Sourcepub fn open(store: Arc<dyn LeafStore>, app_id: &str) -> Result<Self>
pub fn open(store: Arc<dyn LeafStore>, app_id: &str) -> Result<Self>
Loads a device, provisioning one on first boot.
Sourcepub fn address(&self) -> &Address
pub fn address(&self) -> &Address
This device’s address. Self-certifying: it is the hash of the identity key, so a peer checks a claim rather than trusting a directory.
Sourcepub fn key_package_frame(
&mut self,
peer: &str,
now_unix_secs: u64,
) -> Result<Message>
pub fn key_package_frame( &mut self, peer: &str, now_unix_secs: u64, ) -> Result<Message>
Mints a key package and wraps it in a signed frame for peer.
now_unix_secs is the pairing time source. It is a parameter because a
device has no clock, and passing something wrong here is the difference
between pairing and being refused as expired.
Sourcepub fn seal(
&mut self,
peer: &str,
plaintext: &str,
now_unix_secs: u64,
) -> Result<Message>
pub fn seal( &mut self, peer: &str, plaintext: &str, now_unix_secs: u64, ) -> Result<Message>
Seals plaintext to peer.
The ratchet advances, the new state is persisted, and only then does the frame exist. A store that fails produces an error and no frame, which is the whole point: a device that emitted first and persisted second would, after a power cut, come back and reuse an AEAD nonce.
Sourcepub fn handle(
&mut self,
message: &Message,
now_unix_secs: u64,
) -> Result<Handled>
pub fn handle( &mut self, message: &Message, now_unix_secs: u64, ) -> Result<Handled>
Handles one inbound frame.
Returns the frames to send and what happened. Everything in
Handled::outbound is already durable by the time it is returned.
§A frame addressed elsewhere is answered by nothing
A radio hears what it is not the recipient of, so this is the first question asked, before a signature is verified or a prefix is read. Nothing further down asks it again, and neither kind of frame answers it on its own: a control frame’s signature covers the recipient rather than checking it, so one honestly signed for somebody else verifies perfectly here, and a sealed frame carries no signature at all, so its recipient is whatever the last hand to touch it wrote there. Being able to open a frame is a different claim from having been sent it.
Three things follow from not asking. An overheard key package admits a peer, spends flash on a record, mints a private init key nobody asked this device for, and answers a phone that never addressed it. A sealed frame this device really can open is acted on after anyone who captured it rewrote the recipient, because that field is not inside the AEAD. And every other prefix arrives as an identity-binding failure, so ordinary traffic between two neighbours reaches firmware wearing the shape of an attack, on a device whose only account of itself is that error stream.
What the check does not do is keep anyone else’s ciphertext readable here: the group and credential gates below hold either way.
It is LeafEvent::Ignored rather than an error because overhearing is
what a shared radio does, and because firmware that carries frames for
its neighbours needs “not mine” to be a fact it can act on rather than a
failure it has to interpret.
Sourcepub fn has_session(&self, peer: &str) -> Result<bool>
pub fn has_session(&self, peer: &str) -> Result<bool>
Whether a session with peer exists on flash.
Sourcepub fn peers(&self) -> Result<Vec<String>>
pub fn peers(&self) -> Result<Vec<String>>
The peers this device holds records for.
Firmware’s way to audit what a device accumulated. A session proves who
a peer is and never that the owner meant to have them, so a device that
has been in a hallway for a year may hold peers nobody chose; this is
how they are found, and LeafDevice::unpair is how they are removed.
Not every entry holds a session: a pairing that stopped after the first
frame leaves a record too, and those are the slots a new peer recycles.
Sourcepub fn peer_env_versions(&self, peer: &str) -> Result<Vec<u8>>
pub fn peer_env_versions(&self, peer: &str) -> Result<Vec<u8>>
What this device recorded about a peer’s capabilities.
Sourcepub fn unpair(&mut self, peer: &str) -> Result<()>
pub fn unpair(&mut self, peer: &str) -> Result<()>
Forgets a peer: its session, its prior epochs, the key package minted for it, and what it advertised.
Exposed because a device that is factory reset or unpaired must be able to forget, and because leaving MLS state behind for a peer the owner removed is the kind of residue that outlives the reason it existed.
The key package is part of that and is easy to miss, because the only pointer to it is the reference inside the record this call deletes. It holds an init key, which is single use and therefore never consumed if the pairing it was minted for never completed.
Also releases the slot it held, so unpairing is how an owner makes room on a device whose peer table is full.
Trait Implementations§
Auto Trait Implementations§
impl !RefUnwindSafe for LeafDevice
impl !UnwindSafe for LeafDevice
impl Freeze for LeafDevice
impl Send for LeafDevice
impl Sync for LeafDevice
impl Unpin for LeafDevice
impl UnsafeUnpin for LeafDevice
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more