xmtp_mls_common/invite/
payload.rs1use thiserror::Error;
20use xmtp_proto::xmtp::mls::message_contents::{
21 ExternalInvitePayload, ExternalInvitePayloadV1,
22 external_invite_payload::Version as ExternalInvitePayloadVersion,
23};
24
25pub const SYMMETRIC_KEY_LEN: usize = 32;
28
29pub const NONCE_LEN: usize = 12;
32
33pub const MIN_EXTERNAL_GROUP_ID_LEN: usize = 4;
38
39pub const RECOMMENDED_EXTERNAL_GROUP_ID_LEN: usize = 16;
43
44#[derive(Debug, Error, PartialEq, Eq)]
46pub enum InvitePayloadError {
47 #[error("unsupported or missing external-invite payload version")]
50 UnsupportedVersion,
51 #[error("external_group_id must be at least {min} bytes (got {len})", min = MIN_EXTERNAL_GROUP_ID_LEN)]
53 InvalidExternalGroupIdLength {
54 len: usize,
56 },
57 #[error("symmetric_key must be exactly {SYMMETRIC_KEY_LEN} bytes (got {0})")]
59 InvalidSymmetricKeyLength(usize),
60}
61
62pub fn generate_symmetric_key() -> [u8; SYMMETRIC_KEY_LEN] {
68 xmtp_common::rand_array::<SYMMETRIC_KEY_LEN>()
69}
70
71pub fn generate_nonce() -> [u8; NONCE_LEN] {
77 xmtp_common::rand_array::<NONCE_LEN>()
78}
79
80pub fn generate_external_group_id() -> [u8; RECOMMENDED_EXTERNAL_GROUP_ID_LEN] {
87 xmtp_common::rand_array::<RECOMMENDED_EXTERNAL_GROUP_ID_LEN>()
88}
89
90pub fn validate(
96 payload: &ExternalInvitePayload,
97) -> Result<&ExternalInvitePayloadV1, InvitePayloadError> {
98 let v1 = match &payload.version {
99 Some(ExternalInvitePayloadVersion::V1(v1)) => v1,
100 None => return Err(InvitePayloadError::UnsupportedVersion),
101 };
102 if v1.external_group_id.len() < MIN_EXTERNAL_GROUP_ID_LEN {
103 return Err(InvitePayloadError::InvalidExternalGroupIdLength {
104 len: v1.external_group_id.len(),
105 });
106 }
107 if v1.symmetric_key.len() != SYMMETRIC_KEY_LEN {
108 return Err(InvitePayloadError::InvalidSymmetricKeyLength(
109 v1.symmetric_key.len(),
110 ));
111 }
112 Ok(v1)
113}
114
115pub fn build_payload(
129 service_pointer: Vec<u8>,
130 external_group_id: Vec<u8>,
131 symmetric_key: [u8; SYMMETRIC_KEY_LEN],
132) -> Result<ExternalInvitePayload, InvitePayloadError> {
133 if external_group_id.len() < MIN_EXTERNAL_GROUP_ID_LEN {
134 return Err(InvitePayloadError::InvalidExternalGroupIdLength {
135 len: external_group_id.len(),
136 });
137 }
138 Ok(ExternalInvitePayload {
139 version: Some(ExternalInvitePayloadVersion::V1(ExternalInvitePayloadV1 {
140 service_pointer,
141 external_group_id,
142 symmetric_key: symmetric_key.to_vec(),
143 })),
144 })
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 fn well_formed_payload() -> ExternalInvitePayload {
152 build_payload(
153 b"https://invites.example/abc".to_vec(),
154 generate_external_group_id().to_vec(),
155 [0x42u8; SYMMETRIC_KEY_LEN],
156 )
157 .expect("recommended-length external_group_id is well-formed")
158 }
159
160 #[xmtp_common::test(unwrap_try = true)]
161 fn key_nonce_and_id_are_random() {
162 let k1 = generate_symmetric_key();
163 let k2 = generate_symmetric_key();
164 assert_eq!(k1.len(), SYMMETRIC_KEY_LEN);
165 assert_eq!(k2.len(), SYMMETRIC_KEY_LEN);
166 assert_ne!(k1, k2, "two CSPRNG-generated keys must differ");
167 assert_ne!(k1, [0u8; SYMMETRIC_KEY_LEN], "key must not be all-zero");
168
169 let n1 = generate_nonce();
170 let n2 = generate_nonce();
171 assert_eq!(n1.len(), NONCE_LEN);
172 assert_eq!(n2.len(), NONCE_LEN);
173 assert_ne!(n1, n2, "two CSPRNG-generated nonces must differ");
174
175 let id1 = generate_external_group_id();
176 let id2 = generate_external_group_id();
177 assert_eq!(id1.len(), RECOMMENDED_EXTERNAL_GROUP_ID_LEN);
178 assert_ne!(
179 id1, id2,
180 "two CSPRNG-generated external_group_ids must differ"
181 );
182 }
183
184 #[xmtp_common::test(unwrap_try = true)]
185 fn validate_accepts_well_formed_v1() {
186 let payload = well_formed_payload();
187 let v1 = validate(&payload)?;
188 assert_eq!(v1.symmetric_key.len(), SYMMETRIC_KEY_LEN);
189 assert!(v1.external_group_id.len() >= MIN_EXTERNAL_GROUP_ID_LEN);
190 }
191
192 #[xmtp_common::test(unwrap_try = true)]
193 fn validate_rejects_missing_version() {
194 let payload = ExternalInvitePayload { version: None };
195 assert_eq!(
196 validate(&payload),
197 Err(InvitePayloadError::UnsupportedVersion)
198 );
199 }
200
201 #[xmtp_common::test(unwrap_try = true)]
202 fn build_payload_rejects_short_external_group_id() {
203 let result = build_payload(
204 b"svc".to_vec(),
205 vec![0u8; MIN_EXTERNAL_GROUP_ID_LEN - 1],
206 [0x42u8; SYMMETRIC_KEY_LEN],
207 );
208 assert_eq!(
209 result,
210 Err(InvitePayloadError::InvalidExternalGroupIdLength {
211 len: MIN_EXTERNAL_GROUP_ID_LEN - 1
212 })
213 );
214 }
215
216 #[xmtp_common::test(unwrap_try = true)]
217 fn validate_rejects_short_external_group_id_from_wire() {
218 let payload = ExternalInvitePayload {
223 version: Some(ExternalInvitePayloadVersion::V1(ExternalInvitePayloadV1 {
224 service_pointer: b"svc".to_vec(),
225 external_group_id: vec![0u8; MIN_EXTERNAL_GROUP_ID_LEN - 1],
226 symmetric_key: vec![0x42u8; SYMMETRIC_KEY_LEN],
227 })),
228 };
229 assert_eq!(
230 validate(&payload),
231 Err(InvitePayloadError::InvalidExternalGroupIdLength {
232 len: MIN_EXTERNAL_GROUP_ID_LEN - 1
233 })
234 );
235 }
236
237 #[xmtp_common::test(unwrap_try = true)]
238 fn validate_rejects_wrong_symmetric_key_length() {
239 let mut payload = well_formed_payload();
240 if let Some(ExternalInvitePayloadVersion::V1(ref mut v1)) = payload.version {
241 v1.symmetric_key = vec![0u8; SYMMETRIC_KEY_LEN - 1];
242 }
243 assert_eq!(
244 validate(&payload),
245 Err(InvitePayloadError::InvalidSymmetricKeyLength(
246 SYMMETRIC_KEY_LEN - 1
247 ))
248 );
249 }
250
251 #[xmtp_common::test(unwrap_try = true)]
252 fn build_payload_round_trip() {
253 let service_pointer = b"https://invites.example/abc".to_vec();
254 let external_group_id = generate_external_group_id().to_vec();
255 let key = [0x42u8; SYMMETRIC_KEY_LEN];
256
257 let payload = build_payload(service_pointer.clone(), external_group_id.clone(), key)?;
258 let v1 = validate(&payload)?;
259 assert_eq!(v1.service_pointer, service_pointer);
260 assert_eq!(v1.external_group_id, external_group_id);
261 assert_eq!(v1.symmetric_key, key.to_vec());
262 }
263}