1use crate::{Result, SealedError};
15use alloc::format;
16use alloc::string::{String, ToString};
17use alloc::vec::Vec;
18use core::fmt;
19use serde::{Deserialize, Serialize};
20
21const MAX_STRING_FIELD_LEN: usize = 4096;
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
43pub struct GroupId(String);
44
45impl GroupId {
46 pub const MAX_LEN: usize = MAX_STRING_FIELD_LEN;
52
53 pub fn new(id: impl Into<String>) -> Result<Self> {
55 let id = id.into();
56 Self::validate(&id)?;
57 Ok(Self(id))
58 }
59
60 fn validate(id: &str) -> Result<()> {
61 if id.is_empty() {
62 return Err(SealedError::InvalidGroupId(
63 "Group ID cannot be empty".to_string(),
64 ));
65 }
66 if id.len() > Self::MAX_LEN {
67 return Err(SealedError::InvalidGroupId(format!(
68 "Group ID length {} exceeds maximum {}",
69 id.len(),
70 Self::MAX_LEN
71 )));
72 }
73 for segment in id.split(':') {
74 offline_protocol_core::validate_id_chars(segment, "Group ID segment")
75 .map_err(|e| SealedError::InvalidGroupId(e.to_string()))?;
76 }
77 Ok(())
78 }
79
80 pub fn as_str(&self) -> &str {
82 &self.0
83 }
84
85 pub fn for_session(user_a: &str, user_b: &str) -> Result<Self> {
100 use offline_protocol_core::Address;
101
102 let ordered = match (user_a.parse::<Address>(), user_b.parse::<Address>()) {
103 (Ok(addr_a), Ok(addr_b)) if addr_b < addr_a => [user_b, user_a],
104 (Ok(_), Ok(_)) => [user_a, user_b],
105 _ => {
106 let mut users = [user_a, user_b];
107 users.sort();
108 users
109 }
110 };
111
112 Self::new(format!("session:{}:{}", ordered[0], ordered[1]))
113 }
114}
115
116impl fmt::Display for GroupId {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 write!(f, "{}", self.0)
119 }
120}
121
122impl<'de> Deserialize<'de> for GroupId {
123 fn deserialize<D: serde::Deserializer<'de>>(
124 deserializer: D,
125 ) -> core::result::Result<Self, D::Error> {
126 let s = String::deserialize(deserializer)?;
127 GroupId::new(s).map_err(serde::de::Error::custom)
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
133pub enum MlsMessageType {
134 Application,
136
137 Welcome,
139
140 Commit,
142
143 Proposal,
145}
146
147impl MlsMessageType {
148 pub fn as_str(&self) -> &'static str {
150 match self {
151 Self::Application => "Application",
152 Self::Welcome => "Welcome",
153 Self::Commit => "Commit",
154 Self::Proposal => "Proposal",
155 }
156 }
157
158 pub fn from_str_opt(s: &str) -> Option<Self> {
160 match s {
161 "Application" => Some(Self::Application),
162 "Welcome" => Some(Self::Welcome),
163 "Commit" => Some(Self::Commit),
164 "Proposal" => Some(Self::Proposal),
165 _ => None,
166 }
167 }
168
169 pub fn as_u8(&self) -> u8 {
171 match self {
172 Self::Application => 0,
173 Self::Welcome => 1,
174 Self::Commit => 2,
175 Self::Proposal => 3,
176 }
177 }
178
179 pub fn from_u8(tag: u8) -> Option<Self> {
181 match tag {
182 0 => Some(Self::Application),
183 1 => Some(Self::Welcome),
184 2 => Some(Self::Commit),
185 3 => Some(Self::Proposal),
186 _ => None,
187 }
188 }
189}
190
191impl fmt::Display for MlsMessageType {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 f.write_str(self.as_str())
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct EncryptedMessage {
200 pub group_id: GroupId,
202
203 pub message_type: MlsMessageType,
205
206 pub epoch: u64,
208
209 pub ciphertext: Vec<u8>,
211
212 pub sender_id: String,
214
215 pub timestamp_ms: u64,
217}
218
219impl EncryptedMessage {
220 pub fn to_base64(&self) -> Result<String> {
222 let json =
223 serde_json::to_vec(self).map_err(|e| SealedError::Serialization(e.to_string()))?;
224 Ok(base64::Engine::encode(
225 &base64::engine::general_purpose::STANDARD,
226 &json,
227 ))
228 }
229
230 pub fn from_base64(encoded: &str) -> Result<Self> {
232 let json = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded)
233 .map_err(|e| SealedError::Deserialization(e.to_string()))?;
234 serde_json::from_slice(&json).map_err(|e| SealedError::Deserialization(e.to_string()))
235 }
236
237 const MAX_CIPHERTEXT_LEN: usize = 128 * 1024 * 1024; pub fn to_bytes(&self) -> Vec<u8> {
252 let group_id = self.group_id.as_str().as_bytes();
253 let sender_id = self.sender_id.as_bytes();
254
255 let capacity =
256 4 + group_id.len() + 4 + sender_id.len() + 1 + 8 + 8 + 4 + self.ciphertext.len();
257 let mut buf = Vec::with_capacity(capacity);
258
259 buf.extend_from_slice(&(group_id.len() as u32).to_le_bytes());
260 buf.extend_from_slice(group_id);
261 buf.extend_from_slice(&(sender_id.len() as u32).to_le_bytes());
262 buf.extend_from_slice(sender_id);
263 buf.push(self.message_type.as_u8());
264 buf.extend_from_slice(&self.epoch.to_le_bytes());
265 buf.extend_from_slice(&self.timestamp_ms.to_le_bytes());
266 buf.extend_from_slice(&(self.ciphertext.len() as u32).to_le_bytes());
267 buf.extend_from_slice(&self.ciphertext);
268
269 buf
270 }
271
272 pub fn from_bytes(data: &[u8]) -> Result<Self> {
274 let err = SealedError::Deserialization;
275 let mut pos = 0;
276
277 let read_u32 = |pos: &mut usize| -> Result<u32> {
278 if *pos + 4 > data.len() {
279 return Err(err("unexpected end of data reading u32".to_string()));
280 }
281 let mut bytes = [0u8; 4];
282 bytes.copy_from_slice(&data[*pos..*pos + 4]);
283 let val = u32::from_le_bytes(bytes);
284 *pos += 4;
285 Ok(val)
286 };
287
288 let read_u64 = |pos: &mut usize| -> Result<u64> {
289 if *pos + 8 > data.len() {
290 return Err(err("unexpected end of data reading u64".to_string()));
291 }
292 let mut bytes = [0u8; 8];
293 bytes.copy_from_slice(&data[*pos..*pos + 8]);
294 let val = u64::from_le_bytes(bytes);
295 *pos += 8;
296 Ok(val)
297 };
298
299 let read_bytes = |pos: &mut usize, len: usize| -> Result<Vec<u8>> {
300 if *pos + len > data.len() {
301 return Err(err("unexpected end of data reading bytes".to_string()));
302 }
303 let val = data[*pos..*pos + len].to_vec();
304 *pos += len;
305 Ok(val)
306 };
307
308 let group_id_len = read_u32(&mut pos)? as usize;
309 if group_id_len > MAX_STRING_FIELD_LEN {
310 return Err(err(format!(
311 "group_id_len {} exceeds maximum {}",
312 group_id_len, MAX_STRING_FIELD_LEN
313 )));
314 }
315 let group_id = String::from_utf8(read_bytes(&mut pos, group_id_len)?)
316 .map_err(|e| err(format!("invalid group_id UTF-8: {}", e)))?;
317
318 let sender_len = read_u32(&mut pos)? as usize;
319 if sender_len > MAX_STRING_FIELD_LEN {
320 return Err(err(format!(
321 "sender_len {} exceeds maximum {}",
322 sender_len, MAX_STRING_FIELD_LEN
323 )));
324 }
325 let sender_id = String::from_utf8(read_bytes(&mut pos, sender_len)?)
326 .map_err(|e| err(format!("invalid sender_id UTF-8: {}", e)))?;
327
328 if pos + 1 > data.len() {
329 return Err(err(
330 "unexpected end of data reading message_type".to_string()
331 ));
332 }
333 let message_type = MlsMessageType::from_u8(data[pos])
334 .ok_or_else(|| err(format!("unknown message_type tag {}", data[pos])))?;
335 pos += 1;
336
337 let epoch = read_u64(&mut pos)?;
338 let timestamp_ms = read_u64(&mut pos)?;
339
340 let ciphertext_len = read_u32(&mut pos)? as usize;
341 if ciphertext_len > Self::MAX_CIPHERTEXT_LEN {
342 return Err(err(format!(
343 "ciphertext_len {} exceeds maximum {}",
344 ciphertext_len,
345 Self::MAX_CIPHERTEXT_LEN
346 )));
347 }
348 let ciphertext = read_bytes(&mut pos, ciphertext_len)?;
349
350 Ok(Self {
351 group_id: GroupId::new(group_id)?,
352 message_type,
353 epoch,
354 ciphertext,
355 sender_id,
356 timestamp_ms,
357 })
358 }
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct WelcomeMessage {
367 pub group_id: GroupId,
369
370 pub welcome_data: Vec<u8>,
372
373 pub inviter_id: String,
375
376 pub group_name: Option<String>,
378
379 pub timestamp_ms: u64,
381}
382
383impl WelcomeMessage {
384 pub fn to_base64(&self) -> Result<String> {
386 let json =
387 serde_json::to_vec(self).map_err(|e| SealedError::Serialization(e.to_string()))?;
388 Ok(base64::Engine::encode(
389 &base64::engine::general_purpose::STANDARD,
390 &json,
391 ))
392 }
393
394 pub fn from_base64(encoded: &str) -> Result<Self> {
396 let json = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded)
397 .map_err(|e| SealedError::Deserialization(e.to_string()))?;
398 serde_json::from_slice(&json).map_err(|e| SealedError::Deserialization(e.to_string()))
399 }
400}
401
402#[cfg(all(test, feature = "std"))]
403mod tests {
404 use super::*;
405 use offline_protocol_core::Address;
406
407 fn disagreeing_addresses() -> (Address, Address) {
417 let mut low_bytes = [0u8; Address::HASH_LEN];
418 low_bytes[0] = 0x00;
419 let mut high_bytes = [0u8; Address::HASH_LEN];
420 high_bytes[0] = 0x40;
421 (
422 Address::from_hash_bytes(low_bytes),
423 Address::from_hash_bytes(high_bytes),
424 )
425 }
426
427 #[test]
428 fn session_id_orders_addresses_by_hash_not_by_string() {
429 let (low, high) = disagreeing_addresses();
430
431 assert!(low < high, "hash-byte order");
433 assert!(
434 low.to_string() > high.to_string(),
435 "rendered-string order must be the opposite, or this test proves nothing"
436 );
437
438 let expected = format!("session:{}:{}", low, high);
439 assert_eq!(
440 GroupId::for_session(&low.to_string(), &high.to_string())
441 .unwrap()
442 .as_str(),
443 expected,
444 "address slots must follow Address order, not string order"
445 );
446 }
447
448 #[test]
449 fn session_id_is_symmetric_for_addresses() {
450 let (low, high) = disagreeing_addresses();
451 let (a, b) = (low.to_string(), high.to_string());
452
453 assert_eq!(
454 GroupId::for_session(&a, &b).unwrap(),
455 GroupId::for_session(&b, &a).unwrap(),
456 "both peers must derive the same slot regardless of argument order"
457 );
458 }
459
460 #[test]
463 fn session_id_falls_back_to_string_order_for_non_addresses() {
464 assert_eq!(
465 GroupId::for_session("bob", "alice").unwrap().as_str(),
466 "session:alice:bob"
467 );
468
469 let (address, _) = disagreeing_addresses();
470 let mixed = GroupId::for_session("alice", &address.to_string()).unwrap();
471 let mixed_reversed = GroupId::for_session(&address.to_string(), "alice").unwrap();
472 assert_eq!(mixed, mixed_reversed, "mixed pairs must stay symmetric");
473 }
474
475 fn sample_encrypted_message() -> EncryptedMessage {
476 EncryptedMessage {
477 group_id: GroupId::for_session("alice", "bob").unwrap(),
478 message_type: MlsMessageType::Application,
479 epoch: 42,
480 ciphertext: vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01],
481 sender_id: "alice".to_string(),
482 timestamp_ms: 1_700_000_000_123,
483 }
484 }
485
486 #[test]
487 fn test_encrypted_message_bytes_roundtrip() {
488 let msg = sample_encrypted_message();
489 let bytes = msg.to_bytes();
490 let decoded = EncryptedMessage::from_bytes(&bytes).unwrap();
491
492 assert_eq!(decoded.group_id, msg.group_id);
493 assert_eq!(decoded.message_type, msg.message_type);
494 assert_eq!(decoded.epoch, msg.epoch);
495 assert_eq!(decoded.ciphertext, msg.ciphertext);
496 assert_eq!(decoded.sender_id, msg.sender_id);
497 assert_eq!(decoded.timestamp_ms, msg.timestamp_ms);
498 }
499
500 #[test]
501 fn test_encrypted_message_bytes_roundtrip_empty_ciphertext() {
502 let mut msg = sample_encrypted_message();
503 msg.ciphertext = Vec::new();
504 let decoded = EncryptedMessage::from_bytes(&msg.to_bytes()).unwrap();
505 assert!(decoded.ciphertext.is_empty());
506 }
507
508 #[test]
509 fn test_encrypted_message_from_bytes_truncated() {
510 let bytes = sample_encrypted_message().to_bytes();
511 for len in 0..bytes.len() {
513 assert!(
514 EncryptedMessage::from_bytes(&bytes[..len]).is_err(),
515 "truncation at {} unexpectedly succeeded",
516 len
517 );
518 }
519 }
520
521 #[test]
522 fn test_encrypted_message_from_bytes_oversized_group_id_rejected() {
523 let mut bytes = Vec::new();
525 bytes.extend_from_slice(&(u32::MAX).to_le_bytes());
526 let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
527 assert!(err.to_string().contains("group_id_len"));
528 }
529
530 #[test]
534 fn test_encrypted_message_from_bytes_oversized_sender_id_rejected() {
535 let mut bytes = Vec::new();
536 let group_id = b"session:alice:bob";
537 bytes.extend_from_slice(&(group_id.len() as u32).to_le_bytes());
538 bytes.extend_from_slice(group_id);
539 bytes.extend_from_slice(&(u32::MAX).to_le_bytes());
540 let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
541 assert!(
542 err.to_string().contains("sender_len"),
543 "expected the sender_len cap to refuse it, got: {err}"
544 );
545 }
546
547 #[test]
548 fn test_encrypted_message_from_bytes_oversized_ciphertext_rejected() {
549 let mut msg = sample_encrypted_message();
550 msg.ciphertext = vec![0u8; 4];
551 let mut bytes = msg.to_bytes();
552 let ct_len_offset = bytes.len() - 4 - 4;
554 bytes[ct_len_offset..ct_len_offset + 4].copy_from_slice(&(u32::MAX).to_le_bytes());
555 let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
556 assert!(err.to_string().contains("ciphertext_len"));
557 }
558
559 #[test]
560 fn test_encrypted_message_from_bytes_unknown_message_type_rejected() {
561 let msg = sample_encrypted_message();
562 let mut bytes = msg.to_bytes();
563 let tag_offset = 4 + msg.group_id.as_str().len() + 4 + msg.sender_id.len();
564 bytes[tag_offset] = 0xFF;
565 let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
566 assert!(err.to_string().contains("message_type"));
567 }
568
569 #[test]
577 fn json_bodies_are_refused_by_the_compact_parser() {
578 let json = serde_json::to_vec(&sample_encrypted_message()).unwrap();
579 assert!(json.starts_with(b"{\""), "premise: JSON starts with {{\"");
580
581 let err = EncryptedMessage::from_bytes(&json).unwrap_err();
582 assert!(
583 err.to_string().contains("group_id_len"),
584 "a JSON body must be refused by the string-field cap, got: {err}"
585 );
586
587 let claimed = u32::from_le_bytes([json[0], json[1], json[2], json[3]]) as usize;
588 assert!(
589 claimed > MAX_STRING_FIELD_LEN,
590 "the cap only disambiguates while it stays below {claimed}"
591 );
592 }
593
594 #[test]
595 fn test_group_id_accepts_legitimate_formats() {
596 assert!(GroupId::new("group:0bd6e5f2-3a70-4a4e-9c3f-1c1f2a3b4c5d").is_ok());
597 assert!(GroupId::new("session:alice:bob").is_ok());
598 assert!(GroupId::new("plain-segment_1.x@y").is_ok());
599 }
600
601 #[test]
602 fn test_group_id_rejects_storage_hostile_chars() {
603 assert!(GroupId::new("").is_err());
604 assert!(GroupId::new("group/evil").is_err());
605 assert!(GroupId::new("group\\evil").is_err());
606 assert!(GroupId::new("group\0evil").is_err());
607 assert!(GroupId::new("group\nevil").is_err());
608 assert!(GroupId::new("group\x7Fevil").is_err()); assert!(GroupId::new(".").is_err());
610 assert!(GroupId::new("..").is_err());
611 assert!(GroupId::new("session:..:bob").is_err());
613 assert!(GroupId::new("group:../../etc").is_err());
614 assert!(GroupId::new("session:").is_err());
616 assert!(GroupId::new(":session").is_err());
617 assert!(GroupId::new("a::b").is_err());
618 }
619
620 #[test]
621 fn test_group_id_length_cap() {
622 assert!(GroupId::new("a".repeat(GroupId::MAX_LEN)).is_ok());
623 assert!(GroupId::new("a".repeat(GroupId::MAX_LEN + 1)).is_err());
624 }
625
626 #[test]
627 fn test_group_id_deserialize_rejects_hostile() {
628 assert!(serde_json::from_str::<GroupId>(r#""evil/path""#).is_err());
629 assert!(serde_json::from_str::<GroupId>(r#""..""#).is_err());
630 assert!(serde_json::from_str::<GroupId>(r#""session:alice:bob""#).is_ok());
631 }
632
633 #[test]
634 fn test_encrypted_message_from_bytes_rejects_hostile_group_id() {
635 let mut bytes = Vec::new();
638 let hostile = b"evil/../path";
639 bytes.extend_from_slice(&(hostile.len() as u32).to_le_bytes());
640 bytes.extend_from_slice(hostile);
641 let sender = b"alice";
642 bytes.extend_from_slice(&(sender.len() as u32).to_le_bytes());
643 bytes.extend_from_slice(sender);
644 bytes.push(0); bytes.extend_from_slice(&42u64.to_le_bytes()); bytes.extend_from_slice(&1u64.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
649 assert!(matches!(err, SealedError::InvalidGroupId(_)));
650 }
651
652 #[test]
653 fn test_mls_message_type_u8_roundtrip() {
654 for msg_type in [
655 MlsMessageType::Application,
656 MlsMessageType::Welcome,
657 MlsMessageType::Commit,
658 MlsMessageType::Proposal,
659 ] {
660 assert_eq!(MlsMessageType::from_u8(msg_type.as_u8()), Some(msg_type));
661 }
662 assert_eq!(MlsMessageType::from_u8(4), None);
663 }
664}