1use super::*;
4use xmtp_mls_validation::commit::{extract_group_membership, inbox_id_from_credential};
5
6pub(crate) fn parse_canonical_dm_id(dm_id: Option<&str>) -> Result<DmMembers<String>, GroupError> {
8 let invalid = || {
9 GroupError::from(MetadataPermissionsError::from(
10 DmValidationError::StoredDmIdMismatch,
11 ))
12 };
13 let mut parts = dm_id.ok_or_else(invalid)?.split(':');
14 if parts.next() != Some("dm") {
15 return Err(invalid());
16 }
17 let first = parts.next().ok_or_else(invalid)?;
18 let second = parts.next().ok_or_else(invalid)?;
19 if parts.next().is_some() || first == second {
20 return Err(invalid());
21 }
22 for inbox_id in [first, second] {
23 let bytes = hex::decode(inbox_id).map_err(|_| invalid())?;
24 if bytes.len() != 32 || hex::encode(bytes) != inbox_id {
25 return Err(invalid());
26 }
27 }
28 let members = DmMembers {
29 member_one_inbox_id: first.to_string(),
30 member_two_inbox_id: second.to_string(),
31 };
32 if members.to_string() != dm_id.unwrap_or_default() {
33 return Err(invalid());
34 }
35 Ok(members)
36}
37
38#[cfg(test)]
39pub(crate) fn build_protected_metadata_extension(
40 creator_inbox_id: &str,
41 conversation_type: ConversationType,
42 oneshot_message: Option<OneshotMessage>,
43) -> Result<Extension, MetadataPermissionsError> {
44 assert!(conversation_type != ConversationType::Dm);
45 let metadata = GroupMetadata::new(
46 conversation_type,
47 creator_inbox_id.to_string(),
48 None,
49 oneshot_message,
50 );
51 let protected_metadata = Metadata::new(metadata.try_into()?);
52
53 Ok(Extension::ImmutableMetadata(protected_metadata))
54}
55
56#[cfg(test)]
57pub(in crate::groups) fn build_dm_protected_metadata_extension(
58 creator_inbox_id: &str,
59 dm_inbox_id: InboxId,
60) -> Result<Extension, GroupError> {
61 let dm_members = Some(DmMembers {
62 member_one_inbox_id: creator_inbox_id.to_string(),
63 member_two_inbox_id: dm_inbox_id,
64 });
65
66 let metadata = GroupMetadata::new(
67 ConversationType::Dm,
68 creator_inbox_id.to_string(),
69 dm_members,
70 None,
71 );
72 let protected_metadata = Metadata::new(
73 metadata
74 .try_into()
75 .map_err(MetadataPermissionsError::from)?,
76 );
77
78 Ok(Extension::ImmutableMetadata(protected_metadata))
79}
80
81#[cfg(test)]
82pub(crate) fn build_mutable_permissions_extension(
83 policies: PolicySet,
84) -> Result<Extension, MetadataPermissionsError> {
85 let permissions: Vec<u8> = GroupMutablePermissions::new(policies).try_into()?;
86 let unknown_gc_extension = UnknownExtension(permissions);
87
88 Ok(Extension::Unknown(
89 GROUP_PERMISSIONS_EXTENSION_ID,
90 unknown_gc_extension,
91 ))
92}
93
94pub fn build_mutable_metadata_extension_default(
95 creator_inbox_id: &str,
96 opts: GroupMetadataOptions,
97 commit_log_enabled: bool,
98) -> Result<Extension, GroupError> {
99 let mut commit_log_signer = None;
100 if commit_log_enabled {
102 commit_log_signer = Some(xmtp_cryptography::rand::rand_secret::<ED25519_KEY_LENGTH>());
104 }
105 let mutable_metadata: Vec<u8> =
106 GroupMutableMetadata::new_default(creator_inbox_id.to_string(), commit_log_signer, opts)
107 .try_into()
108 .map_err(MetadataPermissionsError::from)?;
109 let unknown_gc_extension = UnknownExtension(mutable_metadata);
110
111 Ok(Extension::Unknown(
112 MUTABLE_METADATA_EXTENSION_ID,
113 unknown_gc_extension,
114 ))
115}
116
117pub fn build_dm_mutable_metadata_extension_default(
118 creator_inbox_id: &str,
119 dm_target_inbox_id: &str,
120 opts: DMMetadataOptions,
121 commit_log_enabled: bool,
122) -> Result<Extension, MetadataPermissionsError> {
123 let mut commit_log_signer = None;
124 if commit_log_enabled {
126 commit_log_signer = Some(xmtp_cryptography::rand::rand_secret::<ED25519_KEY_LENGTH>());
127 }
128 let mutable_metadata: Vec<u8> = GroupMutableMetadata::new_dm_default(
129 creator_inbox_id.to_string(),
130 dm_target_inbox_id,
131 commit_log_signer,
132 opts,
133 )
134 .try_into()?;
135 let unknown_gc_extension = UnknownExtension(mutable_metadata);
136
137 Ok(Extension::Unknown(
138 MUTABLE_METADATA_EXTENSION_ID,
139 unknown_gc_extension,
140 ))
141}
142
143pub fn build_starting_group_membership_extension(inbox_id: &str, sequence_id: u64) -> Extension {
144 let mut group_membership = GroupMembership::new();
145 group_membership.add(inbox_id.to_string(), sequence_id);
146 build_group_membership_extension(&group_membership)
147}
148
149pub fn build_group_membership_extension(group_membership: &GroupMembership) -> Extension {
150 let unknown_gc_extension = UnknownExtension(group_membership.into());
151
152 Extension::Unknown(GROUP_MEMBERSHIP_EXTENSION_ID, unknown_gc_extension)
153}
154
155pub(crate) fn build_group_config(
157 dictionary: openmls::extensions::AppDataDictionary,
158) -> Result<MlsGroupCreateConfig, GroupError> {
159 let required_extension_types = &[
160 ExtensionType::AppDataDictionary,
161 ExtensionType::LastResort,
162 ExtensionType::ApplicationId,
163 ];
164 let required_proposal_types = &[ProposalType::AppDataUpdate];
165 let mut supported_extensions = required_extension_types.to_vec();
166 supported_extensions.extend([
167 ExtensionType::Unknown(WELCOME_WRAPPER_ENCRYPTION_EXTENSION_ID),
168 ExtensionType::Unknown(WELCOME_POINTEE_ENCRYPTION_AEAD_TYPES_EXTENSION_ID),
169 ]);
170 let capabilities = Capabilities::new(
171 None,
172 None,
173 Some(&supported_extensions),
174 Some(required_proposal_types),
175 None,
176 );
177 let extensions = Extensions::from_vec(vec![
178 Extension::AppDataDictionary(openmls::extensions::AppDataDictionaryExtension::new(
179 dictionary,
180 )),
181 Extension::RequiredCapabilities(RequiredCapabilitiesExtension::new(
182 required_extension_types,
183 required_proposal_types,
184 &[CredentialType::Basic],
185 )),
186 ])?;
187 Ok(MlsGroupCreateConfig::builder()
188 .with_group_context_extensions(extensions)
189 .capabilities(capabilities)
190 .ciphersuite(CIPHERSUITE)
191 .wire_format_policy(WireFormatPolicy::default())
192 .max_past_epochs(MAX_PAST_EPOCHS)
193 .use_ratchet_tree_extension(true)
194 .build())
195}
196
197#[cfg(test)]
199pub(crate) fn build_legacy_test_group_config(
200 protected_metadata_extension: Extension,
201 mutable_metadata_extension: Extension,
202 group_membership_extension: Extension,
203 mutable_permission_extension: Extension,
204) -> Result<MlsGroupCreateConfig, GroupError> {
205 let required_extension_types = &[
207 ExtensionType::Unknown(GROUP_MEMBERSHIP_EXTENSION_ID),
208 ExtensionType::Unknown(MUTABLE_METADATA_EXTENSION_ID),
209 ExtensionType::Unknown(GROUP_PERMISSIONS_EXTENSION_ID),
210 ExtensionType::ImmutableMetadata,
211 ExtensionType::LastResort,
212 ExtensionType::ApplicationId,
213 ];
214
215 let mut creator_capability_extensions = required_extension_types.to_vec();
220 creator_capability_extensions.push(ExtensionType::Unknown(
221 WELCOME_WRAPPER_ENCRYPTION_EXTENSION_ID,
222 ));
223 creator_capability_extensions.push(ExtensionType::Unknown(
224 WELCOME_POINTEE_ENCRYPTION_AEAD_TYPES_EXTENSION_ID,
225 ));
226 creator_capability_extensions.push(ExtensionType::AppDataDictionary);
232
233 let required_proposal_types = &[ProposalType::GroupContextExtensions];
234
235 let creator_capability_proposals = &[
245 ProposalType::GroupContextExtensions,
246 ProposalType::AppDataUpdate,
247 ];
248
249 let capabilities = Capabilities::new(
250 None,
251 None,
252 Some(&creator_capability_extensions),
253 Some(creator_capability_proposals),
254 None,
255 );
256 let credentials = &[CredentialType::Basic];
257
258 let required_capabilities =
259 Extension::RequiredCapabilities(RequiredCapabilitiesExtension::new(
260 required_extension_types,
261 required_proposal_types,
262 credentials,
263 ));
264
265 let extensions = Extensions::from_vec(vec![
266 protected_metadata_extension,
267 mutable_metadata_extension,
268 group_membership_extension,
269 mutable_permission_extension,
270 required_capabilities,
271 ])?;
272
273 Ok(MlsGroupCreateConfig::builder()
274 .with_group_context_extensions(extensions)
275 .capabilities(capabilities)
276 .ciphersuite(CIPHERSUITE)
277 .wire_format_policy(WireFormatPolicy::default())
278 .max_past_epochs(MAX_PAST_EPOCHS)
279 .use_ratchet_tree_extension(true)
280 .build())
281}
282
283pub fn filter_inbox_ids_needing_updates<'a>(
284 conn: &impl DbQuery,
285 filters: &[(&'a str, i64)],
286) -> Result<Vec<&'a str>, xmtp_db::ConnectionError> {
287 let existing_sequence_ids =
288 conn.get_latest_sequence_id(&filters.iter().map(|f| f.0).collect::<Vec<&str>>())?;
289
290 let needs_update = filters
291 .iter()
292 .filter_map(|&(inbox_id, seq)| {
293 let existing_sequence_id = existing_sequence_ids.get(inbox_id);
294 if existing_sequence_id.is_some_and(|&s| s >= seq) {
295 return None;
296 }
297
298 Some(inbox_id)
299 })
300 .collect();
301 Ok(needs_update)
302}
303
304pub(in crate::groups) fn validate_dm_group(
306 context: impl XmtpSharedContext,
307 mls_group: &OpenMlsGroup,
308 added_by_inbox: &str,
309) -> Result<(), MetadataPermissionsError> {
310 let metadata = extract_group_metadata(mls_group.extensions())?;
312
313 if metadata.conversation_type != ConversationType::Dm {
315 return Err(DmValidationError::InvalidConversationType.into());
316 }
317
318 let dm_members = match &metadata.dm_members {
320 Some(dm) => dm,
321 None => {
322 return Err(DmValidationError::MustHaveMembersSet.into());
323 }
324 };
325
326 let identity = context.identity();
327 let pair = [
328 dm_members.member_one_inbox_id.as_str(),
329 dm_members.member_two_inbox_id.as_str(),
330 ];
331 if pair[0] == pair[1] || !pair.contains(&identity.inbox_id()) {
332 return Err(DmValidationError::OurInboxMustBeMember.into());
333 }
334 if !pair.contains(&added_by_inbox) {
335 return Err(DmValidationError::ExpectedInboxesDoNotMatch.into());
336 }
337
338 let membership = extract_group_membership(mls_group.extensions())
340 .map_err(|_| DmValidationError::InvalidMembership)?;
341 if membership
342 .members
343 .keys()
344 .any(|inbox| !pair.contains(&inbox.as_str()))
345 {
346 return Err(DmValidationError::MemberOutsidePair.into());
347 }
348 for member in mls_group.members() {
349 let inbox = inbox_id_from_credential(&member.credential)
350 .map_err(|_| DmValidationError::InvalidMemberCredential)?;
351 if !pair.contains(&inbox.as_str()) {
352 return Err(DmValidationError::MemberOutsidePair.into());
353 }
354 }
355
356 let mutable_metadata =
358 xmtp_mls_common::app_data::component_source::extract_group_mutable_metadata_capability_aware(mls_group)
359 .map_err(|error| match error {
360 xmtp_mls_common::app_data::component_source::ComponentSourceError::GroupMutableMetadata(inner) => {
361 MetadataPermissionsError::Mutable(inner)
362 }
363 other => MetadataPermissionsError::ComponentSource(other),
364 })?;
365
366 if !mutable_metadata.admin_list.is_empty() || !mutable_metadata.super_admin_list.is_empty() {
368 return Err(DmValidationError::MustHaveEmptyAdminAndSuperAdmin.into());
369 }
370
371 let permissions = group_permissions::policy_set_from_dictionary(mls_group.extensions())?;
374 let mut expected_permissions = GroupMutablePermissions::new(PolicySet::new_dm());
375 expected_permissions.policies.update_permissions_policy =
377 group_permissions::PermissionsPolicies::allow_if_actor_super_admin();
378
379 if permissions.policies.add_member_policy != expected_permissions.policies.add_member_policy
380 || permissions.policies.remove_member_policy
381 != expected_permissions.policies.remove_member_policy
382 || permissions.policies.add_admin_policy != expected_permissions.policies.add_admin_policy
383 || permissions.policies.remove_admin_policy
384 != expected_permissions.policies.remove_admin_policy
385 || permissions.policies.update_permissions_policy
386 != expected_permissions.policies.update_permissions_policy
387 {
388 return Err(DmValidationError::InvalidPermissions.into());
389 }
390
391 Ok(())
392}