1use crate::crypto::account::OlmAccountState;
108use crate::crypto::device_tracker::StoredDevice;
109use crate::crypto::olm_sessions::{DecryptedToDevice, OlmSessionManager};
110use crate::error::MessengerError;
111use crate::ids::{DeviceId, EventId, RoomId, UserId};
112use crate::store::CryptoStore;
113use crate::wire::events::{
114 ForwardedRoomKeyContent, MegolmEncryptedContent, RelatesTo, RoomEncryptedContent, RoomEncryptionContent, RoomKeyContent,
115};
116use mail4agent_vodozemac::megolm::{
117 DecryptionError as MegolmDecryptionError, ExportedSessionKey, GroupSession, InboundGroupSession, MegolmMessage,
118 SessionConfig, SessionKey, SessionOrdering,
119};
120use mail4agent_vodozemac::{Curve25519PublicKey, DecodeError as VodozemacDecodeError, Ed25519PublicKey};
121use serde::{Deserialize, Serialize};
122use std::collections::{BTreeMap, BTreeSet};
123
124const MEGOLM_ALGORITHM: &str = "m.megolm.v1.aes-sha2";
126
127pub const MAX_DEVICES_PER_SEND_TO_DEVICE_REQUEST: usize = 250;
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub enum UtdReason {
149 MissingSession,
151 WithheldBlacklisted,
154 WithheldUnverified,
157 WithheldUnauthorised,
161 SenderIdentityNotVerified,
165 Unknown,
169}
170
171#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
179pub struct RoomEventPlaintext {
180 #[serde(rename = "type")]
182 pub event_type: String,
183 pub content: serde_json::Value,
185 pub room_id: RoomId,
187}
188
189#[derive(Debug, Clone)]
191pub struct OutboundSessionUpdate {
192 pub session_id: String,
194 pub rotated: bool,
197 pub session_key: String,
201 pub new_recipients: Vec<StoredDevice>,
205 pub room_key_content: Option<RoomKeyContent>,
208}
209
210#[derive(Serialize, Deserialize)]
216struct OutboundGroupSessionRecord {
217 session: mail4agent_vodozemac::megolm::GroupSessionPickle,
218 created_at_ms: i64,
221 shared_with: BTreeSet<(UserId, DeviceId)>,
224}
225
226#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
231pub(crate) struct ReplayEntry {
232 event_id: EventId,
233 origin_server_ts: i64,
234}
235
236#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
243pub(crate) struct BackedUpMark {
244 pub(crate) version: String,
250 pub(crate) first_message_index: u32,
256}
257
258#[derive(Serialize, Deserialize)]
267pub(crate) struct InboundGroupSessionRecord {
268 pub(crate) session: mail4agent_vodozemac::megolm::InboundGroupSessionPickle,
269 pub(crate) bound_user_id: UserId,
278 pub(crate) bound_sender_curve25519: String,
280 pub(crate) bound_sender_ed25519: String,
282 pub(crate) replay_index: BTreeMap<u32, ReplayEntry>,
286 #[serde(default)]
291 pub(crate) backed_up: Option<BackedUpMark>,
292}
293
294#[derive(Debug, thiserror::Error)]
298pub enum GroupDecryptError {
299 #[error("no inbound Megolm session for room {room_id} session {session_id}")]
302 MissingSession {
303 room_id: RoomId,
305 session_id: String,
307 },
308 #[error("decrypted payload's room_id {plaintext_room_id} does not match the event's own room {event_room_id}")]
311 WrongRoom {
312 event_room_id: RoomId,
314 plaintext_room_id: RoomId,
316 },
317 #[error("event sender {event_sender} does not match this session's bound sender {bound_user_id}")]
321 WrongSender {
322 bound_user_id: UserId,
324 event_sender: UserId,
326 },
327 #[error("message index {message_index} of session {session_id} was already seen under a different event")]
332 Replay {
333 session_id: String,
335 message_index: u32,
337 },
338 #[error("malformed Megolm ciphertext: {0}")]
340 MalformedCiphertext(#[from] VodozemacDecodeError),
341 #[error("Megolm decryption failed: {0}")]
343 Vodozemac(#[from] MegolmDecryptionError),
344 #[error("decrypted or stored payload was not valid JSON: {0}")]
347 InvalidPayload(#[from] serde_json::Error),
348 #[error("store: {0}")]
350 Store(#[from] crate::store::StoreError),
351}
352
353impl GroupDecryptError {
354 pub fn utd_reason(&self) -> UtdReason {
364 match self {
365 GroupDecryptError::MissingSession { .. } => UtdReason::MissingSession,
366 _ => UtdReason::Unknown,
367 }
368 }
369}
370
371pub(crate) fn load_inbound_record<S: CryptoStore>(
377 store: &S,
378 room_id: &RoomId,
379 session_id: &str,
380) -> Result<Option<InboundGroupSessionRecord>, MessengerError> {
381 let Some(bytes) = store.inbound_group_session(room_id, session_id)? else { return Ok(None) };
382 let record: InboundGroupSessionRecord = serde_json::from_slice(bytes).map_err(|source| {
383 MessengerError::Crypto(format!("decode inbound Megolm session for room {room_id} session {session_id}: {source}"))
384 })?;
385 Ok(Some(record))
386}
387
388pub(crate) fn save_inbound_record<S: CryptoStore>(
392 store: &mut S,
393 room_id: &RoomId,
394 session_id: &str,
395 record: &InboundGroupSessionRecord,
396) -> Result<(), MessengerError> {
397 let bytes = serde_json::to_vec(record).map_err(|source| {
398 MessengerError::Crypto(format!("encode inbound Megolm session for room {room_id} session {session_id}: {source}"))
399 })?;
400 store.save_inbound_group_session(room_id, session_id, bytes)?;
401 Ok(())
402}
403
404pub(crate) enum InboundMergeOutcome {
408 New {
411 record: InboundGroupSessionRecord,
413 },
414 Kept,
419 Merged {
426 record: InboundGroupSessionRecord,
428 },
429 IgnoredSenderMismatch,
436}
437
438pub(crate) fn merge_inbound_session(
449 existing: Option<InboundGroupSessionRecord>,
450 mut incoming_session: InboundGroupSession,
451 sender_user_id: &UserId,
452 sender_curve25519: Curve25519PublicKey,
453 sender_ed25519: Ed25519PublicKey,
454) -> InboundMergeOutcome {
455 let Some(existing_record) = existing else {
456 return InboundMergeOutcome::New {
457 record: InboundGroupSessionRecord {
458 session: incoming_session.pickle(),
459 bound_user_id: sender_user_id.clone(),
460 bound_sender_curve25519: sender_curve25519.to_base64(),
461 bound_sender_ed25519: sender_ed25519.to_base64(),
462 replay_index: BTreeMap::new(),
463 backed_up: None,
464 },
465 };
466 };
467
468 let InboundGroupSessionRecord { session, bound_user_id, bound_sender_curve25519, bound_sender_ed25519, replay_index, backed_up } =
469 existing_record;
470 let mut existing_session = InboundGroupSession::from_pickle(session);
471
472 match existing_session.compare(&mut incoming_session) {
473 SessionOrdering::Equal | SessionOrdering::Better | SessionOrdering::Unconnected => InboundMergeOutcome::Kept,
477 SessionOrdering::Worse => {
478 let sender_matches = bound_user_id == *sender_user_id
479 && bound_sender_curve25519 == sender_curve25519.to_base64()
480 && bound_sender_ed25519 == sender_ed25519.to_base64();
481 if sender_matches {
482 let merged = existing_session
491 .merge(&mut incoming_session)
492 .unwrap_or_else(|| InboundGroupSession::from_pickle(incoming_session.pickle()));
493 InboundMergeOutcome::Merged {
494 record: InboundGroupSessionRecord {
495 session: merged.pickle(),
496 bound_user_id,
497 bound_sender_curve25519,
498 bound_sender_ed25519,
499 replay_index,
500 backed_up,
501 },
502 }
503 } else {
504 InboundMergeOutcome::IgnoredSenderMismatch
509 }
510 }
511 }
512}
513
514pub struct GroupSessionManager;
519
520impl GroupSessionManager {
521 #[allow(clippy::too_many_arguments)]
533 pub fn ensure_outbound_session<S: CryptoStore>(
534 store: &mut S,
535 room_id: &RoomId,
536 our_user_id: &UserId,
537 our_device_id: &DeviceId,
538 encryption: &RoomEncryptionContent,
539 member_user_ids: &BTreeSet<UserId>,
540 known_devices: &[StoredDevice],
541 now_ms: i64,
542 ) -> Result<OutboundSessionUpdate, MessengerError> {
543 let existing_bytes = store.outbound_group_session(room_id)?.map(<[u8]>::to_vec);
544
545 let (group_session, created_at_ms, mut shared_with, rotated) = match existing_bytes {
546 None => (GroupSession::new(SessionConfig::version_1()), now_ms, BTreeSet::new(), true),
547 Some(bytes) => {
548 let record: OutboundGroupSessionRecord = serde_json::from_slice(&bytes).map_err(|source| {
549 MessengerError::Crypto(format!("decode outbound Megolm session for room {room_id}: {source}"))
550 })?;
551 let group_session = GroupSession::from_pickle(record.session);
552 let msgs_exhausted = i64::from(group_session.message_index()) >= encryption.rotation_period_msgs;
553 let time_elapsed = now_ms.saturating_sub(record.created_at_ms) >= encryption.rotation_period_ms;
554 let a_shared_user_left =
555 record.shared_with.iter().any(|(user_id, _)| !member_user_ids.contains(user_id));
556 let a_shared_device_disappeared = record.shared_with.iter().any(|(user_id, device_id)| {
557 !known_devices.iter().any(|device| &device.user_id == user_id && &device.device_id == device_id)
558 });
559 if msgs_exhausted || time_elapsed || a_shared_user_left || a_shared_device_disappeared {
560 (GroupSession::new(SessionConfig::version_1()), now_ms, BTreeSet::new(), true)
561 } else {
562 (group_session, record.created_at_ms, record.shared_with, false)
563 }
564 }
565 };
566
567 let new_recipients: Vec<StoredDevice> = known_devices
568 .iter()
569 .filter(|device| {
570 member_user_ids.contains(&device.user_id)
571 && !device.blocked
572 && !(device.user_id == *our_user_id && device.device_id == *our_device_id)
573 })
574 .filter(|device| !shared_with.contains(&(device.user_id.clone(), device.device_id.clone())))
575 .cloned()
576 .collect();
577
578 let session_id = group_session.session_id();
579 let session_key = group_session.session_key().to_base64();
580 let room_key_content = if new_recipients.is_empty() {
581 None
582 } else {
583 for device in &new_recipients {
584 shared_with.insert((device.user_id.clone(), device.device_id.clone()));
585 }
586 Some(RoomKeyContent {
587 algorithm: MEGOLM_ALGORITHM.to_string(),
588 room_id: room_id.clone(),
589 session_id: session_id.clone(),
590 session_key: session_key.clone(),
591 })
592 };
593
594 if rotated || room_key_content.is_some() {
595 let record = OutboundGroupSessionRecord { session: group_session.pickle(), created_at_ms, shared_with };
596 let bytes = serde_json::to_vec(&record).map_err(|source| {
597 MessengerError::Crypto(format!("encode outbound Megolm session for room {room_id}: {source}"))
598 })?;
599 store.save_outbound_group_session(room_id, bytes)?;
600 }
601
602 Ok(OutboundSessionUpdate { session_id, rotated, session_key, new_recipients, room_key_content })
603 }
604
605 pub fn forget_shared_device<S: CryptoStore>(
611 store: &mut S,
612 room_ids: &[RoomId],
613 user_id: &UserId,
614 device_id: &DeviceId,
615 ) -> Result<usize, MessengerError> {
616 let mut touched = 0usize;
617 for room_id in room_ids {
618 let Some(bytes) = store.outbound_group_session(room_id)?.map(<[u8]>::to_vec) else {
619 continue;
620 };
621 let mut record: OutboundGroupSessionRecord = serde_json::from_slice(&bytes).map_err(|source| {
622 MessengerError::Crypto(format!("decode outbound Megolm session for room {room_id}: {source}"))
623 })?;
624 let before = record.shared_with.len();
625 record.shared_with.retain(|(u, d)| !(u == user_id && d == device_id));
626 if record.shared_with.len() != before {
627 let out = serde_json::to_vec(&record).map_err(|source| {
628 MessengerError::Crypto(format!("encode outbound Megolm session for room {room_id}: {source}"))
629 })?;
630 store.save_outbound_group_session(room_id, out)?;
631 touched += 1;
632 }
633 }
634 Ok(touched)
635 }
636
637 pub fn adopt_own_outbound_session<S: CryptoStore>(
646 store: &mut S,
647 room_id: &RoomId,
648 our_user_id: &UserId,
649 our_curve25519: Curve25519PublicKey,
650 our_ed25519: Ed25519PublicKey,
651 update: &OutboundSessionUpdate,
652 ) -> Result<(), MessengerError> {
653 if !update.rotated {
654 return Ok(());
655 }
656 let content = RoomKeyContent {
657 algorithm: MEGOLM_ALGORITHM.to_string(),
658 room_id: room_id.clone(),
659 session_id: update.session_id.clone(),
660 session_key: update.session_key.clone(),
661 };
662 Self::receive_room_key(store, our_user_id, our_curve25519, our_ed25519, &content)
663 }
664
665 pub fn encrypt_event<S: CryptoStore>(
672 store: &mut S,
673 room_id: &RoomId,
674 sender_curve25519_b64: &str,
675 sender_device_id: &DeviceId,
676 event_type: &str,
677 content: serde_json::Value,
678 relates_to: Option<RelatesTo>,
679 ) -> Result<MegolmEncryptedContent, MessengerError> {
680 let bytes = store
681 .outbound_group_session(room_id)?
682 .map(<[u8]>::to_vec)
683 .ok_or_else(|| MessengerError::Crypto(format!("no outbound Megolm session for room {room_id} yet")))?;
684 let mut record: OutboundGroupSessionRecord = serde_json::from_slice(&bytes).map_err(|source| {
685 MessengerError::Crypto(format!("decode outbound Megolm session for room {room_id}: {source}"))
686 })?;
687 let mut group_session = GroupSession::from_pickle(record.session);
688
689 let plaintext = RoomEventPlaintext { event_type: event_type.to_string(), content, room_id: room_id.clone() };
690 let plaintext_bytes = serde_json::to_vec(&plaintext)?;
691 let message = group_session.encrypt(plaintext_bytes);
692 let session_id = group_session.session_id();
693
694 record.session = group_session.pickle();
695 let record_bytes = serde_json::to_vec(&record).map_err(|source| {
696 MessengerError::Crypto(format!("encode outbound Megolm session for room {room_id}: {source}"))
697 })?;
698 store.save_outbound_group_session(room_id, record_bytes)?;
699
700 Ok(MegolmEncryptedContent {
701 ciphertext: message.to_base64(),
702 session_id,
703 sender_key: Some(sender_curve25519_b64.to_string()),
704 device_id: Some(sender_device_id.clone()),
705 relates_to,
706 })
707 }
708
709 pub fn chunk_recipients_for_send_to_device(recipients: &[StoredDevice]) -> std::slice::Chunks<'_, StoredDevice> {
717 recipients.chunks(MAX_DEVICES_PER_SEND_TO_DEVICE_REQUEST)
718 }
719
720 pub fn build_room_key_send_to_device_body<S: CryptoStore>(
728 store: &mut S,
729 account: &OlmAccountState,
730 our_user_id: &UserId,
731 our_device_id: &DeviceId,
732 chunk: &[StoredDevice],
733 room_key_content: &RoomKeyContent,
734 ) -> Result<serde_json::Value, MessengerError> {
735 let content_value = serde_json::to_value(room_key_content)?;
736 Self::build_encrypted_to_device_body(
737 store,
738 account,
739 our_user_id,
740 our_device_id,
741 chunk,
742 "m.room_key",
743 content_value,
744 )
745 }
746
747 pub fn build_encrypted_to_device_body<S: CryptoStore>(
752 store: &mut S,
753 account: &OlmAccountState,
754 our_user_id: &UserId,
755 our_device_id: &DeviceId,
756 chunk: &[StoredDevice],
757 event_type: &str,
758 content_value: serde_json::Value,
759 ) -> Result<serde_json::Value, MessengerError> {
760 let mut by_user: BTreeMap<UserId, serde_json::Map<String, serde_json::Value>> = BTreeMap::new();
761 for device in chunk {
762 let encrypted = OlmSessionManager::encrypt_to_device(
763 store,
764 account,
765 our_user_id,
766 our_device_id,
767 device,
768 event_type,
769 content_value.clone(),
770 )?;
771 let wire_content = serde_json::to_value(RoomEncryptedContent::Olm(encrypted))?;
772 by_user.entry(device.user_id.clone()).or_default().insert(device.device_id.as_str().to_string(), wire_content);
773 }
774 let mut top = serde_json::Map::new();
775 for (user_id, devices) in by_user {
776 top.insert(user_id.as_str().to_string(), serde_json::Value::Object(devices));
777 }
778 Ok(serde_json::json!({ "messages": serde_json::Value::Object(top) }))
779 }
780
781 pub fn forwarded_room_key_content<S: CryptoStore>(
785 store: &S,
786 room_id: &RoomId,
787 session_id: &str,
788 our_curve25519_b64: &str,
789 ) -> Result<Option<ForwardedRoomKeyContent>, MessengerError> {
790 let Some(record) = load_inbound_record(store, room_id, session_id)? else { return Ok(None) };
791 let InboundGroupSessionRecord { session, bound_sender_curve25519, bound_sender_ed25519, .. } = record;
792 let inbound = InboundGroupSession::from_pickle(session);
793 if inbound.session_id() != session_id {
794 return Ok(None);
795 }
796 let exported = inbound.export_at_first_known_index();
797 Ok(Some(ForwardedRoomKeyContent {
798 algorithm: MEGOLM_ALGORITHM.to_string(),
799 room_id: room_id.clone(),
800 session_id: session_id.to_string(),
801 session_key: exported.to_base64(),
802 sender_key: bound_sender_curve25519,
803 sender_claimed_ed25519_key: Some(bound_sender_ed25519),
804 forwarding_curve25519_key_chain: vec![our_curve25519_b64.to_string()],
805 }))
806 }
807
808 pub fn outbound_room_key_content<S: CryptoStore>(
813 store: &S,
814 room_id: &RoomId,
815 session_id: &str,
816 ) -> Result<Option<RoomKeyContent>, MessengerError> {
817 let Some(bytes) = store.outbound_group_session(room_id)?.map(<[u8]>::to_vec) else { return Ok(None) };
818 let record: OutboundGroupSessionRecord = serde_json::from_slice(&bytes).map_err(|source| {
819 MessengerError::Crypto(format!("decode outbound Megolm session for room {room_id}: {source}"))
820 })?;
821 let session = GroupSession::from_pickle(record.session);
822 if session.session_id() != session_id {
823 return Ok(None);
824 }
825 Ok(Some(RoomKeyContent {
826 algorithm: MEGOLM_ALGORITHM.to_string(),
827 room_id: room_id.clone(),
828 session_id: session_id.to_string(),
829 session_key: session.session_key().to_base64(),
830 }))
831 }
832
833 pub fn accept_room_key_from_to_device<S: CryptoStore>(
843 store: &mut S,
844 decrypted: &DecryptedToDevice,
845 ) -> Result<bool, MessengerError> {
846 if decrypted.event_type != "m.room_key" {
847 return Ok(false);
848 }
849 let content: RoomKeyContent = serde_json::from_value(decrypted.content.clone())?;
850 if content.algorithm != MEGOLM_ALGORITHM {
851 return Ok(false);
852 }
853 Self::receive_room_key(store, &decrypted.sender, decrypted.sender_device_curve25519, decrypted.sender_ed25519, &content)?;
854 Ok(true)
855 }
856
857 pub fn receive_room_key<S: CryptoStore>(
864 store: &mut S,
865 sender_user_id: &UserId,
866 sender_curve25519: Curve25519PublicKey,
867 sender_ed25519: Ed25519PublicKey,
868 content: &RoomKeyContent,
869 ) -> Result<(), MessengerError> {
870 if content.algorithm != MEGOLM_ALGORITHM {
871 return Ok(());
872 }
873 let session_key = SessionKey::from_base64(&content.session_key)
874 .map_err(|source| MessengerError::Crypto(format!("malformed Megolm session_key: {source}")))?;
875 let incoming_session = InboundGroupSession::new(&session_key, SessionConfig::version_1());
876
877 let existing = load_inbound_record(store, &content.room_id, &content.session_id)?;
878 match merge_inbound_session(existing, incoming_session, sender_user_id, sender_curve25519, sender_ed25519) {
879 InboundMergeOutcome::New { record } | InboundMergeOutcome::Merged { record } => {
882 save_inbound_record(store, &content.room_id, &content.session_id, &record)
883 }
884 InboundMergeOutcome::Kept | InboundMergeOutcome::IgnoredSenderMismatch => Ok(()),
889 }
890 }
891
892 pub fn receive_forwarded_room_key<S: CryptoStore>(
898 store: &mut S,
899 bound_user_id: &UserId,
900 bound_curve25519: Curve25519PublicKey,
901 bound_ed25519: Ed25519PublicKey,
902 content: &ForwardedRoomKeyContent,
903 ) -> Result<bool, MessengerError> {
904 if content.algorithm != MEGOLM_ALGORITHM {
905 return Ok(false);
906 }
907 let exported = ExportedSessionKey::from_base64(&content.session_key)
908 .map_err(|source| MessengerError::Crypto(format!("malformed forwarded Megolm session_key: {source}")))?;
909 let incoming = InboundGroupSession::import(&exported, SessionConfig::version_1());
910 if incoming.session_id() != content.session_id {
911 return Ok(false);
912 }
913 let existing = load_inbound_record(store, &content.room_id, &content.session_id)?;
914 match merge_inbound_session(existing, incoming, bound_user_id, bound_curve25519, bound_ed25519) {
915 InboundMergeOutcome::New { record } | InboundMergeOutcome::Merged { record } => {
916 save_inbound_record(store, &content.room_id, &content.session_id, &record)?;
917 Ok(true)
918 }
919 InboundMergeOutcome::Kept | InboundMergeOutcome::IgnoredSenderMismatch => Ok(false),
920 }
921 }
922
923 pub fn decrypt_event<S: CryptoStore>(
926 store: &mut S,
927 room_id: &RoomId,
928 event_id: &EventId,
929 origin_server_ts: i64,
930 sender: &UserId,
931 content: &MegolmEncryptedContent,
932 ) -> Result<RoomEventPlaintext, GroupDecryptError> {
933 let bytes = store.inbound_group_session(room_id, &content.session_id)?.map(<[u8]>::to_vec).ok_or_else(|| {
934 GroupDecryptError::MissingSession { room_id: room_id.clone(), session_id: content.session_id.clone() }
935 })?;
936 let mut record: InboundGroupSessionRecord = serde_json::from_slice(&bytes)?;
937 let mut session = InboundGroupSession::from_pickle(record.session);
938
939 let message = MegolmMessage::from_base64(&content.ciphertext)?;
940 let decrypted = session.decrypt(&message)?;
941 let plaintext: RoomEventPlaintext = serde_json::from_slice(&decrypted.plaintext)?;
942
943 if plaintext.room_id != *room_id {
944 return Err(GroupDecryptError::WrongRoom {
945 event_room_id: room_id.clone(),
946 plaintext_room_id: plaintext.room_id,
947 });
948 }
949 if record.bound_user_id != *sender {
950 return Err(GroupDecryptError::WrongSender {
951 bound_user_id: record.bound_user_id,
952 event_sender: sender.clone(),
953 });
954 }
955
956 match record.replay_index.get(&decrypted.message_index) {
957 Some(seen) if seen.event_id != *event_id || seen.origin_server_ts != origin_server_ts => {
958 return Err(GroupDecryptError::Replay {
959 session_id: content.session_id.clone(),
960 message_index: decrypted.message_index,
961 });
962 }
963 Some(_) => {
964 }
966 None => {
967 record
968 .replay_index
969 .insert(decrypted.message_index, ReplayEntry { event_id: event_id.clone(), origin_server_ts });
970 record.session = session.pickle();
971 let bytes = serde_json::to_vec(&record)?;
972 store.save_inbound_group_session(room_id, &content.session_id, bytes)?;
973 }
974 }
975
976 Ok(plaintext)
977 }
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983 use crate::room::timeline::{ItemContent, Timeline};
984 use crate::store::{InsecurePlainCodecForTests, Store};
985 use crate::wire::events::RawEvent;
986 use mail4agent_vodozemac::olm::Account;
987
988 fn new_store() -> Store<InsecurePlainCodecForTests> {
989 Store::new(DeviceId::parse("DEV").expect("valid device id"), InsecurePlainCodecForTests)
990 }
991
992 fn user(name: &str) -> UserId {
993 UserId::parse(format!("@{name}:example.org")).expect("valid user id")
994 }
995
996 fn device(name: &str) -> DeviceId {
997 DeviceId::parse(name).expect("valid device id")
998 }
999
1000 fn room(name: &str) -> RoomId {
1001 RoomId::parse(format!("!{name}:example.org")).expect("valid room id")
1002 }
1003
1004 fn stored_device(user_id: &UserId, device_id: &DeviceId) -> StoredDevice {
1005 let identity = Account::new().identity_keys();
1006 StoredDevice {
1007 user_id: user_id.clone(),
1008 device_id: device_id.clone(),
1009 curve25519: identity.curve25519,
1010 ed25519: identity.ed25519,
1011 algorithms: vec!["m.olm.v1.curve25519-aes-sha2".to_string(), "m.megolm.v1.aes-sha2".to_string()],
1012 display_name: None,
1013 verified: false,
1014 blocked: false,
1015 }
1016 }
1017
1018 fn default_encryption() -> RoomEncryptionContent {
1019 RoomEncryptionContent {
1020 algorithm: MEGOLM_ALGORITHM.to_string(),
1021 rotation_period_ms: 604_800_000,
1022 rotation_period_msgs: 100,
1023 }
1024 }
1025
1026 fn plaintext_bytes(room_id: &RoomId, body: &str) -> Vec<u8> {
1027 serde_json::to_vec(&RoomEventPlaintext {
1028 event_type: "m.room.message".to_string(),
1029 content: serde_json::json!({ "body": body }),
1030 room_id: room_id.clone(),
1031 })
1032 .expect("valid JSON")
1033 }
1034
1035 #[test]
1036 fn outbound_session_rotates_after_rotation_period_msgs() {
1037 let mut store = new_store();
1038 let room_id = room("r");
1039 let alice = user("alice");
1040 let alice_dev = device("ALICEDEV");
1041 let bob = user("bob");
1042 let bob_device = stored_device(&bob, &device("BOBDEV"));
1043 let members: BTreeSet<UserId> = [alice.clone(), bob.clone()].into_iter().collect();
1044 let known = vec![bob_device];
1045 let encryption =
1046 RoomEncryptionContent { algorithm: MEGOLM_ALGORITHM.to_string(), rotation_period_ms: 604_800_000, rotation_period_msgs: 2 };
1047
1048 let first = GroupSessionManager::ensure_outbound_session(&mut store, &room_id, &alice, &alice_dev, &encryption, &members, &known, 0)
1049 .expect("create session");
1050 assert!(first.rotated);
1051 assert!(first.room_key_content.is_some());
1052
1053 GroupSessionManager::encrypt_event(&mut store, &room_id, "alice-curve", &alice_dev, "m.room.message", serde_json::json!({}), None)
1054 .expect("encrypt 1");
1055 GroupSessionManager::encrypt_event(&mut store, &room_id, "alice-curve", &alice_dev, "m.room.message", serde_json::json!({}), None)
1056 .expect("encrypt 2");
1057
1058 let second = GroupSessionManager::ensure_outbound_session(&mut store, &room_id, &alice, &alice_dev, &encryption, &members, &known, 0)
1059 .expect("re-check");
1060 assert!(second.rotated, "message-count threshold reached");
1061 assert_ne!(second.session_id, first.session_id);
1062 }
1063
1064 #[test]
1065 fn outbound_session_rotates_after_rotation_period_ms() {
1066 let mut store = new_store();
1067 let room_id = room("r");
1068 let alice = user("alice");
1069 let alice_dev = device("ALICEDEV");
1070 let bob = user("bob");
1071 let bob_device = stored_device(&bob, &device("BOBDEV"));
1072 let members: BTreeSet<UserId> = [alice.clone(), bob.clone()].into_iter().collect();
1073 let known = vec![bob_device];
1074 let encryption =
1075 RoomEncryptionContent { algorithm: MEGOLM_ALGORITHM.to_string(), rotation_period_ms: 1_000, rotation_period_msgs: 100 };
1076
1077 let first = GroupSessionManager::ensure_outbound_session(&mut store, &room_id, &alice, &alice_dev, &encryption, &members, &known, 0)
1078 .expect("create session");
1079
1080 let not_yet =
1081 GroupSessionManager::ensure_outbound_session(&mut store, &room_id, &alice, &alice_dev, &encryption, &members, &known, 500)
1082 .expect("re-check before the period elapses");
1083 assert!(!not_yet.rotated, "rotation_period_ms has not elapsed yet");
1084 assert_eq!(not_yet.session_id, first.session_id);
1085
1086 let elapsed =
1087 GroupSessionManager::ensure_outbound_session(&mut store, &room_id, &alice, &alice_dev, &encryption, &members, &known, 1_000)
1088 .expect("re-check after the period elapses");
1089 assert!(elapsed.rotated, "rotation_period_ms has elapsed");
1090 assert_ne!(elapsed.session_id, first.session_id);
1091 }
1092
1093 #[test]
1094 fn outbound_session_rotates_on_member_removed() {
1095 let mut store = new_store();
1096 let room_id = room("r");
1097 let alice = user("alice");
1098 let alice_dev = device("ALICEDEV");
1099 let bob = user("bob");
1100 let bob_device = stored_device(&bob, &device("BOBDEV"));
1101 let members: BTreeSet<UserId> = [alice.clone(), bob.clone()].into_iter().collect();
1102 let known = vec![bob_device];
1103 let encryption = default_encryption();
1104
1105 let first = GroupSessionManager::ensure_outbound_session(&mut store, &room_id, &alice, &alice_dev, &encryption, &members, &known, 0)
1106 .expect("create+share");
1107
1108 let members_after: BTreeSet<UserId> = [alice.clone()].into_iter().collect();
1109 let second =
1110 GroupSessionManager::ensure_outbound_session(&mut store, &room_id, &alice, &alice_dev, &encryption, &members_after, &known, 0)
1111 .expect("bob removed");
1112 assert!(second.rotated, "a previously-shared user no longer being a member forces rotation");
1113 assert_ne!(second.session_id, first.session_id);
1114 assert!(second.new_recipients.is_empty(), "bob is no longer eligible, nothing new to share");
1115 }
1116
1117 #[test]
1118 fn outbound_session_rotates_when_a_shared_device_disappears() {
1119 let mut store = new_store();
1120 let room_id = room("r");
1121 let alice = user("alice");
1122 let alice_dev = device("ALICEDEV");
1123 let bob = user("bob");
1124 let bob_old_device = stored_device(&bob, &device("BOBOLD"));
1125 let bob_new_device = stored_device(&bob, &device("BOBNEW"));
1126 let members: BTreeSet<UserId> = [alice.clone(), bob.clone()].into_iter().collect();
1127 let encryption = default_encryption();
1128
1129 let first = GroupSessionManager::ensure_outbound_session(
1130 &mut store,
1131 &room_id,
1132 &alice,
1133 &alice_dev,
1134 &encryption,
1135 &members,
1136 std::slice::from_ref(&bob_old_device),
1137 0,
1138 )
1139 .expect("create+share with the old device");
1140
1141 let second = GroupSessionManager::ensure_outbound_session(
1144 &mut store,
1145 &room_id,
1146 &alice,
1147 &alice_dev,
1148 &encryption,
1149 &members,
1150 std::slice::from_ref(&bob_new_device),
1151 0,
1152 )
1153 .expect("old device gone");
1154 assert!(second.rotated, "a previously-shared device disappearing forces rotation");
1155 assert_ne!(second.session_id, first.session_id);
1156 assert_eq!(second.new_recipients, vec![bob_new_device]);
1157 }
1158
1159 #[test]
1160 fn new_device_gets_current_session_without_rotation() {
1161 let mut store = new_store();
1162 let room_id = room("r");
1163 let alice = user("alice");
1164 let alice_dev = device("ALICEDEV");
1165 let bob = user("bob");
1166 let bob_device_1 = stored_device(&bob, &device("BOBDEV1"));
1167 let bob_device_2 = stored_device(&bob, &device("BOBDEV2"));
1168 let members: BTreeSet<UserId> = [alice.clone(), bob.clone()].into_iter().collect();
1169 let encryption = default_encryption();
1170
1171 let first = GroupSessionManager::ensure_outbound_session(
1172 &mut store,
1173 &room_id,
1174 &alice,
1175 &alice_dev,
1176 &encryption,
1177 &members,
1178 std::slice::from_ref(&bob_device_1),
1179 0,
1180 )
1181 .expect("create+share with device 1");
1182
1183 GroupSessionManager::encrypt_event(&mut store, &room_id, "alice-curve", &alice_dev, "m.room.message", serde_json::json!({}), None)
1184 .expect("encrypt one message, advancing the ratchet to index 1");
1185
1186 let known = vec![bob_device_1.clone(), bob_device_2.clone()];
1187 let second = GroupSessionManager::ensure_outbound_session(&mut store, &room_id, &alice, &alice_dev, &encryption, &members, &known, 0)
1188 .expect("device 2 appears");
1189 assert!(!second.rotated, "a new device of an already-shared member never forces rotation");
1190 assert_eq!(second.session_id, first.session_id);
1191 assert_eq!(second.new_recipients, vec![bob_device_2]);
1192
1193 let shared = second.room_key_content.expect("something to share with device 2");
1196 let session_key = SessionKey::from_base64(&shared.session_key).expect("valid session key");
1197 let inbound = InboundGroupSession::new(&session_key, SessionConfig::version_1());
1198 assert_eq!(inbound.first_known_index(), 1);
1199 }
1200
1201 #[test]
1202 fn room_key_from_a_room_event_is_ignored() {
1203 let event: RawEvent = serde_json::from_value(serde_json::json!({
1211 "event_id": "$x:example.org",
1212 "type": "m.room_key",
1213 "sender": "@mallory:example.org",
1214 "origin_server_ts": 1,
1215 "content": {
1216 "algorithm": "m.megolm.v1.aes-sha2",
1217 "room_id": "!r:example.org",
1218 "session_id": "abc",
1219 "session_key": "irrelevant"
1220 }
1221 }))
1222 .expect("valid raw event shape");
1223
1224 let mut timeline = Timeline::new();
1225 timeline.apply_timeline_batch(&[event], false, None);
1226
1227 assert_eq!(timeline.items().len(), 1);
1228 assert_eq!(timeline.items()[0].content, ItemContent::Unknown);
1229 }
1230
1231 #[test]
1232 fn decrypted_payload_for_another_room_is_rejected() {
1233 let mut alice_store = new_store();
1234 let room_a = room("a");
1235 let room_b = room("b");
1236 let alice = user("alice");
1237 let alice_dev = device("ALICEDEV");
1238 let bob = user("bob");
1239 let bob_device = stored_device(&bob, &device("BOBDEV"));
1240 let members: BTreeSet<UserId> = [alice.clone(), bob.clone()].into_iter().collect();
1241 let encryption = default_encryption();
1242 let identity = Account::new().identity_keys();
1243
1244 let update = GroupSessionManager::ensure_outbound_session(
1245 &mut alice_store,
1246 &room_a,
1247 &alice,
1248 &alice_dev,
1249 &encryption,
1250 &members,
1251 std::slice::from_ref(&bob_device),
1252 0,
1253 )
1254 .expect("create+share");
1255 let mut room_key_content = update.room_key_content.expect("bob is a new recipient");
1256
1257 let encrypted = GroupSessionManager::encrypt_event(
1258 &mut alice_store,
1259 &room_a,
1260 &identity.curve25519.to_base64(),
1261 &alice_dev,
1262 "m.room.message",
1263 serde_json::json!({ "body": "hi" }),
1264 None,
1265 )
1266 .expect("encrypt under room A");
1267
1268 room_key_content.room_id = room_b.clone();
1272 let mut bob_store = new_store();
1273 GroupSessionManager::receive_room_key(&mut bob_store, &alice, identity.curve25519, identity.ed25519, &room_key_content)
1274 .expect("receive");
1275
1276 let event_id = EventId::parse("$evt:example.org").expect("valid event id");
1277 let result = GroupSessionManager::decrypt_event(&mut bob_store, &room_b, &event_id, 1, &alice, &encrypted);
1278 assert!(matches!(result, Err(GroupDecryptError::WrongRoom { .. })), "got {result:?}");
1279 }
1280
1281 #[test]
1282 fn session_cannot_authenticate_another_sender() {
1283 let mut alice_store = new_store();
1284 let room_id = room("r");
1285 let alice = user("alice");
1286 let alice_dev = device("ALICEDEV");
1287 let mallory = user("mallory");
1288 let bob = user("bob");
1289 let bob_device = stored_device(&bob, &device("BOBDEV"));
1290 let members: BTreeSet<UserId> = [alice.clone(), bob.clone()].into_iter().collect();
1291 let encryption = default_encryption();
1292 let identity = Account::new().identity_keys();
1293
1294 let update = GroupSessionManager::ensure_outbound_session(
1295 &mut alice_store,
1296 &room_id,
1297 &alice,
1298 &alice_dev,
1299 &encryption,
1300 &members,
1301 std::slice::from_ref(&bob_device),
1302 0,
1303 )
1304 .expect("create+share");
1305 let room_key_content = update.room_key_content.expect("bob is a new recipient");
1306
1307 let mut bob_store = new_store();
1308 GroupSessionManager::receive_room_key(&mut bob_store, &alice, identity.curve25519, identity.ed25519, &room_key_content)
1309 .expect("bob genuinely received this key from alice");
1310
1311 let encrypted = GroupSessionManager::encrypt_event(
1312 &mut alice_store,
1313 &room_id,
1314 &identity.curve25519.to_base64(),
1315 &alice_dev,
1316 "m.room.message",
1317 serde_json::json!({ "body": "hi" }),
1318 None,
1319 )
1320 .expect("encrypt");
1321
1322 let event_id = EventId::parse("$evt:example.org").expect("valid event id");
1324 let result = GroupSessionManager::decrypt_event(&mut bob_store, &room_id, &event_id, 1, &mallory, &encrypted);
1325 assert!(matches!(result, Err(GroupDecryptError::WrongSender { .. })), "got {result:?}");
1326 }
1327
1328 #[test]
1329 fn inbound_session_rejects_replayed_message_index() {
1330 let mut alice_store = new_store();
1331 let room_id = room("r");
1332 let alice = user("alice");
1333 let alice_dev = device("ALICEDEV");
1334 let bob = user("bob");
1335 let bob_device = stored_device(&bob, &device("BOBDEV"));
1336 let members: BTreeSet<UserId> = [alice.clone(), bob.clone()].into_iter().collect();
1337 let encryption = default_encryption();
1338 let identity = Account::new().identity_keys();
1339
1340 let update = GroupSessionManager::ensure_outbound_session(
1341 &mut alice_store,
1342 &room_id,
1343 &alice,
1344 &alice_dev,
1345 &encryption,
1346 &members,
1347 std::slice::from_ref(&bob_device),
1348 0,
1349 )
1350 .expect("create+share");
1351 let room_key_content = update.room_key_content.expect("bob is a new recipient");
1352
1353 let mut bob_store = new_store();
1354 GroupSessionManager::receive_room_key(&mut bob_store, &alice, identity.curve25519, identity.ed25519, &room_key_content)
1355 .expect("receive");
1356
1357 let encrypted = GroupSessionManager::encrypt_event(
1358 &mut alice_store,
1359 &room_id,
1360 &identity.curve25519.to_base64(),
1361 &alice_dev,
1362 "m.room.message",
1363 serde_json::json!({ "body": "hi" }),
1364 None,
1365 )
1366 .expect("encrypt");
1367
1368 let event_id_1 = EventId::parse("$e1:example.org").expect("valid event id");
1369 let first = GroupSessionManager::decrypt_event(&mut bob_store, &room_id, &event_id_1, 100, &alice, &encrypted)
1370 .expect("first decrypt succeeds");
1371 assert_eq!(first.content, serde_json::json!({ "body": "hi" }));
1372
1373 let redelivered = GroupSessionManager::decrypt_event(&mut bob_store, &room_id, &event_id_1, 100, &alice, &encrypted)
1374 .expect("redelivery of the exact same event is not a replay");
1375 assert_eq!(redelivered.content, serde_json::json!({ "body": "hi" }));
1376
1377 let event_id_2 = EventId::parse("$e2:example.org").expect("valid event id");
1378 let replay = GroupSessionManager::decrypt_event(&mut bob_store, &room_id, &event_id_2, 200, &alice, &encrypted);
1379 assert!(matches!(replay, Err(GroupDecryptError::Replay { .. })), "got {replay:?}");
1380 }
1381
1382 #[test]
1383 fn later_share_does_not_replace_earlier_index() {
1384 let alice = user("alice");
1385 let identity = Account::new().identity_keys();
1386 let room_id = room("r");
1387
1388 let mut group_session = GroupSession::new(SessionConfig::version_1());
1389 let key_at_0 = group_session.session_key();
1390 let message_0 = group_session.encrypt(plaintext_bytes(&room_id, "msg0"));
1391 let key_at_1 = group_session.session_key();
1392
1393 let content_at_1 = RoomKeyContent {
1394 algorithm: MEGOLM_ALGORITHM.to_string(),
1395 room_id: room_id.clone(),
1396 session_id: group_session.session_id(),
1397 session_key: key_at_1.to_base64(),
1398 };
1399 let content_at_0 = RoomKeyContent { session_key: key_at_0.to_base64(), ..content_at_1.clone() };
1400
1401 let mut bob_store = new_store();
1402 GroupSessionManager::receive_room_key(&mut bob_store, &alice, identity.curve25519, identity.ed25519, &content_at_1)
1403 .expect("receive the later (higher-index) share first");
1404 GroupSessionManager::receive_room_key(&mut bob_store, &alice, identity.curve25519, identity.ed25519, &content_at_0)
1405 .expect("receive the earlier (lower-index) share second");
1406
1407 let encrypted_0 = MegolmEncryptedContent {
1408 ciphertext: message_0.to_base64(),
1409 session_id: group_session.session_id(),
1410 sender_key: None,
1411 device_id: None,
1412 relates_to: None,
1413 };
1414 let event_id = EventId::parse("$evt0:example.org").expect("valid event id");
1415 let decrypted = GroupSessionManager::decrypt_event(&mut bob_store, &room_id, &event_id, 1, &alice, &encrypted_0)
1416 .expect("the earlier share's lower index must have won, so message 0 is decryptable");
1417 assert_eq!(decrypted.content, serde_json::json!({ "body": "msg0" }));
1418 }
1419
1420 #[test]
1421 fn lower_index_share_from_a_different_sender_does_not_rebind() {
1422 let alice = user("alice");
1423 let mallory = user("mallory");
1424 let alice_identity = Account::new().identity_keys();
1425 let mallory_identity = Account::new().identity_keys();
1426 let room_id = room("r");
1427
1428 let mut group_session = GroupSession::new(SessionConfig::version_1());
1429 let key_at_0 = group_session.session_key();
1435 let message_0 = group_session.encrypt(plaintext_bytes(&room_id, "msg0"));
1436 let key_at_1 = group_session.session_key();
1437 let message_1 = group_session.encrypt(plaintext_bytes(&room_id, "msg1"));
1438
1439 let content_at_1 = RoomKeyContent {
1440 algorithm: MEGOLM_ALGORITHM.to_string(),
1441 room_id: room_id.clone(),
1442 session_id: group_session.session_id(),
1443 session_key: key_at_1.to_base64(),
1444 };
1445 let content_at_0 = RoomKeyContent { session_key: key_at_0.to_base64(), ..content_at_1.clone() };
1446
1447 let mut bob_store = new_store();
1448 GroupSessionManager::receive_room_key(&mut bob_store, &alice, alice_identity.curve25519, alice_identity.ed25519, &content_at_1)
1451 .expect("receive alice's genuine share at index 1");
1452
1453 GroupSessionManager::receive_room_key(
1457 &mut bob_store,
1458 &mallory,
1459 mallory_identity.curve25519,
1460 mallory_identity.ed25519,
1461 &content_at_0,
1462 )
1463 .expect("receiving mallory's share is not itself an error -- it's just ignored");
1464
1465 let encrypted_1 = MegolmEncryptedContent {
1470 ciphertext: message_1.to_base64(),
1471 session_id: group_session.session_id(),
1472 sender_key: None,
1473 device_id: None,
1474 relates_to: None,
1475 };
1476 let event_id_1 = EventId::parse("$evt1:example.org").expect("valid event id");
1477 let claimed_mallory = GroupSessionManager::decrypt_event(&mut bob_store, &room_id, &event_id_1, 1, &mallory, &encrypted_1);
1478 assert!(
1479 matches!(claimed_mallory, Err(GroupDecryptError::WrongSender { .. })),
1480 "record is still bound to alice, not mallory: {claimed_mallory:?}"
1481 );
1482 let claimed_alice = GroupSessionManager::decrypt_event(&mut bob_store, &room_id, &event_id_1, 1, &alice, &encrypted_1)
1483 .expect("the record is genuinely bound to alice, so an event she sent decrypts fine");
1484 assert_eq!(claimed_alice.content, serde_json::json!({ "body": "msg1" }));
1485
1486 let encrypted_0 = MegolmEncryptedContent {
1490 ciphertext: message_0.to_base64(),
1491 session_id: group_session.session_id(),
1492 sender_key: None,
1493 device_id: None,
1494 relates_to: None,
1495 };
1496 let event_id_0 = EventId::parse("$evt0:example.org").expect("valid event id");
1497 let still_index_1 = GroupSessionManager::decrypt_event(&mut bob_store, &room_id, &event_id_0, 2, &alice, &encrypted_0);
1498 assert!(
1499 matches!(still_index_1, Err(GroupDecryptError::Vodozemac(MegolmDecryptionError::UnknownMessageIndex(..)))),
1500 "the ratchet never advanced to index 0: {still_index_1:?}"
1501 );
1502 }
1503
1504 #[test]
1505 fn old_record_without_backed_up_field_deserializes() {
1506 let mut store = new_store();
1507 let room_id = room("r");
1508 let alice = user("alice");
1509 let alice_dev = device("ALICEDEV");
1510 let bob = user("bob");
1511 let bob_device = stored_device(&bob, &device("BOBDEV"));
1512 let members: BTreeSet<UserId> = [alice.clone(), bob.clone()].into_iter().collect();
1513 let encryption = default_encryption();
1514 let identity = Account::new().identity_keys();
1515
1516 let mut alice_store = new_store();
1517 let update = GroupSessionManager::ensure_outbound_session(
1518 &mut alice_store,
1519 &room_id,
1520 &alice,
1521 &alice_dev,
1522 &encryption,
1523 &members,
1524 std::slice::from_ref(&bob_device),
1525 0,
1526 )
1527 .expect("create+share");
1528 let room_key_content = update.room_key_content.expect("bob is a new recipient");
1529
1530 GroupSessionManager::receive_room_key(&mut store, &alice, identity.curve25519, identity.ed25519, &room_key_content)
1531 .expect("receive");
1532
1533 let bytes = store
1534 .inbound_group_session(&room_id, &room_key_content.session_id)
1535 .expect("no error")
1536 .expect("record exists");
1537
1538 let mut value: serde_json::Value = serde_json::from_slice(bytes).expect("valid JSON");
1541 value.as_object_mut().expect("a JSON object").remove("backed_up");
1542 let old_bytes = serde_json::to_vec(&value).expect("valid JSON");
1543
1544 let record: InboundGroupSessionRecord =
1545 serde_json::from_slice(&old_bytes).expect("a record without `backed_up` at all still deserializes");
1546 assert_eq!(record.backed_up, None);
1547 assert_eq!(record.bound_user_id, alice);
1548 }
1549
1550 #[test]
1551 fn the_author_decrypts_their_own_message_only_through_the_adopted_outbound_session() {
1552 let mut store = new_store();
1553 let room_id = room("r");
1554 let alice = user("alice");
1555 let alice_dev = device("ALICEDEV");
1556 let members: BTreeSet<UserId> = [alice.clone()].into_iter().collect();
1557 let identity = Account::new().identity_keys();
1558 let event_id = EventId::parse("$own:example.org").expect("valid event id");
1559
1560 let update =
1561 GroupSessionManager::ensure_outbound_session(&mut store, &room_id, &alice, &alice_dev, &default_encryption(), &members, &[], 0)
1562 .expect("create session");
1563 assert!(update.rotated);
1564 let encrypted = GroupSessionManager::encrypt_event(
1565 &mut store,
1566 &room_id,
1567 &identity.curve25519.to_base64(),
1568 &alice_dev,
1569 "m.room.message",
1570 serde_json::json!({ "body": "mine" }),
1571 None,
1572 )
1573 .expect("encrypt");
1574
1575 let before = GroupSessionManager::decrypt_event(&mut store, &room_id, &event_id, 1, &alice, &encrypted);
1578 assert!(matches!(before, Err(GroupDecryptError::MissingSession { .. })), "got {before:?}");
1579
1580 GroupSessionManager::adopt_own_outbound_session(&mut store, &room_id, &alice, identity.curve25519, identity.ed25519, &update)
1581 .expect("adopt");
1582 let plaintext = GroupSessionManager::decrypt_event(&mut store, &room_id, &event_id, 1, &alice, &encrypted).expect("the author decrypts");
1583 assert_eq!(plaintext.content, serde_json::json!({ "body": "mine" }));
1584
1585 let again =
1587 GroupSessionManager::ensure_outbound_session(&mut store, &room_id, &alice, &alice_dev, &default_encryption(), &members, &[], 1)
1588 .expect("re-check");
1589 assert!(!again.rotated);
1590 GroupSessionManager::adopt_own_outbound_session(&mut store, &room_id, &alice, identity.curve25519, identity.ed25519, &again)
1591 .expect("no-op");
1592 GroupSessionManager::decrypt_event(&mut store, &room_id, &event_id, 1, &alice, &encrypted).expect("still decrypts");
1593 }
1594
1595 #[test]
1596 fn a_forwarded_inbound_session_decrypts_the_original_senders_later_event() {
1597 let mut alice_store = new_store();
1598 let mut bob_store = new_store();
1599 let room_id = room("fwd");
1600 let alice = user("alice");
1601 let alice_dev = device("ALICEDEV");
1602 let members: BTreeSet<UserId> = [alice.clone()].into_iter().collect();
1603 let identity = Account::new().identity_keys();
1604 let event_id = EventId::parse("$fwd:example.org").expect("valid event id");
1605
1606 let update = GroupSessionManager::ensure_outbound_session(
1607 &mut alice_store,
1608 &room_id,
1609 &alice,
1610 &alice_dev,
1611 &default_encryption(),
1612 &members,
1613 &[],
1614 0,
1615 )
1616 .expect("create");
1617 GroupSessionManager::adopt_own_outbound_session(
1618 &mut alice_store,
1619 &room_id,
1620 &alice,
1621 identity.curve25519,
1622 identity.ed25519,
1623 &update,
1624 )
1625 .expect("adopt");
1626 let encrypted = GroupSessionManager::encrypt_event(
1627 &mut alice_store,
1628 &room_id,
1629 &identity.curve25519.to_base64(),
1630 &alice_dev,
1631 "m.room.message",
1632 serde_json::json!({ "body": "later" }),
1633 None,
1634 )
1635 .expect("encrypt");
1636
1637 let forwarded = GroupSessionManager::forwarded_room_key_content(
1638 &alice_store,
1639 &room_id,
1640 &encrypted.session_id,
1641 &identity.curve25519.to_base64(),
1642 )
1643 .expect("export")
1644 .expect("inbound exists at the first index");
1645 let stored = GroupSessionManager::receive_forwarded_room_key(
1646 &mut bob_store,
1647 &alice,
1648 identity.curve25519,
1649 identity.ed25519,
1650 &forwarded,
1651 )
1652 .expect("import");
1653 assert!(stored);
1654
1655 let plaintext =
1656 GroupSessionManager::decrypt_event(&mut bob_store, &room_id, &event_id, 1, &alice, &encrypted).expect("bob decrypts");
1657 assert_eq!(plaintext.content, serde_json::json!({ "body": "later" }));
1658 }
1659}