1use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::str::FromStr;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
17pub struct GroupId(String);
18
19impl GroupId {
20 pub const MAX_LEN: usize = 4096;
24
25 pub fn new(id: impl Into<String>) -> crate::error::Result<Self> {
27 let id = id.into();
28 Self::validate(&id)?;
29 Ok(Self(id))
30 }
31
32 fn validate(id: &str) -> crate::error::Result<()> {
33 if id.is_empty() {
34 return Err(crate::MlsError::InvalidGroupId(
35 "Group ID cannot be empty".to_string(),
36 ));
37 }
38 if id.len() > Self::MAX_LEN {
39 return Err(crate::MlsError::InvalidGroupId(format!(
40 "Group ID length {} exceeds maximum {}",
41 id.len(),
42 Self::MAX_LEN
43 )));
44 }
45 for segment in id.split(':') {
46 offline_protocol_core::validate_id_chars(segment, "Group ID segment")
47 .map_err(|e| crate::MlsError::InvalidGroupId(e.to_string()))?;
48 }
49 Ok(())
50 }
51
52 pub fn as_str(&self) -> &str {
54 &self.0
55 }
56
57 pub fn for_session(user_a: &str, user_b: &str) -> crate::error::Result<Self> {
72 use offline_protocol_core::Address;
73
74 let ordered = match (user_a.parse::<Address>(), user_b.parse::<Address>()) {
75 (Ok(addr_a), Ok(addr_b)) if addr_b < addr_a => [user_b, user_a],
76 (Ok(_), Ok(_)) => [user_a, user_b],
77 _ => {
78 let mut users = [user_a, user_b];
79 users.sort();
80 users
81 }
82 };
83
84 Self::new(format!("session:{}:{}", ordered[0], ordered[1]))
85 }
86}
87
88impl std::fmt::Display for GroupId {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 write!(f, "{}", self.0)
91 }
92}
93
94impl<'de> Deserialize<'de> for GroupId {
95 fn deserialize<D: serde::Deserializer<'de>>(
96 deserializer: D,
97 ) -> std::result::Result<Self, D::Error> {
98 let s = String::deserialize(deserializer)?;
99 GroupId::new(s).map_err(serde::de::Error::custom)
100 }
101}
102
103#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
105#[serde(rename_all = "lowercase")]
106pub enum GroupRole {
107 Admin,
109 #[default]
111 #[serde(other)]
112 Member,
113}
114
115impl fmt::Display for GroupRole {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 match self {
118 Self::Admin => write!(f, "admin"),
119 Self::Member => write!(f, "member"),
120 }
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
126#[error("Invalid role '{0}', must be 'admin' or 'member'")]
127pub struct ParseGroupRoleError(pub String);
128
129impl FromStr for GroupRole {
130 type Err = ParseGroupRoleError;
131
132 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
133 match s.to_ascii_lowercase().as_str() {
134 "admin" => Ok(Self::Admin),
135 "member" => Ok(Self::Member),
136 _ => Err(ParseGroupRoleError(s.to_string())),
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct KeyPackageBundle {
144 pub package_id: String,
146
147 pub user_id: String,
149
150 pub key_package_data: Vec<u8>,
152
153 pub created_at_ms: u64,
155
156 pub expires_at_ms: u64,
158
159 pub synced: bool,
161
162 #[serde(default)]
177 pub reserved_for_publication: bool,
178
179 #[serde(default)]
199 pub assigned_peer: Option<String>,
200
201 #[serde(default)]
213 pub provider_hash_ref: Option<Vec<u8>>,
214}
215
216impl KeyPackageBundle {
217 pub fn new(
219 package_id: String,
220 user_id: String,
221 key_package_data: Vec<u8>,
222 lifetime_secs: u64,
223 ) -> Self {
224 let now_ms = chrono::Utc::now().timestamp_millis() as u64;
225 Self {
226 package_id,
227 user_id,
228 key_package_data,
229 created_at_ms: now_ms,
230 expires_at_ms: now_ms + (lifetime_secs * 1000),
231 synced: false,
232 reserved_for_publication: false,
233 assigned_peer: None,
234 provider_hash_ref: None,
235 }
236 }
237
238 pub fn is_expired(&self) -> bool {
243 let now_ms = chrono::Utc::now().timestamp_millis() as u64;
244 now_ms >= self.expires_at_ms
245 }
246
247 pub fn expired_past_grace(&self, grace_secs: u64) -> bool {
257 let now_ms = chrono::Utc::now().timestamp_millis() as u64;
258 now_ms
259 >= self
260 .expires_at_ms
261 .saturating_add(grace_secs.saturating_mul(1000))
262 }
263
264 pub fn remaining_lifetime_ms(&self) -> u64 {
270 let now_ms = chrono::Utc::now().timestamp_millis() as u64;
271 self.expires_at_ms.saturating_sub(now_ms)
272 }
273
274 pub fn from_transfer(
277 package_id: String,
278 user_id: String,
279 key_package_data: Vec<u8>,
280 remaining_lifetime_ms: u64,
281 ) -> Self {
282 let now_ms = chrono::Utc::now().timestamp_millis() as u64;
283 Self {
284 package_id,
285 user_id,
286 key_package_data,
287 created_at_ms: now_ms,
288 expires_at_ms: now_ms.saturating_add(remaining_lifetime_ms),
289 synced: false,
290 reserved_for_publication: false,
292 assigned_peer: None,
295 provider_hash_ref: None,
298 }
299 }
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct GroupInfo {
305 pub group_id: GroupId,
307
308 pub name: Option<String>,
310
311 pub members: Vec<String>,
313
314 pub members_count: u32,
316
317 pub epoch: u64,
319
320 pub is_session: bool,
322
323 pub created_at_ms: u64,
325
326 pub last_activity_ms: u64,
328
329 #[serde(default)]
348 pub unproven_members: u32,
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
353pub enum MlsMessageType {
354 Application,
356
357 Welcome,
359
360 Commit,
362
363 Proposal,
365}
366
367impl MlsMessageType {
368 pub fn as_str(&self) -> &'static str {
370 match self {
371 Self::Application => "Application",
372 Self::Welcome => "Welcome",
373 Self::Commit => "Commit",
374 Self::Proposal => "Proposal",
375 }
376 }
377
378 pub fn from_str_opt(s: &str) -> Option<Self> {
380 match s {
381 "Application" => Some(Self::Application),
382 "Welcome" => Some(Self::Welcome),
383 "Commit" => Some(Self::Commit),
384 "Proposal" => Some(Self::Proposal),
385 _ => None,
386 }
387 }
388
389 pub fn as_u8(&self) -> u8 {
391 match self {
392 Self::Application => 0,
393 Self::Welcome => 1,
394 Self::Commit => 2,
395 Self::Proposal => 3,
396 }
397 }
398
399 pub fn from_u8(tag: u8) -> Option<Self> {
401 match tag {
402 0 => Some(Self::Application),
403 1 => Some(Self::Welcome),
404 2 => Some(Self::Commit),
405 3 => Some(Self::Proposal),
406 _ => None,
407 }
408 }
409}
410
411impl std::fmt::Display for MlsMessageType {
412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413 f.write_str(self.as_str())
414 }
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct EncryptedMessage {
420 pub group_id: GroupId,
422
423 pub message_type: MlsMessageType,
425
426 pub epoch: u64,
428
429 pub ciphertext: Vec<u8>,
431
432 pub sender_id: String,
434
435 pub timestamp_ms: u64,
437}
438
439impl EncryptedMessage {
440 pub fn to_base64(&self) -> Result<String, crate::MlsError> {
442 let json =
443 serde_json::to_vec(self).map_err(|e| crate::MlsError::Serialization(e.to_string()))?;
444 Ok(base64::Engine::encode(
445 &base64::engine::general_purpose::STANDARD,
446 &json,
447 ))
448 }
449
450 pub fn from_base64(encoded: &str) -> Result<Self, crate::MlsError> {
452 let json = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded)
453 .map_err(|e| crate::MlsError::Deserialization(e.to_string()))?;
454 serde_json::from_slice(&json).map_err(|e| crate::MlsError::Deserialization(e.to_string()))
455 }
456
457 const MAX_STRING_FIELD_LEN: usize = 4096;
460 const MAX_CIPHERTEXT_LEN: usize = 128 * 1024 * 1024; pub fn to_bytes(&self) -> Vec<u8> {
475 let group_id = self.group_id.as_str().as_bytes();
476 let sender_id = self.sender_id.as_bytes();
477
478 let capacity =
479 4 + group_id.len() + 4 + sender_id.len() + 1 + 8 + 8 + 4 + self.ciphertext.len();
480 let mut buf = Vec::with_capacity(capacity);
481
482 buf.extend_from_slice(&(group_id.len() as u32).to_le_bytes());
483 buf.extend_from_slice(group_id);
484 buf.extend_from_slice(&(sender_id.len() as u32).to_le_bytes());
485 buf.extend_from_slice(sender_id);
486 buf.push(self.message_type.as_u8());
487 buf.extend_from_slice(&self.epoch.to_le_bytes());
488 buf.extend_from_slice(&self.timestamp_ms.to_le_bytes());
489 buf.extend_from_slice(&(self.ciphertext.len() as u32).to_le_bytes());
490 buf.extend_from_slice(&self.ciphertext);
491
492 buf
493 }
494
495 pub fn from_bytes(data: &[u8]) -> Result<Self, crate::MlsError> {
497 let err = |msg: String| crate::MlsError::Deserialization(msg);
498 let mut pos = 0;
499
500 let read_u32 = |pos: &mut usize| -> Result<u32, crate::MlsError> {
501 if *pos + 4 > data.len() {
502 return Err(err("unexpected end of data reading u32".to_string()));
503 }
504 let mut bytes = [0u8; 4];
505 bytes.copy_from_slice(&data[*pos..*pos + 4]);
506 let val = u32::from_le_bytes(bytes);
507 *pos += 4;
508 Ok(val)
509 };
510
511 let read_u64 = |pos: &mut usize| -> Result<u64, crate::MlsError> {
512 if *pos + 8 > data.len() {
513 return Err(err("unexpected end of data reading u64".to_string()));
514 }
515 let mut bytes = [0u8; 8];
516 bytes.copy_from_slice(&data[*pos..*pos + 8]);
517 let val = u64::from_le_bytes(bytes);
518 *pos += 8;
519 Ok(val)
520 };
521
522 let read_bytes = |pos: &mut usize, len: usize| -> Result<Vec<u8>, crate::MlsError> {
523 if *pos + len > data.len() {
524 return Err(err("unexpected end of data reading bytes".to_string()));
525 }
526 let val = data[*pos..*pos + len].to_vec();
527 *pos += len;
528 Ok(val)
529 };
530
531 let group_id_len = read_u32(&mut pos)? as usize;
532 if group_id_len > Self::MAX_STRING_FIELD_LEN {
533 return Err(err(format!(
534 "group_id_len {} exceeds maximum {}",
535 group_id_len,
536 Self::MAX_STRING_FIELD_LEN
537 )));
538 }
539 let group_id = String::from_utf8(read_bytes(&mut pos, group_id_len)?)
540 .map_err(|e| err(format!("invalid group_id UTF-8: {}", e)))?;
541
542 let sender_len = read_u32(&mut pos)? as usize;
543 if sender_len > Self::MAX_STRING_FIELD_LEN {
544 return Err(err(format!(
545 "sender_len {} exceeds maximum {}",
546 sender_len,
547 Self::MAX_STRING_FIELD_LEN
548 )));
549 }
550 let sender_id = String::from_utf8(read_bytes(&mut pos, sender_len)?)
551 .map_err(|e| err(format!("invalid sender_id UTF-8: {}", e)))?;
552
553 if pos + 1 > data.len() {
554 return Err(err(
555 "unexpected end of data reading message_type".to_string()
556 ));
557 }
558 let message_type = MlsMessageType::from_u8(data[pos])
559 .ok_or_else(|| err(format!("unknown message_type tag {}", data[pos])))?;
560 pos += 1;
561
562 let epoch = read_u64(&mut pos)?;
563 let timestamp_ms = read_u64(&mut pos)?;
564
565 let ciphertext_len = read_u32(&mut pos)? as usize;
566 if ciphertext_len > Self::MAX_CIPHERTEXT_LEN {
567 return Err(err(format!(
568 "ciphertext_len {} exceeds maximum {}",
569 ciphertext_len,
570 Self::MAX_CIPHERTEXT_LEN
571 )));
572 }
573 let ciphertext = read_bytes(&mut pos, ciphertext_len)?;
574
575 Ok(Self {
576 group_id: GroupId::new(group_id)?,
577 message_type,
578 epoch,
579 ciphertext,
580 sender_id,
581 timestamp_ms,
582 })
583 }
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct WelcomeMessage {
589 pub group_id: GroupId,
591
592 pub welcome_data: Vec<u8>,
594
595 pub inviter_id: String,
597
598 pub group_name: Option<String>,
600
601 pub timestamp_ms: u64,
603}
604
605impl WelcomeMessage {
606 pub fn to_base64(&self) -> Result<String, crate::MlsError> {
608 let json =
609 serde_json::to_vec(self).map_err(|e| crate::MlsError::Serialization(e.to_string()))?;
610 Ok(base64::Engine::encode(
611 &base64::engine::general_purpose::STANDARD,
612 &json,
613 ))
614 }
615
616 pub fn from_base64(encoded: &str) -> Result<Self, crate::MlsError> {
618 let json = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded)
619 .map_err(|e| crate::MlsError::Deserialization(e.to_string()))?;
620 serde_json::from_slice(&json).map_err(|e| crate::MlsError::Deserialization(e.to_string()))
621 }
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize)]
626pub struct GroupMetadata {
627 pub name: Option<String>,
629
630 pub created_at_ms: u64,
632
633 pub last_activity_ms: u64,
635
636 #[serde(default)]
638 pub custom: std::collections::HashMap<String, String>,
639
640 #[serde(default)]
642 pub created_by: Option<String>,
643
644 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
647 pub roles: std::collections::HashMap<String, GroupRole>,
648}
649
650impl GroupMetadata {
651 pub const LEGACY_ROLE_KEY_PREFIX: &'static str = "role:";
653
654 pub fn new(name: Option<String>) -> Self {
656 let now_ms = chrono::Utc::now().timestamp_millis() as u64;
657 Self {
658 name,
659 created_at_ms: now_ms,
660 last_activity_ms: now_ms,
661 custom: std::collections::HashMap::new(),
662 created_by: None,
663 roles: std::collections::HashMap::new(),
664 }
665 }
666
667 pub fn new_with_creator(name: Option<String>, creator_id: &str) -> Self {
669 let mut meta = Self::new(name);
670 meta.created_by = Some(creator_id.to_string());
671 meta.set_role(creator_id, GroupRole::Admin);
672 meta
673 }
674
675 pub fn migrate_legacy_roles(&mut self) {
679 let legacy_keys: Vec<String> = self
680 .custom
681 .keys()
682 .filter(|k| k.starts_with(Self::LEGACY_ROLE_KEY_PREFIX))
683 .cloned()
684 .collect();
685 for key in legacy_keys {
686 if let Some(uid) = key.strip_prefix(Self::LEGACY_ROLE_KEY_PREFIX) {
687 if !self.roles.contains_key(uid) {
688 let role = self
689 .custom
690 .get(&key)
691 .and_then(|v| v.parse().ok())
692 .unwrap_or_default();
693 self.roles.insert(uid.to_string(), role);
694 }
695 }
696 self.custom.remove(&key);
697 }
698 }
699
700 pub fn touch(&mut self) {
702 self.last_activity_ms = chrono::Utc::now().timestamp_millis() as u64;
703 }
704
705 pub fn get_role(&self, user_id: &str) -> GroupRole {
707 self.roles.get(user_id).copied().unwrap_or_default()
708 }
709
710 pub fn set_role(&mut self, user_id: &str, role: GroupRole) {
712 self.roles.insert(user_id.to_string(), role);
713 }
714
715 pub fn remove_role(&mut self, user_id: &str) {
717 self.roles.remove(user_id);
718 }
719
720 pub fn has_any_admin(&self) -> bool {
722 self.roles.values().any(|r| *r == GroupRole::Admin)
723 }
724
725 pub fn get_all_roles(&self) -> std::collections::HashMap<String, GroupRole> {
727 self.roles.clone()
728 }
729}
730
731#[derive(Debug, Clone, Copy, PartialEq, Eq)]
733pub enum StorageKeyType {
734 Identity,
736
737 KeyPackage,
739
740 GroupState,
742
743 EpochSecrets,
745
746 Credential,
748
749 ContactKeyPackage,
751
752 GroupMetadata,
754
755 PendingWelcome,
757}
758
759impl StorageKeyType {
760 pub fn as_str(&self) -> &'static str {
762 match self {
763 Self::Identity => "identity",
764 Self::KeyPackage => "key_package",
765 Self::GroupState => "group_state",
766 Self::EpochSecrets => "epoch_secrets",
767 Self::Credential => "credential",
768 Self::ContactKeyPackage => "contact_key_package",
769 Self::GroupMetadata => "group_metadata",
770 Self::PendingWelcome => "pending_welcome",
771 }
772 }
773}
774
775impl std::fmt::Display for StorageKeyType {
776 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
777 write!(f, "{}", self.as_str())
778 }
779}
780
781#[cfg(test)]
782mod tests {
783 use super::*;
784 use offline_protocol_core::Address;
785
786 fn disagreeing_addresses() -> (Address, Address) {
796 let mut low_bytes = [0u8; Address::HASH_LEN];
797 low_bytes[0] = 0x00;
798 let mut high_bytes = [0u8; Address::HASH_LEN];
799 high_bytes[0] = 0x40;
800 (
801 Address::from_hash_bytes(low_bytes),
802 Address::from_hash_bytes(high_bytes),
803 )
804 }
805
806 #[test]
807 fn session_id_orders_addresses_by_hash_not_by_string() {
808 let (low, high) = disagreeing_addresses();
809
810 assert!(low < high, "hash-byte order");
812 assert!(
813 low.to_string() > high.to_string(),
814 "rendered-string order must be the opposite, or this test proves nothing"
815 );
816
817 let expected = format!("session:{}:{}", low, high);
818 assert_eq!(
819 GroupId::for_session(&low.to_string(), &high.to_string())
820 .unwrap()
821 .as_str(),
822 expected,
823 "address slots must follow Address order, not string order"
824 );
825 }
826
827 #[test]
828 fn session_id_is_symmetric_for_addresses() {
829 let (low, high) = disagreeing_addresses();
830 let (a, b) = (low.to_string(), high.to_string());
831
832 assert_eq!(
833 GroupId::for_session(&a, &b).unwrap(),
834 GroupId::for_session(&b, &a).unwrap(),
835 "both peers must derive the same slot regardless of argument order"
836 );
837 }
838
839 #[test]
842 fn session_id_falls_back_to_string_order_for_non_addresses() {
843 assert_eq!(
844 GroupId::for_session("bob", "alice").unwrap().as_str(),
845 "session:alice:bob"
846 );
847
848 let (address, _) = disagreeing_addresses();
849 let mixed = GroupId::for_session("alice", &address.to_string()).unwrap();
850 let mixed_reversed = GroupId::for_session(&address.to_string(), "alice").unwrap();
851 assert_eq!(mixed, mixed_reversed, "mixed pairs must stay symmetric");
852 }
853
854 fn sample_encrypted_message() -> EncryptedMessage {
855 EncryptedMessage {
856 group_id: GroupId::for_session("alice", "bob").unwrap(),
857 message_type: MlsMessageType::Application,
858 epoch: 42,
859 ciphertext: vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01],
860 sender_id: "alice".to_string(),
861 timestamp_ms: 1_700_000_000_123,
862 }
863 }
864
865 #[test]
866 fn test_encrypted_message_bytes_roundtrip() {
867 let msg = sample_encrypted_message();
868 let bytes = msg.to_bytes();
869 let decoded = EncryptedMessage::from_bytes(&bytes).unwrap();
870
871 assert_eq!(decoded.group_id, msg.group_id);
872 assert_eq!(decoded.message_type, msg.message_type);
873 assert_eq!(decoded.epoch, msg.epoch);
874 assert_eq!(decoded.ciphertext, msg.ciphertext);
875 assert_eq!(decoded.sender_id, msg.sender_id);
876 assert_eq!(decoded.timestamp_ms, msg.timestamp_ms);
877 }
878
879 #[test]
880 fn test_encrypted_message_bytes_roundtrip_empty_ciphertext() {
881 let mut msg = sample_encrypted_message();
882 msg.ciphertext = Vec::new();
883 let decoded = EncryptedMessage::from_bytes(&msg.to_bytes()).unwrap();
884 assert!(decoded.ciphertext.is_empty());
885 }
886
887 #[test]
888 fn test_encrypted_message_from_bytes_truncated() {
889 let bytes = sample_encrypted_message().to_bytes();
890 for len in 0..bytes.len() {
892 assert!(
893 EncryptedMessage::from_bytes(&bytes[..len]).is_err(),
894 "truncation at {} unexpectedly succeeded",
895 len
896 );
897 }
898 }
899
900 #[test]
901 fn test_encrypted_message_from_bytes_oversized_group_id_rejected() {
902 let mut bytes = Vec::new();
904 bytes.extend_from_slice(&(u32::MAX).to_le_bytes());
905 let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
906 assert!(err.to_string().contains("group_id_len"));
907 }
908
909 #[test]
910 fn test_encrypted_message_from_bytes_oversized_ciphertext_rejected() {
911 let mut msg = sample_encrypted_message();
912 msg.ciphertext = vec![0u8; 4];
913 let mut bytes = msg.to_bytes();
914 let ct_len_offset = bytes.len() - 4 - 4;
916 bytes[ct_len_offset..ct_len_offset + 4].copy_from_slice(&(u32::MAX).to_le_bytes());
917 let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
918 assert!(err.to_string().contains("ciphertext_len"));
919 }
920
921 #[test]
922 fn test_encrypted_message_from_bytes_unknown_message_type_rejected() {
923 let msg = sample_encrypted_message();
924 let mut bytes = msg.to_bytes();
925 let tag_offset = 4 + msg.group_id.as_str().len() + 4 + msg.sender_id.len();
926 bytes[tag_offset] = 0xFF;
927 let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
928 assert!(err.to_string().contains("message_type"));
929 }
930
931 #[test]
932 fn test_group_id_accepts_legitimate_formats() {
933 assert!(GroupId::new("group:0bd6e5f2-3a70-4a4e-9c3f-1c1f2a3b4c5d").is_ok());
934 assert!(GroupId::new("session:alice:bob").is_ok());
935 assert!(GroupId::new("plain-segment_1.x@y").is_ok());
936 }
937
938 #[test]
939 fn test_group_id_rejects_storage_hostile_chars() {
940 assert!(GroupId::new("").is_err());
941 assert!(GroupId::new("group/evil").is_err());
942 assert!(GroupId::new("group\\evil").is_err());
943 assert!(GroupId::new("group\0evil").is_err());
944 assert!(GroupId::new("group\nevil").is_err());
945 assert!(GroupId::new("group\x7Fevil").is_err()); assert!(GroupId::new(".").is_err());
947 assert!(GroupId::new("..").is_err());
948 assert!(GroupId::new("session:..:bob").is_err());
950 assert!(GroupId::new("group:../../etc").is_err());
951 assert!(GroupId::new("session:").is_err());
953 assert!(GroupId::new(":session").is_err());
954 assert!(GroupId::new("a::b").is_err());
955 }
956
957 #[test]
958 fn test_group_id_length_cap() {
959 assert!(GroupId::new("a".repeat(GroupId::MAX_LEN)).is_ok());
960 assert!(GroupId::new("a".repeat(GroupId::MAX_LEN + 1)).is_err());
961 }
962
963 #[test]
964 fn test_group_id_deserialize_rejects_hostile() {
965 assert!(serde_json::from_str::<GroupId>(r#""evil/path""#).is_err());
966 assert!(serde_json::from_str::<GroupId>(r#""..""#).is_err());
967 assert!(serde_json::from_str::<GroupId>(r#""session:alice:bob""#).is_ok());
968 }
969
970 #[test]
971 fn test_encrypted_message_from_bytes_rejects_hostile_group_id() {
972 let mut bytes = Vec::new();
975 let hostile = b"evil/../path";
976 bytes.extend_from_slice(&(hostile.len() as u32).to_le_bytes());
977 bytes.extend_from_slice(hostile);
978 let sender = b"alice";
979 bytes.extend_from_slice(&(sender.len() as u32).to_le_bytes());
980 bytes.extend_from_slice(sender);
981 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();
986 assert!(matches!(err, crate::MlsError::InvalidGroupId(_)));
987 }
988
989 #[test]
990 fn test_mls_message_type_u8_roundtrip() {
991 for msg_type in [
992 MlsMessageType::Application,
993 MlsMessageType::Welcome,
994 MlsMessageType::Commit,
995 MlsMessageType::Proposal,
996 ] {
997 assert_eq!(MlsMessageType::from_u8(msg_type.as_u8()), Some(msg_type));
998 }
999 assert_eq!(MlsMessageType::from_u8(4), None);
1000 }
1001}