xmtp_mls/groups/
builders.rs1use super::*;
4use openmls::prelude::SenderRatchetConfiguration;
5use xmtp_configuration::{
6 SENDER_RATCHET_MAX_FORWARD_DISTANCE, SENDER_RATCHET_OUT_OF_ORDER_TOLERANCE,
7};
8use xmtp_mls_group_state::app_data::dm_registry_profile::{
9 dm_registry_profiles, matches_dm_registry_profile,
10};
11use xmtp_mls_validation::commit::{extract_group_membership, inbox_id_from_credential};
12
13pub(crate) fn parse_canonical_dm_id(dm_id: Option<&str>) -> Result<DmMembers<String>, GroupError> {
15 let invalid = || {
16 GroupError::from(MetadataPermissionsError::from(
17 DmValidationError::StoredDmIdMismatch,
18 ))
19 };
20 let mut parts = dm_id.ok_or_else(invalid)?.split(':');
21 if parts.next() != Some("dm") {
22 return Err(invalid());
23 }
24 let first = parts.next().ok_or_else(invalid)?;
25 let second = parts.next().ok_or_else(invalid)?;
26 if parts.next().is_some() || first == second {
27 return Err(invalid());
28 }
29 for inbox_id in [first, second] {
30 let bytes = hex::decode(inbox_id).map_err(|_| invalid())?;
31 if bytes.len() != 32 || hex::encode(bytes) != inbox_id {
32 return Err(invalid());
33 }
34 }
35 let members = DmMembers {
36 member_one_inbox_id: first.to_string(),
37 member_two_inbox_id: second.to_string(),
38 };
39 if members.to_string() != dm_id.unwrap_or_default() {
40 return Err(invalid());
41 }
42 Ok(members)
43}
44
45pub fn build_starting_group_membership_extension(inbox_id: &str, sequence_id: u64) -> Extension {
46 let mut group_membership = GroupMembership::new();
47 group_membership.add(inbox_id.to_string(), sequence_id);
48 build_group_membership_extension(&group_membership)
49}
50
51pub fn build_group_membership_extension(group_membership: &GroupMembership) -> Extension {
52 let unknown_gc_extension = UnknownExtension(group_membership.into());
53
54 Extension::Unknown(GROUP_MEMBERSHIP_EXTENSION_ID, unknown_gc_extension)
55}
56
57pub(crate) fn build_group_config(
59 dictionary: openmls::extensions::AppDataDictionary,
60) -> Result<MlsGroupCreateConfig, GroupError> {
61 let required_extension_types = &[
64 ExtensionType::AppDataDictionary,
65 ExtensionType::ApplicationId,
66 ];
67 let required_proposal_types = &[ProposalType::AppDataUpdate];
68 let mut supported_extensions = required_extension_types.to_vec();
69 supported_extensions.extend([
70 ExtensionType::Unknown(WELCOME_WRAPPER_ENCRYPTION_EXTENSION_ID),
71 ExtensionType::Unknown(WELCOME_POINTEE_ENCRYPTION_AEAD_TYPES_EXTENSION_ID),
72 ]);
73 let capabilities = Capabilities::new(
74 None,
75 None,
76 Some(&supported_extensions),
77 Some(required_proposal_types),
78 None,
79 );
80 let extensions = Extensions::from_vec(vec![
81 Extension::AppDataDictionary(openmls::extensions::AppDataDictionaryExtension::new(
82 dictionary,
83 )),
84 Extension::RequiredCapabilities(RequiredCapabilitiesExtension::new(
85 required_extension_types,
86 required_proposal_types,
87 &[CredentialType::Basic],
88 )),
89 ])?;
90 Ok(MlsGroupCreateConfig::builder()
91 .with_group_context_extensions(extensions)
92 .capabilities(capabilities)
93 .ciphersuite(CIPHERSUITE)
94 .wire_format_policy(WireFormatPolicy::default())
95 .max_past_epochs(MAX_PAST_EPOCHS)
96 .sender_ratchet_configuration(SenderRatchetConfiguration::new(
97 SENDER_RATCHET_OUT_OF_ORDER_TOLERANCE,
98 SENDER_RATCHET_MAX_FORWARD_DISTANCE,
99 ))
100 .use_ratchet_tree_extension(true)
101 .build())
102}
103
104pub fn filter_inbox_ids_needing_updates<'a>(
105 conn: &impl DbQuery,
106 filters: &[(&'a str, i64)],
107) -> Result<Vec<&'a str>, xmtp_db::ConnectionError> {
108 let existing_sequence_ids =
109 conn.get_latest_sequence_id(&filters.iter().map(|f| f.0).collect::<Vec<&str>>())?;
110
111 let needs_update = filters
112 .iter()
113 .filter_map(|&(inbox_id, seq)| {
114 let existing_sequence_id = existing_sequence_ids.get(inbox_id);
115 if existing_sequence_id.is_some_and(|&s| s >= seq) {
116 return None;
117 }
118
119 Some(inbox_id)
120 })
121 .collect();
122 Ok(needs_update)
123}
124
125pub(in crate::groups) fn validate_dm_group(
127 context: impl XmtpSharedContext,
128 mls_group: &OpenMlsGroup,
129 added_by_inbox: &str,
130) -> Result<(), MetadataPermissionsError> {
131 let metadata = extract_group_metadata(mls_group.extensions())?;
133
134 if metadata.conversation_type != ConversationType::Dm {
136 return Err(DmValidationError::InvalidConversationType.into());
137 }
138
139 let dm_members = match &metadata.dm_members {
141 Some(dm) => dm,
142 None => {
143 return Err(DmValidationError::MustHaveMembersSet.into());
144 }
145 };
146
147 let identity = context.identity();
148 let pair = [
149 dm_members.member_one_inbox_id.as_str(),
150 dm_members.member_two_inbox_id.as_str(),
151 ];
152 if pair[0] == pair[1] || !pair.contains(&identity.inbox_id()) {
153 return Err(DmValidationError::OurInboxMustBeMember.into());
154 }
155 if !pair.contains(&added_by_inbox) {
156 return Err(DmValidationError::ExpectedInboxesDoNotMatch.into());
157 }
158
159 let membership = extract_group_membership(mls_group.extensions())
161 .map_err(|_| DmValidationError::InvalidMembership)?;
162 if membership
163 .members
164 .keys()
165 .any(|inbox| !pair.contains(&inbox.as_str()))
166 {
167 return Err(DmValidationError::MemberOutsidePair.into());
168 }
169 for member in mls_group.members() {
170 let inbox = inbox_id_from_credential(&member.credential)
171 .map_err(|_| DmValidationError::InvalidMemberCredential)?;
172 if !pair.contains(&inbox.as_str()) {
173 return Err(DmValidationError::MemberOutsidePair.into());
174 }
175 }
176
177 let mutable_metadata =
179 xmtp_mls_group_state::app_data::component_source::extract_group_mutable_metadata_capability_aware(mls_group)
180 .map_err(|error| match error {
181 xmtp_mls_group_state::app_data::component_source::ComponentSourceError::GroupMutableMetadata(inner) => {
182 MetadataPermissionsError::Mutable(inner)
183 }
184 other => MetadataPermissionsError::ComponentSource(other),
185 })?;
186
187 if !mutable_metadata.admin_list.is_empty() || !mutable_metadata.super_admin_list.is_empty() {
189 return Err(DmValidationError::MustHaveEmptyAdminAndSuperAdmin.into());
190 }
191
192 let received = app_data::load_component_registry(mls_group)?;
195 if !matches_dm_registry_profile(&received, &dm_registry_profiles()) {
196 return Err(DmValidationError::InvalidPermissions.into());
197 }
198
199 Ok(())
200}