Skip to main content

LeafDevice

Struct LeafDevice 

Source
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

Source

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.

Source

pub fn resume(store: Arc<dyn LeafStore>, app_id: &str) -> Result<Self>

Loads a device that was provisioned earlier.

Source

pub fn open(store: Arc<dyn LeafStore>, app_id: &str) -> Result<Self>

Loads a device, provisioning one on first boot.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn has_session(&self, peer: &str) -> Result<bool>

Whether a session with peer exists on flash.

Source

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.

Source

pub fn peer_env_versions(&self, peer: &str) -> Result<Vec<u8>>

What this device recorded about a peer’s capabilities.

Source

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§

Source§

impl Debug for LeafDevice

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more