use std::collections::HashMap;
use openmls::prelude::{CredentialWithKey, SignaturePublicKey};
use openmls::{group::MlsGroup, prelude::Credential};
use openmls_traits::{types::SignatureScheme, OpenMlsCryptoProvider};
use core_crypto_keystore::CryptoKeystoreMls;
use mls_crypto_provider::MlsCryptoProvider;
use config::MlsConversationConfiguration;
use crate::{
group_store::GroupStoreValue,
mls::{client::Client, MlsCentral},
prelude::{CryptoError, CryptoResult, MlsCiphersuite, MlsCredentialType, MlsError},
};
mod buffer_messages;
pub(crate) mod commit;
mod commit_delay;
pub(crate) mod config;
#[cfg(test)]
mod db_count;
pub mod decrypt;
mod duplicate;
#[cfg(test)]
mod durability;
pub(crate) mod encrypt;
pub(crate) mod export;
pub(crate) mod external_sender;
pub(crate) mod group_info;
mod leaf_node_validation;
pub(crate) mod merge;
mod orphan_welcome;
mod own_commit;
pub(crate) mod proposal;
mod renew;
pub(crate) mod welcome;
mod wipe;
pub type ConversationId = Vec<u8>;
#[derive(Debug)]
#[allow(dead_code)]
pub struct MlsConversation {
pub(crate) id: ConversationId,
pub(crate) parent_id: Option<ConversationId>,
pub(crate) group: MlsGroup,
configuration: MlsConversationConfiguration,
}
impl MlsConversation {
pub async fn create(
id: ConversationId,
author_client: &mut Client,
creator_credential_type: MlsCredentialType,
configuration: MlsConversationConfiguration,
backend: &MlsCryptoProvider,
) -> CryptoResult<Self> {
let (cs, ct) = (configuration.ciphersuite, creator_credential_type);
let cb = author_client
.get_most_recent_or_create_credential_bundle(backend, cs.signature_algorithm(), ct)
.await?;
let group = MlsGroup::new_with_group_id(
backend,
&cb.signature_key,
&configuration.as_openmls_default_configuration()?,
openmls::prelude::GroupId::from_slice(id.as_slice()),
cb.to_mls_credential_with_key(),
)
.await
.map_err(MlsError::from)?;
let mut conversation = Self {
id,
group,
parent_id: None,
configuration,
};
conversation.persist_group_when_changed(backend, true).await?;
Ok(conversation)
}
pub(crate) async fn from_mls_group(
group: MlsGroup,
configuration: MlsConversationConfiguration,
backend: &MlsCryptoProvider,
) -> CryptoResult<Self> {
let id = ConversationId::from(group.group_id().as_slice());
let mut conversation = Self {
id,
group,
configuration,
parent_id: None,
};
conversation.persist_group_when_changed(backend, true).await?;
Ok(conversation)
}
pub(crate) fn from_serialized_state(buf: Vec<u8>, parent_id: Option<ConversationId>) -> CryptoResult<Self> {
let group: MlsGroup = core_crypto_keystore::deser(&buf)?;
let id = ConversationId::from(group.group_id().as_slice());
let configuration = MlsConversationConfiguration {
ciphersuite: group.ciphersuite().into(),
..Default::default()
};
Ok(Self {
id,
group,
parent_id,
configuration,
})
}
pub fn id(&self) -> &ConversationId {
&self.id
}
pub fn members(&self) -> HashMap<Vec<u8>, Credential> {
self.group.members().fold(HashMap::new(), |mut acc, kp| {
let credential = kp.credential;
let id = credential.identity().to_vec();
acc.entry(id).or_insert(credential);
acc
})
}
pub fn members_with_key(&self) -> HashMap<Vec<u8>, CredentialWithKey> {
self.group.members().fold(HashMap::new(), |mut acc, kp| {
let credential = kp.credential;
let id = credential.identity().to_vec();
let signature_key = SignaturePublicKey::from(kp.signature_key);
let credential = CredentialWithKey {
credential,
signature_key,
};
acc.entry(id).or_insert(credential);
acc
})
}
pub(crate) async fn persist_group_when_changed(
&mut self,
backend: &MlsCryptoProvider,
force: bool,
) -> CryptoResult<()> {
if force || self.group.state_changed() == openmls::group::InnerState::Changed {
use core_crypto_keystore::CryptoKeystoreMls as _;
backend
.key_store()
.mls_group_persist(
&self.id,
&core_crypto_keystore::ser(&self.group)?,
self.parent_id.as_deref(),
)
.await?;
self.group.set_state(openmls::group::InnerState::Persisted);
}
Ok(())
}
pub async fn mark_as_child_of(
&mut self,
parent_id: &ConversationId,
backend: &MlsCryptoProvider,
) -> CryptoResult<()> {
if backend.key_store().mls_group_exists(parent_id).await {
self.parent_id = Some(parent_id.clone());
self.persist_group_when_changed(backend, true).await?;
Ok(())
} else {
Err(CryptoError::ParentGroupNotFound)
}
}
pub(crate) fn own_credential_type(&self) -> CryptoResult<MlsCredentialType> {
Ok(self
.group
.own_leaf_node()
.ok_or(CryptoError::InternalMlsError)?
.credential()
.credential_type()
.into())
}
pub(crate) fn ciphersuite(&self) -> MlsCiphersuite {
self.configuration.ciphersuite
}
pub(crate) fn signature_scheme(&self) -> SignatureScheme {
self.ciphersuite().signature_algorithm()
}
}
impl MlsCentral {
pub(crate) async fn get_conversation(
&mut self,
id: &ConversationId,
) -> CryptoResult<crate::group_store::GroupStoreValue<MlsConversation>> {
let keystore = self.mls_backend.borrow_keystore_mut();
self.mls_groups
.get_fetch(id, keystore, None)
.await?
.ok_or_else(|| CryptoError::ConversationNotFound(id.clone()))
}
pub(crate) async fn get_parent_conversation(
&mut self,
conversation: &GroupStoreValue<MlsConversation>,
) -> CryptoResult<Option<crate::group_store::GroupStoreValue<MlsConversation>>> {
let conversation_lock = conversation.read().await;
if let Some(parent_id) = conversation_lock.parent_id.as_ref() {
Ok(Some(
self.get_conversation(parent_id)
.await
.map_err(|_| CryptoError::ParentGroupNotFound)?,
))
} else {
Ok(None)
}
}
pub(crate) async fn get_all_conversations(
&mut self,
) -> CryptoResult<Vec<crate::group_store::GroupStoreValue<MlsConversation>>> {
let keystore = self.mls_backend.borrow_keystore_mut();
self.mls_groups.get_fetch_all(keystore).await
}
#[cfg_attr(test, crate::idempotent)]
pub async fn mark_conversation_as_child_of(
&mut self,
child_id: &ConversationId,
parent_id: &ConversationId,
) -> CryptoResult<()> {
let conversation = self.get_conversation(child_id).await?;
conversation
.write()
.await
.mark_as_child_of(parent_id, &self.mls_backend)
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::e2e_identity::rotate::tests::all::failsafe_ctx;
use wasm_bindgen_test::*;
use crate::{
prelude::{
ClientIdentifier, MlsCentralConfiguration, MlsConversationCreationMessage, INITIAL_KEYING_MATERIAL_COUNT,
},
test_utils::*,
};
use super::*;
wasm_bindgen_test_configure!(run_in_browser);
#[apply(all_cred_cipher)]
#[wasm_bindgen_test]
pub async fn create_self_conversation_should_succeed(case: TestCase) {
run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
Box::pin(async move {
let id = conversation_id();
alice_central
.mls_central
.new_conversation(&id, case.credential_type, case.cfg.clone())
.await
.unwrap();
assert_eq!(alice_central.mls_central.get_conversation_unchecked(&id).await.id, id);
assert_eq!(
alice_central
.mls_central
.get_conversation_unchecked(&id)
.await
.group
.group_id()
.as_slice(),
id
);
assert_eq!(
alice_central
.mls_central
.get_conversation_unchecked(&id)
.await
.members()
.len(),
1
);
let alice_can_send_message = alice_central.mls_central.encrypt_message(&id, b"me").await;
assert!(alice_can_send_message.is_ok());
})
})
.await;
}
#[apply(all_cred_cipher)]
#[wasm_bindgen_test]
pub async fn create_1_1_conversation_should_succeed(case: TestCase) {
run_test_with_client_ids(
case.clone(),
["alice", "bob"],
move |[mut alice_central, mut bob_central]| {
Box::pin(async move {
let id = conversation_id();
alice_central
.mls_central
.new_conversation(&id, case.credential_type, case.cfg.clone())
.await
.unwrap();
let bob = bob_central.mls_central.rand_key_package(&case).await;
let MlsConversationCreationMessage { welcome, .. } = alice_central
.mls_central
.add_members_to_conversation(&id, vec![bob])
.await
.unwrap();
assert_eq!(
alice_central
.mls_central
.get_conversation_unchecked(&id)
.await
.members()
.len(),
1
);
alice_central.mls_central.commit_accepted(&id).await.unwrap();
assert_eq!(alice_central.mls_central.get_conversation_unchecked(&id).await.id, id);
assert_eq!(
alice_central
.mls_central
.get_conversation_unchecked(&id)
.await
.group
.group_id()
.as_slice(),
id
);
assert_eq!(
alice_central
.mls_central
.get_conversation_unchecked(&id)
.await
.members()
.len(),
2
);
bob_central
.mls_central
.process_welcome_message(welcome.into(), case.custom_cfg())
.await
.unwrap();
assert_eq!(
bob_central.mls_central.get_conversation_unchecked(&id).await.id(),
alice_central.mls_central.get_conversation_unchecked(&id).await.id()
);
assert!(alice_central
.mls_central
.try_talk_to(&id, &mut bob_central.mls_central)
.await
.is_ok());
})
},
)
.await;
}
#[apply(all_cred_cipher)]
#[wasm_bindgen_test]
pub async fn create_many_people_conversation(case: TestCase) {
run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
Box::pin(async move {
let x509_test_chain_arc = failsafe_ctx(&mut [&mut alice_central], case.signature_scheme()).await;
let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
let id = conversation_id();
alice_central
.mls_central
.new_conversation(&id, case.credential_type, case.cfg.clone())
.await
.unwrap();
let mut bob_and_friends = Vec::with_capacity(GROUP_SAMPLE_SIZE);
for _ in 0..GROUP_SAMPLE_SIZE {
let uuid = uuid::Uuid::new_v4();
let name = uuid.hyphenated().to_string();
let path = tmp_db_file();
let config = MlsCentralConfiguration::try_new(
path.0,
name.clone(),
None,
vec![case.ciphersuite()],
None,
Some(INITIAL_KEYING_MATERIAL_COUNT),
)
.unwrap();
let mut central = MlsCentral::try_new(config).await.unwrap();
x509_test_chain.register_with_central(¢ral).await;
let client_id: crate::prelude::ClientId = name.as_str().into();
let identity = match case.credential_type {
MlsCredentialType::Basic => ClientIdentifier::Basic(client_id),
MlsCredentialType::X509 => {
let x509_test_chain = alice_central
.x509_test_chain
.as_ref()
.as_ref()
.expect("No x509 test chain");
let cert = crate::prelude::CertificateBundle::rand(
&client_id,
x509_test_chain.find_local_intermediate_ca(),
);
ClientIdentifier::X509(HashMap::from([(case.cfg.ciphersuite.signature_algorithm(), cert)]))
}
};
central
.mls_init(
identity,
vec![case.cfg.ciphersuite],
Some(INITIAL_KEYING_MATERIAL_COUNT),
)
.await
.unwrap();
bob_and_friends.push(central);
}
let number_of_friends = bob_and_friends.len();
let mut bob_and_friends_kps = vec![];
for c in &bob_and_friends {
bob_and_friends_kps.push(c.rand_key_package(&case).await);
}
let MlsConversationCreationMessage { welcome, .. } = alice_central
.mls_central
.add_members_to_conversation(&id, bob_and_friends_kps)
.await
.unwrap();
assert_eq!(
alice_central
.mls_central
.get_conversation_unchecked(&id)
.await
.members()
.len(),
1
);
alice_central.mls_central.commit_accepted(&id).await.unwrap();
assert_eq!(alice_central.mls_central.get_conversation_unchecked(&id).await.id, id);
assert_eq!(
alice_central
.mls_central
.get_conversation_unchecked(&id)
.await
.group
.group_id()
.as_slice(),
id
);
assert_eq!(
alice_central
.mls_central
.get_conversation_unchecked(&id)
.await
.members()
.len(),
1 + number_of_friends
);
let mut bob_and_friends_groups = Vec::with_capacity(bob_and_friends.len());
for mut c in bob_and_friends {
c.process_welcome_message(welcome.clone().into(), case.custom_cfg())
.await
.unwrap();
assert!(c.try_talk_to(&id, &mut alice_central.mls_central).await.is_ok());
bob_and_friends_groups.push(c);
}
assert_eq!(bob_and_friends_groups.len(), GROUP_SAMPLE_SIZE);
})
})
.await;
}
}