pub struct MlsManager { /* private fields */ }Expand description
Main MLS manager for end-to-end encryption.
Implementations§
Source§impl MlsManager
impl MlsManager
Sourcepub fn new(
user_id: impl Into<String>,
storage: Arc<dyn MlsStorage>,
) -> Result<Self>
pub fn new( user_id: impl Into<String>, storage: Arc<dyn MlsStorage>, ) -> Result<Self>
Creates a new MLS manager.
Sourcepub fn load_or_create_identity(
storage: &Arc<dyn MlsStorage>,
) -> Result<(SignatureKeyPair, Address)>
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.
Sourcepub fn generate_key_package(&self) -> Result<KeyPackageBundle>
pub fn generate_key_package(&self) -> Result<KeyPackageBundle>
Generates a new key package for distribution over the push path.
Sourcepub fn generate_publication_key_package(&self) -> Result<KeyPackageBundle>
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.
Sourcepub fn get_or_create_key_package(&self) -> Result<KeyPackageBundle>
pub fn get_or_create_key_package(&self) -> Result<KeyPackageBundle>
Gets an existing unclaimed key package or generates a new one.
Two 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 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.
Sourcepub fn take_push_key_package(&self, peer_id: &str) -> Result<PushKeyPackage>
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:
- this peer’s own live package — a repeat push costs no new material;
- an unclaimed package (minted by the peer-less entry point, or by a build predating assignment), claimed here so upgrading does not strand it;
- 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.
Sourcepub fn key_package_by_id(
&self,
package_id: &str,
) -> Result<Option<KeyPackageBundle>>
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.
Sourcepub fn delete_key_package(&self, package_id: &str) -> Result<()>
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.
Sourcepub fn import_key_package(
&self,
user_id: &str,
key_package_data: &[u8],
) -> Result<()>
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.
Sourcepub fn get_pending_key_packages(&self) -> Result<Vec<KeyPackageBundle>>
pub fn get_pending_key_packages(&self) -> Result<Vec<KeyPackageBundle>>
Gets pending key packages.
Sourcepub fn mark_key_package_synced(&self, package_id: &str) -> Result<()>
pub fn mark_key_package_synced(&self, package_id: &str) -> Result<()>
Marks a key package as synced.
Sourcepub fn has_session(&self, other_user_id: &str) -> Result<bool>
pub fn has_session(&self, other_user_id: &str) -> Result<bool>
Checks if a session exists with another user.
Sourcepub fn create_session(&self, other_user_id: &str) -> Result<WelcomeMessage>
pub fn create_session(&self, other_user_id: &str) -> Result<WelcomeMessage>
Creates a new 1:1 session with another user.
Sourcepub fn join_session(&self, welcome: &WelcomeMessage) -> Result<GroupInfo>
pub fn join_session(&self, welcome: &WelcomeMessage) -> Result<GroupInfo>
Joins a session using a Welcome message.
Sourcepub fn replace_session_with_welcome(
&self,
welcome: &WelcomeMessage,
) -> Result<GroupInfo>
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.
Sourcepub fn encrypt_for_user(
&self,
other_user_id: &str,
plaintext: &[u8],
) -> Result<EncryptedMessage>
pub fn encrypt_for_user( &self, other_user_id: &str, plaintext: &[u8], ) -> Result<EncryptedMessage>
Encrypts a message for a 1:1 session.
Sourcepub fn encrypt_for_existing_session(
&self,
other_user_id: &str,
plaintext: &[u8],
) -> Result<EncryptedMessage>
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.
Sourcepub fn get_pending_welcome(
&self,
other_user_id: &str,
) -> Result<Option<WelcomeMessage>>
pub fn get_pending_welcome( &self, other_user_id: &str, ) -> Result<Option<WelcomeMessage>>
Gets a pending Welcome message.
Sourcepub fn clear_pending_welcome(&self, other_user_id: &str) -> Result<()>
pub fn clear_pending_welcome(&self, other_user_id: &str) -> Result<()>
Clears a pending Welcome message.
Sourcepub fn decrypt_from_user(
&self,
encrypted: &EncryptedMessage,
claimed_sender: &str,
) -> Result<Option<Vec<u8>>>
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).
Sourcepub fn list_sessions(&self) -> Result<Vec<String>>
pub fn list_sessions(&self) -> Result<Vec<String>>
Lists all active 1:1 sessions.
Sourcepub fn delete_session(&self, other_user_id: &str) -> Result<()>
pub fn delete_session(&self, other_user_id: &str) -> Result<()>
Deletes a 1:1 session.
Sourcepub fn create_group(&self, group_name: &str) -> Result<GroupInfo>
pub fn create_group(&self, group_name: &str) -> Result<GroupInfo>
Creates a new group.
Sourcepub fn add_group_member(
&self,
group_id: &GroupId,
invitee_user_id: &str,
member_key_package: &[u8],
) -> Result<(WelcomeMessage, EncryptedMessage)>
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
two checks.
Sourcepub fn remove_group_member(
&self,
group_id: &GroupId,
member_id: &str,
) -> Result<EncryptedMessage>
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.
Sourcepub fn leave_group(&self, group_id: &GroupId) -> Result<()>
pub fn leave_group(&self, group_id: &GroupId) -> Result<()>
Leaves a group.
Sourcepub fn encrypt_for_group(
&self,
group_id: &GroupId,
plaintext: &[u8],
) -> Result<EncryptedMessage>
pub fn encrypt_for_group( &self, group_id: &GroupId, plaintext: &[u8], ) -> Result<EncryptedMessage>
Encrypts a message for a group.
Sourcepub fn decrypt_from_group(
&self,
encrypted: &EncryptedMessage,
claimed_sender: &str,
) -> Result<Option<Vec<u8>>>
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.
Sourcepub fn join_group(&self, welcome: &WelcomeMessage) -> Result<GroupInfo>
pub fn join_group(&self, welcome: &WelcomeMessage) -> Result<GroupInfo>
Joins a group using a Welcome message.
Sourcepub fn list_groups(&self) -> Result<Vec<GroupId>>
pub fn list_groups(&self) -> Result<Vec<GroupId>>
Lists all groups.
Sourcepub fn get_group_info(&self, group_id: &GroupId) -> Result<Option<GroupInfo>>
pub fn get_group_info(&self, group_id: &GroupId) -> Result<Option<GroupInfo>>
Gets information about a group.
Sourcepub fn has_group(&self, group_id: &GroupId) -> Result<bool>
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.
Sourcepub fn set_group_name(&self, group_id: &GroupId, name: &str) -> Result<()>
pub fn set_group_name(&self, group_id: &GroupId, name: &str) -> Result<()>
Updates the group name.
Sourcepub fn get_group_metadata(
&self,
group_id: &GroupId,
) -> Result<Option<GroupMetadata>>
pub fn get_group_metadata( &self, group_id: &GroupId, ) -> Result<Option<GroupMetadata>>
Gets group metadata.
Sourcepub fn set_group_custom_metadata(
&self,
group_id: &GroupId,
key: &str,
value: &str,
) -> Result<()>
pub fn set_group_custom_metadata( &self, group_id: &GroupId, key: &str, value: &str, ) -> Result<()>
Sets custom metadata for a group.
Sourcepub fn remove_group_custom_metadata(
&self,
group_id: &GroupId,
key: &str,
) -> Result<()>
pub fn remove_group_custom_metadata( &self, group_id: &GroupId, key: &str, ) -> Result<()>
Removes a custom metadata key for a group.
Sourcepub fn set_member_role(
&self,
group_id: &GroupId,
user_id: &str,
role: GroupRole,
) -> Result<()>
pub fn set_member_role( &self, group_id: &GroupId, user_id: &str, role: GroupRole, ) -> Result<()>
Sets a member’s role in a group.
Sourcepub fn set_enforce_admin_commits(&mut self, enforce: bool)
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.
Sourcepub fn set_group_creator(
&self,
group_id: &GroupId,
creator_id: &str,
) -> Result<()>
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.
Sourcepub fn remove_member_role(
&self,
group_id: &GroupId,
user_id: &str,
) -> Result<()>
pub fn remove_member_role( &self, group_id: &GroupId, user_id: &str, ) -> Result<()>
Removes a member’s role metadata from a group.
Sourcepub fn decrypt(
&self,
encrypted: &EncryptedMessage,
claimed_sender: &str,
) -> Result<Option<Vec<u8>>>
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).
Sourcepub fn process_welcome(&self, welcome: &WelcomeMessage) -> Result<GroupInfo>
pub fn process_welcome(&self, welcome: &WelcomeMessage) -> Result<GroupInfo>
Processes a Welcome message.
Source§impl MlsManager
impl MlsManager
Sourcepub fn update_keys(&self, group_id: &GroupId) -> Result<EncryptedMessage>
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.
Sourcepub fn ensure_min_key_packages(&self, min: usize) -> Result<usize>
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 atMAX_PUSH_KEY_PACKAGES. The packages this mints are unclaimed, soSelf::take_push_key_packagedraws 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.
§Returns
Returns the total number of valid key packages after ensuring minimum.
Sourcepub fn count_valid_key_packages(&self) -> Result<usize>
pub fn count_valid_key_packages(&self) -> Result<usize>
Returns the number of valid (non-expired) key packages available.
Sourcepub fn get_identity_public_key(&self) -> Result<Vec<u8>>
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.
Sourcepub fn derive_address(public_key: &[u8]) -> Result<Address>
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. Bridges and apps reach
it through the derive_address FFI function rather than reimplementing
it, so that 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.
Sourcepub 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
pub fn derive_user_id_from_public_key(public_key: &[u8]) -> String
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
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§
impl !Freeze for MlsManager
impl !RefUnwindSafe for MlsManager
impl !UnwindSafe for MlsManager
impl Send for MlsManager
impl Sync for MlsManager
impl Unpin for MlsManager
impl UnsafeUnpin for MlsManager
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> Declassify for T
impl<T> Declassify for T
type Declassified = T
fn declassify(self) -> 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