Skip to main content

MlsManager

Struct MlsManager 

Source
pub struct MlsManager { /* private fields */ }
Expand description

Main MLS manager for end-to-end encryption.

Implementations§

Source§

impl MlsManager

Source

pub fn identity_assertion(&self, signed_data: &[u8]) -> Result<Vec<u8>>

Builds this identity’s assertion over signed_data.

The result is what a Bluetooth LE peripheral serves from its Identity characteristic and what a peer-stream transport sends first. The key never leaves the manager: the caller supplies the bytes to sign and receives the whole layout, so no bridge assembles it.

signed_data means nothing to a verifier and may be empty. The mobile peripherals put their mesh advertisement there; a peripheral with nothing to say should pass nothing.

§Errors

Whatever MlsManager::sign_data returns when there is no identity to sign with.

Source§

impl MlsManager

Source

pub fn new( user_id: impl Into<String>, storage: Arc<dyn MlsStorage>, ) -> Result<Self>

Creates a new MLS manager.

Source

pub fn user_id(&self) -> &str

Returns the local user’s ID.

Source

pub fn load_or_create_identity( storage: &Arc<dyn MlsStorage>, ) -> Result<(SignatureKeyPair, Address)>

Loads this device’s identity key from storage, minting and persisting one on first call, and returns the address it derives to.

This is the bootstrap entry point: it exists so the address can be known before an MlsManager is constructed. The manager needs its own id at construction time (it goes into the credential and every session slot), but under self-certifying addressing that id is a function of the identity key — which used to be minted inside the constructor. Callers break the circle by calling this first and passing the resulting address to Self::new.

Idempotent: a second call returns the stored key and the same address. The keypair is written before the address is derived, so a caller that crashes between the two finds the same identity on the next start rather than minting a second one.

Source

pub fn generate_key_package(&self) -> Result<KeyPackageBundle>

Generates a new key package for distribution over the push path.

Source

pub fn generate_publication_key_package(&self) -> Result<KeyPackageBundle>

Generates a key package reserved for a publication slot.

Distinct from Self::generate_key_package only in that the result is withheld from Self::get_or_create_key_package — see KeyPackageBundle::reserved_for_publication for why sharing one package between a published record and a pushed exchange corrupts both.

Source

pub fn get_or_create_key_package(&self) -> Result<KeyPackageBundle>

Gets an existing unclaimed key package or generates a new one.

Three kinds of package are skipped, for the same underlying reason — an MLS init key is consumed by its first user, so two parties must never be pointed at one:

  • packages reserved for publication slots, or synced by the application (Self::mark_key_package_synced), or a pushed-to peer and a stranger who fetched the published record would race for it;
  • packages already claimed by a peer over the push path (Self::take_push_key_package), or this peer-less entry point would hand out a key another peer is expected to use.

This is the peer-less escape hatch (the FFI surface and tests). Callers that know the recipient should use Self::take_push_key_package, which is what actually holds the one-key-per-peer property; a package handed out here is unclaimed, so the push path may later claim it for the first peer it is pushed to.

Source

pub fn take_push_key_package(&self, peer_id: &str) -> Result<PushKeyPackage>

Hands peer_id the key package to advertise to it, minting one if it has no live package of its own.

This is the push path’s entry point, and the whole point of it is that two peers never receive the same init key. Before this existed the push path returned the first stored package to every caller until somebody’s Welcome consumed it — the LastResort-style reuse RFC 9420 §16.8 permits only as a denial-of-service fallback, and which the Least Authority MDK audit flagged as enabling unsolicited joins and cross-group linkage. It also made a second peer’s Welcome unprocessable once the first had used the key, with nothing to re-drive the exchange.

Assignment is stored on the package rather than in a separate map, so it survives restarts for free and cannot disagree with the pool it describes. Resolution order:

  1. this peer’s own live package — a repeat push costs no new material;
  2. an unclaimed package (minted by the peer-less entry point, or by a build predating assignment), claimed here so upgrading does not strand it;
  3. a freshly minted package, claimed for this peer.

Past MAX_PUSH_KEY_PACKAGES live packages it stops growing the pool and reuses the newest one, reporting that through PushKeyPackage::pool_exhausted so the caller can surface it. That is a deliberate degradation to the old behaviour rather than an error: the alternative — refusing to advertise, or evicting a package a peer may still be about to use — costs session establishment, which is worse than the weakened forward secrecy it would buy.

Only step 3 is gated by the ceiling, because only minting grows the pool: claiming an unclaimed package relabels one that already exists, so degrading to a shared package while a claimable one sits idle would buy nothing. A corollary is that the shared package is always assigned to another peer — reaching that branch means neither an own nor an unclaimed package was found — and “newest” makes it the most recently assigned one, which has the most lifetime left but is also the peer most likely to be mid-establishment. If the over-ceiling peer’s Welcome lands first, that peer’s held advertisement becomes unprocessable until the next push to it mints a successor. Sharing the oldest instead would trade that for the shortest remaining lifetime; neither is free, and the exhaustion report is what makes the condition visible either way.

Consumption is what rotates a peer’s key: a Welcome built against the package removes its init key from provider storage, the loader then reports the package gone, and the next push to that peer mints a fresh one.

Source

pub fn key_package_by_id( &self, package_id: &str, ) -> Result<Option<KeyPackageBundle>>

Loads a specific key package this device owns, or None if it is gone.

None is the consumption signal the publication slots run on: the load prunes and reports missing any package whose private init key has left the OpenMLS provider, which is exactly what processing a Welcome built against it does. A slot whose package reads back None has been used by somebody and needs replacing.

Source

pub fn delete_key_package(&self, package_id: &str) -> Result<()>

Deletes a key package this device owns.

Exists to reclaim a publication package that was minted but whose record was never built or queued. Such a package is reserved, so Self::get_or_create_key_package will never hand it out, and no slot references it — nothing else would remove it before its lifetime runs out, so a repeatedly failing publish would otherwise strand fresh provider key material every refresh.

“Strand” is meant literally, which is why this destroys the private init key too: deleting the bundle record alone leaves the material OpenMLS holds in place with nothing left pointing at it.

Source

pub fn import_key_package( &self, user_id: &str, key_package_data: &[u8], ) -> Result<()>

Imports a contact’s key package for later use.

Security (SEC-M5): the caller-supplied user_id becomes both the storage key and — at session creation — the identity this key package is trusted for. Reject storage-hostile ids (the id is a raw key_id), cryptographically validate the key package, and require the embedded credential identity to equal user_id so a key package generated by one user can never be imported under another’s name.

The identity claim is then proved rather than trusted: see Self::verify_address_binding. No Authentication Service verdict travels with the call any more, because there is nothing for the caller to know — the key package carries its own proof.

Source

pub fn get_pending_key_packages(&self) -> Result<Vec<KeyPackageBundle>>

Lists the live packages an application may publish itself.

Only packages nobody has spoken for: unclaimed, unreserved and not yet synced. A package the push path assigned to a peer, or one standing in one of the engine’s own publication slots, is the engine’s to publish. Listing them here invited an application following the documented upload loop to mark them synced, which handed one init key to two parties and, while marking deleted the record, stranded the old key every time the engine minted a successor.

Source

pub fn mark_key_package_synced(&mut self, package_id: &str) -> Result<()>

Records that the application has published a package itself.

The record is kept, flagged, and withheld from both hand-out paths. It is not deleted, and the private init key is not destroyed:

  • The key must outlive the mark, because a stranger who fetched the uploaded copy may still Welcome us, and only that Welcome consumes it.
  • The record must outlive the mark, because the record is the only thing that carries expiry. Deleting it, which is what this used to do, left the init key resident for the life of the install whenever the uploaded copy was never claimed (issue 367). Kept, it is withdrawn at expiry and its key destroyed past the grace window like any other.

Marking an id that is unknown, expired or already consumed does nothing: there is no live package left to publish. Marking a package the push path already handed to a peer is allowed but logged, since that peer and the uploaded copy now share an init key; the peer’s held copy stays openable, and a failed establishment is recovered by the next push, which mints that peer a successor.

Takes &mut self so a caller sharing the manager behind an RwLock must hold the write half. The push path claims a package under the read half, and both are a load, a change and a store of the same record: under two read guards the claim’s store can land second and erase the mark, leaving the package uploaded and advertised to a peer with nothing logged.

Source

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

Checks if a session exists with another user.

Source

pub fn create_session(&self, other_user_id: &str) -> Result<WelcomeMessage>

Creates a new 1:1 session with another user.

Source

pub fn join_session(&self, welcome: &WelcomeMessage) -> Result<GroupInfo>

Joins a session using a Welcome message.

Source

pub fn session_established_at_ms( &self, other_user_id: &str, ) -> Result<Option<u64>>

When the current 1:1 session with other_user_id was created or adopted on this device (local clock, ms), or None when there is no session or it predates the stamp.

A frame sealed before this moment cannot have been sealed under this session, so its failure to decrypt says nothing about whether the two sides hold the same session now.

Source

pub fn replace_session_with_welcome( &self, welcome: &WelcomeMessage, ) -> Result<GroupInfo>

Replaces an existing session with an incoming Welcome message.

This implements the “welcome-wins” strategy for race condition resolution. When both peers simultaneously create a session, this method allows one peer to replace their own session with the other peer’s Welcome, ensuring both end up with the same cryptographic state.

Source

pub fn encrypt_for_user( &self, other_user_id: &str, plaintext: &[u8], ) -> Result<EncryptedMessage>

Encrypts a message for a 1:1 session.

Source

pub fn encrypt_for_existing_session( &self, other_user_id: &str, plaintext: &[u8], ) -> Result<EncryptedMessage>

Encrypts a message for a 1:1 session that is known to exist.

Unlike encrypt_for_user, this skips the has_session() storage check and goes directly to encrypt_message(). The caller must guarantee the session exists (e.g., via an in-memory cache). If the session was deleted externally, SessionNotFound is returned.

Source

pub fn get_pending_welcome( &self, other_user_id: &str, ) -> Result<Option<WelcomeMessage>>

Gets a pending Welcome message.

Source

pub fn clear_pending_welcome(&self, other_user_id: &str) -> Result<()>

Clears a pending Welcome message.

Source

pub fn decrypt_from_user( &self, encrypted: &EncryptedMessage, claimed_sender: &str, ) -> Result<Option<Vec<u8>>>

Decrypts a message from a 1:1 session.

claimed_sender is the transport-level sender this message will be attributed to; decryption fails with MlsError::SenderIdentityMismatch if it does not match the MLS-authenticated credential (SEC-M1).

Source

pub fn list_sessions(&self) -> Result<Vec<String>>

Lists all active 1:1 sessions.

Source

pub fn delete_session(&self, other_user_id: &str) -> Result<()>

Deletes a 1:1 session.

The establishment stamp goes first: an Err must mean the session is still there, because callers clean up their own state for the peer only once this succeeds. Deleting the session first and then failing on the stamp left a peer recorded as confirmed with no session.

Source

pub fn create_group(&self, group_name: &str) -> Result<GroupInfo>

Creates a new group.

Source

pub fn add_group_member( &self, group_id: &GroupId, invitee_user_id: &str, member_key_package: &[u8], ) -> Result<(WelcomeMessage, EncryptedMessage)>

Adds a member to a group.

Returns a tuple of (WelcomeMessage, EncryptedMessage) where the WelcomeMessage should be sent to the invitee and the EncryptedMessage (Commit) should be distributed to all existing group members so they can advance their MLS epoch.

invitee_user_id binds the package to the identity it is being added under. This path previously ran neither check — not even the credential-identity one import_key_package has always had — so a package could be admitted to a group under a roster label that had nothing to do with it. The 1:1 path and the group path apply the same three checks.

The third is the validity-window cap, and it belongs here for the reason it belongs anywhere: this is a third route by which a key package this install did not mint is admitted, it takes its bytes straight off the wire out of pending_key_packages, and a bound applied on two routes of three is not a bound. What differs from the 1:1 path is only how long the damage lasts, since a package spent on a commit is not also cached for re-use.

Source

pub fn remove_group_member( &self, group_id: &GroupId, member_id: &str, ) -> Result<EncryptedMessage>

Removes a member from a group.

Removes every leaf whose credential names member_id, in one commit, rather than the first one found.

Two leaves cannot name one identity in a tree whose leaves all passed the binding, and the reason is worth writing down because it is not obvious: RFC 9420 requires signature keys to be unique across a group (OpenMLS enforces it — “Duplicate signature key in proposals and group”), and since verify_leaf_binding ties a credential to the hash of its own signature key, two leaves with the same credential would have to carry the same key. So through the wire gates the duplicate case is refused one layer down, twice over.

That argument covers the gates, not the tree. It says nothing about the case verify_sender_leaf and the get_group_info filter exist for — a leaf written straight into the install-scoped provider store. A forged leaf claiming a peer’s address carries the attacker’s signature key, so it violates no uniqueness rule and sits alongside that peer’s real leaf quite happily. There the loop is doing real work: first-match would remove one of the two and leave the other in the group holding live keys, while every roster read shows the member gone — an access-revocation failure that would surface as nothing at all.

Removing every match costs one Vec and cannot have that failure mode.

Source

pub fn leave_group(&self, group_id: &GroupId) -> Result<()>

Leaves a group.

Source

pub fn encrypt_for_group( &self, group_id: &GroupId, plaintext: &[u8], ) -> Result<EncryptedMessage>

Encrypts a message for a group.

Source

pub fn decrypt_from_group( &self, encrypted: &EncryptedMessage, claimed_sender: &str, ) -> Result<Option<Vec<u8>>>

Decrypts a message from a group.

claimed_sender is the transport-level sender this message will be attributed to; decryption fails with MlsError::SenderIdentityMismatch if it does not match the MLS-authenticated credential (SEC-M1). The check runs before any commit is merged, so a spoofed commit cannot advance group state.

Source

pub fn join_group(&self, welcome: &WelcomeMessage) -> Result<GroupInfo>

Joins a group using a Welcome message.

Source

pub fn list_groups(&self) -> Result<Vec<GroupId>>

Lists all groups.

Source

pub fn get_group_info(&self, group_id: &GroupId) -> Result<Option<GroupInfo>>

Gets information about a group.

Source

pub fn has_group(&self, group_id: &GroupId) -> Result<bool>

Returns true if local MLS group state exists for group_id.

This is the authoritative test for “do we actually participate in this group via MLS” — it gates on the stored group marker, not the member send-cache (which relay reconciliation can populate without any MLS state). Callers use it to distinguish a genuine legacy relay-only (unencrypted) group, which has no MLS state, from an unauthenticated plaintext frame spoofed against a group the node secures with MLS.

Source

pub fn set_group_name(&self, group_id: &GroupId, name: &str) -> Result<()>

Updates the group name.

Source

pub fn get_group_metadata( &self, group_id: &GroupId, ) -> Result<Option<GroupMetadata>>

Gets group metadata.

Source

pub fn set_group_custom_metadata( &self, group_id: &GroupId, key: &str, value: &str, ) -> Result<()>

Sets custom metadata for a group.

Source

pub fn remove_group_custom_metadata( &self, group_id: &GroupId, key: &str, ) -> Result<()>

Removes a custom metadata key for a group.

Source

pub fn set_member_role( &self, group_id: &GroupId, user_id: &str, role: GroupRole, ) -> Result<()>

Sets a member’s role in a group.

Source

pub fn set_enforce_admin_commits(&mut self, enforce: bool)

Enables or disables receive-side authorization of group membership commits. See crate::group::GroupManager::set_enforce_admin_commits for the fork consequence — this is off by default and enabling it is a deployment decision, not a hardening toggle.

Applies to group commits only. The session manager owns a separate group manager and is left untouched, so 1:1 sessions are unaffected regardless of this setting.

Source

pub fn set_group_creator( &self, group_id: &GroupId, creator_id: &str, ) -> Result<()>

Records the group’s creator, only if none is on record yet.

created_by is the fallback GroupMetadata consults when no admin role is stored (see the protocol layer’s check_is_admin), so it must behave monotonically: a device that created the group, or already adopted a creator from the Welcome that admitted it, keeps what it has. Only the “no information” state is fillable. That makes the write idempotent under duplicate Welcomes and stops a later invite — from an inviter whose own metadata disagrees — from rewriting an established admin fallback.

Returns Ok(()) whether or not the value was adopted; use Self::get_group_metadata to observe the result.

Source

pub fn remove_member_role( &self, group_id: &GroupId, user_id: &str, ) -> Result<()>

Removes a member’s role metadata from a group.

Source

pub fn decrypt( &self, encrypted: &EncryptedMessage, claimed_sender: &str, ) -> Result<Option<Vec<u8>>>

Decrypts any incoming encrypted message.

claimed_sender is the transport-level sender this message will be attributed to; it must match the MLS-authenticated credential (SEC-M1).

Source

pub fn process_welcome(&self, welcome: &WelcomeMessage) -> Result<GroupInfo>

Processes a Welcome message.

Source§

impl MlsManager

Source

pub fn update_keys(&self, group_id: &GroupId) -> Result<EncryptedMessage>

Updates the cryptographic keys for a group (triggers MLS self-update).

This provides post-compromise security by rotating keys. The returned commit message must be sent to all other group members.

§Returns

Returns the commit message that must be distributed to all group members.

Source

pub fn ensure_min_key_packages(&self, min: usize) -> Result<usize>

Ensures at least min valid key packages are available.

Generates new key packages if the current count is below the minimum. This is useful for offline scenarios where multiple key packages should be pre-generated for distribution.

§Arguments
  • min - Minimum number of key packages to maintain, capped at MAX_PUSH_KEY_PACKAGES. The packages this mints are unclaimed, so Self::take_push_key_package draws them down one per peer; asking for more than the pool ceiling would only mint key material that ceiling stops it ever handing out, while holding the pool at capacity so every peer past the last claim is advertised a shared package.

Only packages the push path can hand out count toward min. A slot package or one the application marked synced is held by somebody else and never advertised to a peer, so counting it would leave the pool short by exactly that many.

§Returns

Returns the number of live packages the push path can hand out after ensuring the minimum.

Source

pub fn count_valid_key_packages(&self) -> Result<usize>

Returns the number of live key package records: unexpired, init key resident.

This includes slot packages and packages the application marked synced, which are never handed to a peer. For the pool the push path draws from, see Self::ensure_min_key_packages.

Source

pub fn get_identity_public_key(&self) -> Result<Vec<u8>>

Returns the identity public key as raw bytes.

This is the Ed25519 public key used for MLS operations. It can be shared with others to establish your identity and verify signatures.

Source

pub fn sign_data(&self, data: &[u8]) -> Result<Vec<u8>>

Signs arbitrary data with the identity private key.

Uses Ed25519 signatures (the same algorithm used for MLS operations). The signature can be verified by anyone with the corresponding public key.

§Arguments
  • data - The data to sign
§Returns

Returns the signature as raw bytes.

Source

pub fn verify_signature( public_key: &[u8], data: &[u8], signature: &[u8], ) -> Result<bool>

Verifies a signature against a public key.

§Arguments
  • public_key - The Ed25519 public key bytes (32 bytes)
  • data - The original data that was signed
  • signature - The signature to verify (64 bytes)
§Returns

Returns true if the signature is valid, false otherwise.

Source

pub fn derive_address(public_key: &[u8]) -> Result<Address>

Derives the canonical self-certifying address of an Ed25519 identity key: off1…, the bech32m encoding of 0x01 ‖ SHA-256(public_key)[..20].

This is the only address derivation in the SDK. It is implemented once, in offline_protocol_sealed::derive_address, where a bare-metal leaf node can reach it as well; this method, the derive_address FFI function the bridges and apps call, and the interop harness are all delegations to that one function, so every platform agrees byte for byte.

§Arguments
  • public_key - The Ed25519 public key bytes (exactly 32)
§Errors

Returns MlsError::InvalidPublicKey if public_key is not 32 bytes. The length is part of the format contract: hashing a differently-sized input would yield a different address for the same identity. The bytes are deliberately not checked against the curve. An address is defined over key bytes, and what proves ownership is the signature verification that accompanies it (see Self::verify_signature), so binding the address format to a signature library’s parsing strictness would only risk the derivation drifting between versions.

Source

pub fn derive_user_id_from_public_key(public_key: &[u8]) -> String

👎Deprecated:

use derive_address; this returns the self-certifying off1… address, no longer base58

Derives a deterministic user ID from a public key.

§Arguments
  • public_key - The Ed25519 public key bytes
§Returns

Returns the derived address in its canonical off1… string form. Unlike Self::derive_address this accepts any input length, which It is also the one place that still spells the derivation out instead of delegating to offline_protocol_sealed::derive_address, because it deliberately does not do the length check that function starts with. deprecated_derive_agrees_with_derive_address pins the two together for the inputs where both are defined; the copy goes away with the method at the next breaking release. is precisely why it is deprecated: a caller passing a truncated or padded key gets a plausible-looking address for an identity that cannot exist.

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> Classify for T

Source§

type Classified = T

Source§

fn classify(self) -> T

Source§

impl<T> Declassify for T

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> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. 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, !>

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<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

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