Skip to main content

de_mls/mls_crypto/service/
openmls.rs

1//! [`OpenMlsService`] — the OpenMLS-backed
2//! [`MlsService`](crate::mls_crypto::MlsService) impl, plus its
3//! `new_as_creator` / `new_from_welcome` constructors and the
4//! conversation-free [`OpenMlsService::generate_key_package`] used by
5//! joiners before any MLS state exists.
6
7use std::sync::Arc;
8
9use openmls::{
10    group::{
11        GroupId, MlsGroup, MlsGroupCreateConfig, MlsGroupJoinConfig, StagedCommit, StagedWelcome,
12    },
13    key_packages::KeyPackage,
14    prelude::{DeserializeBytes, MlsMessageBodyIn, MlsMessageIn},
15};
16use openmls_rust_crypto::RustCrypto;
17use openmls_traits::OpenMlsProvider;
18
19use crate::mls_crypto::{
20    DeMlsStorage, KeyPackageBytes, MlsCredentials, MlsError,
21    service::{CIPHERSUITE, backend::MlsProvider},
22};
23
24/// OpenMLS-backed MLS service, scoped to a single conversation. Owns
25/// one `MlsGroup` plus an optional staged-commit slot for the inbound
26/// stage→merge/discard pipeline. Credentials are `Arc<MlsCredentials>`
27/// so one user's keypair backs every per-conversation service.
28pub struct OpenMlsService<S: DeMlsStorage> {
29    pub(super) storage: S,
30    pub(super) crypto: RustCrypto,
31    pub(super) credentials: Arc<MlsCredentials>,
32    pub(super) conversation_id: String,
33    pub(super) group: MlsGroup,
34    pub(super) pending_staged_commit: Option<StagedCommit>,
35}
36
37impl<S: DeMlsStorage> OpenMlsService<S> {
38    /// Create a fresh MLS group as the sole initial member ("creator").
39    pub fn new_as_creator(
40        conversation_id: String,
41        storage: S,
42        credentials: Arc<MlsCredentials>,
43    ) -> Result<Self, MlsError> {
44        let crypto = RustCrypto::default();
45        let group = {
46            let provider = MlsProvider::new(&crypto, storage.mls_storage());
47            let config = MlsGroupCreateConfig::builder()
48                .use_ratchet_tree_extension(true)
49                .build();
50            MlsGroup::new_with_group_id(
51                &provider,
52                credentials.signer(),
53                &config,
54                GroupId::from_slice(conversation_id.as_bytes()),
55                credentials.credential().clone(),
56            )?
57        };
58
59        Ok(Self {
60            storage,
61            crypto,
62            credentials,
63            conversation_id,
64            group,
65            pending_staged_commit: None,
66        })
67    }
68
69    /// Try to join a group from a serialized welcome.
70    ///
71    /// Returns `Ok(None)` when the welcome doesn't address one of our key
72    /// packages — that's the "not for us" branch, not an error. On
73    /// `Ok(Some(svc))` the caller has a fully initialized service for the
74    /// group the welcome described.
75    pub fn new_from_welcome(
76        welcome_bytes: &[u8],
77        storage: S,
78        credentials: Arc<MlsCredentials>,
79    ) -> Result<Option<Self>, MlsError> {
80        let crypto = RustCrypto::default();
81
82        let (mls_message, _) = MlsMessageIn::tls_deserialize_bytes(welcome_bytes)?;
83        let welcome = match mls_message.extract() {
84            MlsMessageBodyIn::Welcome(w) => w,
85            _ => return Ok(None),
86        };
87
88        let is_for_us = welcome.secrets().iter().any(|s| {
89            storage
90                .is_our_key_package(s.new_member().as_slice())
91                .unwrap_or(false)
92        });
93        if !is_for_us {
94            return Ok(None);
95        }
96
97        for secret in welcome.secrets() {
98            storage.remove_key_package_ref(secret.new_member().as_slice())?;
99        }
100
101        let group = {
102            let provider = MlsProvider::new(&crypto, storage.mls_storage());
103            let config = MlsGroupJoinConfig::builder()
104                .use_ratchet_tree_extension(true)
105                .build();
106            StagedWelcome::new_from_welcome(&provider, &config, welcome, None)?
107                .into_group(&provider)?
108        };
109
110        let conversation_id = String::from_utf8_lossy(group.group_id().as_slice()).to_string();
111        Ok(Some(Self {
112            storage,
113            crypto,
114            credentials,
115            conversation_id,
116            group,
117            pending_staged_commit: None,
118        }))
119    }
120
121    /// Generate a single-use key package for `credentials` backed by `storage`.
122    ///
123    /// Takes only storage + credentials rather than `&self`, so a joiner
124    /// can publish a key package before any MLS group has been created.
125    /// The resulting hash ref is registered in `storage` so a later
126    /// `new_from_welcome` can identify the welcome as "for us".
127    pub fn generate_key_package(
128        storage: &S,
129        credentials: &MlsCredentials,
130    ) -> Result<KeyPackageBytes, MlsError> {
131        let crypto = RustCrypto::default();
132        let provider = MlsProvider::new(&crypto, storage.mls_storage());
133
134        let kp_bundle = KeyPackage::builder().build(
135            CIPHERSUITE,
136            &provider,
137            credentials.signer(),
138            credentials.credential().clone(),
139        )?;
140
141        let kp = kp_bundle.key_package();
142        let hash_ref = kp.hash_ref(provider.crypto())?.as_slice().to_vec();
143        let bytes = serde_json::to_vec(kp).map_err(MlsError::InvalidJson)?;
144
145        storage.store_key_package_ref(&hash_ref)?;
146
147        let identity_bytes = credentials
148            .credential()
149            .credential
150            .serialized_content()
151            .to_vec();
152
153        Ok(KeyPackageBytes::new(bytes, identity_bytes))
154    }
155}