Skip to main content

matrix_sdk_crypto/olm/group_sessions/
outbound.rs

1// Copyright 2020 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    cmp::max,
17    collections::{BTreeMap, BTreeSet},
18    fmt,
19    ops::Bound,
20    sync::{
21        Arc, RwLockReadGuard,
22        atomic::{AtomicBool, AtomicU64, Ordering},
23    },
24    time::Duration,
25};
26
27use matrix_sdk_common::{deserialized_responses::WithheldCode, locks::RwLock as StdRwLock};
28#[cfg(feature = "experimental-encrypted-state-events")]
29use ruma::events::AnyStateEventContent;
30use ruma::{
31    DeviceId, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId,
32    SecondsSinceUnixEpoch, TransactionId, UserId,
33    events::{
34        AnyMessageLikeEventContent,
35        room::{
36            encryption::{PossiblyRedactedRoomEncryptionEventContent, RoomEncryptionEventContent},
37            history_visibility::HistoryVisibility,
38        },
39    },
40    serde::Raw,
41};
42use serde::{Deserialize, Serialize};
43use tokio::sync::RwLock;
44use tracing::{debug, error, info};
45use vodozemac::{Curve25519PublicKey, megolm::SessionConfig};
46pub use vodozemac::{
47    PickleError,
48    megolm::{GroupSession, GroupSessionPickle, MegolmMessage, SessionKey},
49    olm::IdentityKeys,
50};
51
52use super::SessionCreationError;
53#[cfg(feature = "experimental-algorithms")]
54use crate::types::events::room::encrypted::MegolmV2AesSha2Content;
55use crate::{
56    DeviceData,
57    olm::account::shared_history_from_history_visibility,
58    session_manager::CollectStrategy,
59    store::caches::SequenceNumber,
60    types::{
61        EventEncryptionAlgorithm,
62        events::{
63            room::encrypted::{
64                MegolmV1AesSha2Content, RoomEncryptedEventContent, RoomEventEncryptionScheme,
65            },
66            room_key::{MegolmV1AesSha2Content as MegolmV1AesSha2RoomKeyContent, RoomKeyContent},
67            room_key_withheld::RoomKeyWithheldContent,
68        },
69        requests::ToDeviceRequest,
70    },
71};
72
73const ONE_HOUR: Duration = Duration::from_secs(60 * 60);
74const ONE_WEEK: Duration = Duration::from_secs(60 * 60 * 24 * 7);
75
76const ROTATION_PERIOD: Duration = ONE_WEEK;
77const ROTATION_MESSAGES: u64 = 100;
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
80/// Information about whether a session was shared with a device.
81pub(crate) enum ShareState {
82    /// The session was not shared with the device.
83    NotShared,
84    /// The session was shared with the device with the given device ID, but
85    /// with a different curve25519 key.
86    SharedButChangedSenderKey,
87    /// The session was shared with the device, at the given message index. The
88    /// `olm_wedging_index` is the value of the `olm_wedging_index` from the
89    /// [`DeviceData`] at the time that we last shared the session with the
90    /// device, and indicates whether we need to re-share the session with the
91    /// device.
92    Shared { message_index: u32, olm_wedging_index: SequenceNumber },
93}
94
95/// Settings for an encrypted room.
96///
97/// This determines the algorithm and rotation periods of a group session.
98#[derive(Clone, Debug, Deserialize, Serialize)]
99pub struct EncryptionSettings {
100    /// The encryption algorithm that should be used in the room.
101    pub algorithm: EventEncryptionAlgorithm,
102    /// Whether state event encryption is enabled.
103    #[cfg(feature = "experimental-encrypted-state-events")]
104    #[serde(default)]
105    pub encrypt_state_events: bool,
106    /// How long the session should be used before changing it.
107    pub rotation_period: Duration,
108    /// How many messages should be sent before changing the session.
109    pub rotation_period_msgs: u64,
110    /// The history visibility of the room when the session was created.
111    pub history_visibility: HistoryVisibility,
112    /// The strategy used to distribute the room keys to participant.
113    /// Default will send to all devices.
114    #[serde(default)]
115    pub sharing_strategy: CollectStrategy,
116}
117
118impl Default for EncryptionSettings {
119    fn default() -> Self {
120        Self {
121            algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
122            #[cfg(feature = "experimental-encrypted-state-events")]
123            encrypt_state_events: false,
124            rotation_period: ROTATION_PERIOD,
125            rotation_period_msgs: ROTATION_MESSAGES,
126            history_visibility: HistoryVisibility::Shared,
127            sharing_strategy: CollectStrategy::default(),
128        }
129    }
130}
131
132impl EncryptionSettings {
133    /// Create new encryption settings using an `RoomEncryptionEventContent`,
134    /// a history visibility, and key sharing strategy.
135    pub fn new(
136        content: RoomEncryptionEventContent,
137        history_visibility: HistoryVisibility,
138        sharing_strategy: CollectStrategy,
139    ) -> Self {
140        let rotation_period: Duration =
141            content.rotation_period_ms.map_or(ROTATION_PERIOD, |r| Duration::from_millis(r.into()));
142        let rotation_period_msgs: u64 =
143            content.rotation_period_msgs.map_or(ROTATION_MESSAGES, Into::into);
144
145        Self {
146            algorithm: EventEncryptionAlgorithm::from(content.algorithm.as_str()),
147            #[cfg(feature = "experimental-encrypted-state-events")]
148            encrypt_state_events: false,
149            rotation_period,
150            rotation_period_msgs,
151            history_visibility,
152            sharing_strategy,
153        }
154    }
155
156    /// Create new encryption settings using a
157    /// `PossiblyRedactedRoomEncryptionEventContent`, a history visibility,
158    /// and key sharing strategy.
159    ///
160    /// Returns `None` if the `content` was redacted.
161    pub fn from_possibly_redacted(
162        content: PossiblyRedactedRoomEncryptionEventContent,
163        history_visibility: HistoryVisibility,
164        sharing_strategy: CollectStrategy,
165    ) -> Option<Self> {
166        let rotation_period: Duration =
167            content.rotation_period_ms.map_or(ROTATION_PERIOD, |r| Duration::from_millis(r.into()));
168        let rotation_period_msgs: u64 =
169            content.rotation_period_msgs.map_or(ROTATION_MESSAGES, Into::into);
170
171        Some(Self {
172            algorithm: EventEncryptionAlgorithm::from(content.algorithm?.as_str()),
173            #[cfg(feature = "experimental-encrypted-state-events")]
174            encrypt_state_events: false,
175            rotation_period,
176            rotation_period_msgs,
177            history_visibility,
178            sharing_strategy,
179        })
180    }
181}
182
183/// The result of encrypting a message with an outbound group session.
184///
185/// Contains the encrypted content, the algorithm used, and the session ID.
186#[derive(Debug)]
187pub struct OutboundGroupSessionEncryptionResult {
188    /// The encrypted content of the message.
189    pub content: Raw<RoomEncryptedEventContent>,
190    /// The algorithm used to encrypt the message.
191    pub algorithm: EventEncryptionAlgorithm,
192    /// The session ID used to encrypt the message.
193    pub session_id: Arc<str>,
194}
195
196/// Outbound group session.
197///
198/// Outbound group sessions are used to exchange room messages between a group
199/// of participants. Outbound group sessions are used to encrypt the room
200/// messages.
201#[derive(Clone)]
202pub struct OutboundGroupSession {
203    inner: Arc<RwLock<GroupSession>>,
204    device_id: OwnedDeviceId,
205    account_identity_keys: Arc<IdentityKeys>,
206    session_id: Arc<str>,
207    room_id: OwnedRoomId,
208    pub(crate) creation_time: SecondsSinceUnixEpoch,
209    message_count: Arc<AtomicU64>,
210    shared: Arc<AtomicBool>,
211    invalidated: Arc<AtomicBool>,
212    settings: Arc<EncryptionSettings>,
213    shared_with_set: Arc<StdRwLock<ShareInfoSet>>,
214    to_share_with_set: Arc<StdRwLock<ToShareMap>>,
215}
216
217/// A a map of userid/device it to a `ShareInfo`.
218///
219/// Holds the `ShareInfo` for all the user/device pairs that will receive the
220/// room key.
221pub type ShareInfoSet = BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, ShareInfo>>;
222
223type ToShareMap = BTreeMap<OwnedTransactionId, (Arc<ToDeviceRequest>, ShareInfoSet)>;
224
225/// Struct holding info about the share state of a outbound group session.
226#[derive(Clone, Debug, Serialize, Deserialize)]
227pub enum ShareInfo {
228    /// When the key has been shared
229    Shared(SharedWith),
230    /// When the session has been withheld
231    Withheld(WithheldCode),
232}
233
234impl ShareInfo {
235    /// Helper to create a SharedWith info
236    pub fn new_shared(
237        sender_key: Curve25519PublicKey,
238        message_index: u32,
239        olm_wedging_index: SequenceNumber,
240    ) -> Self {
241        ShareInfo::Shared(SharedWith { sender_key, message_index, olm_wedging_index })
242    }
243
244    /// Helper to create a Withheld info
245    pub fn new_withheld(code: WithheldCode) -> Self {
246        ShareInfo::Withheld(code)
247    }
248}
249
250#[derive(Clone, Debug, Serialize, Deserialize)]
251pub struct SharedWith {
252    /// The sender key of the device that was used to encrypt the room key.
253    pub sender_key: Curve25519PublicKey,
254    /// The message index that the device received.
255    pub message_index: u32,
256    /// The Olm wedging index of the device at the time the session was shared.
257    #[serde(default)]
258    pub olm_wedging_index: SequenceNumber,
259}
260
261/// A read-only view into the device sharing state of an
262/// [`OutboundGroupSession`].
263pub(crate) struct SharingView<'a> {
264    shared_with_set: RwLockReadGuard<'a, ShareInfoSet>,
265    to_share_with_set: RwLockReadGuard<'a, ToShareMap>,
266}
267
268impl SharingView<'_> {
269    /// Has the session been shared with the given user/device pair (or if not,
270    /// is there such a request pending).
271    pub(crate) fn get_share_state(&self, device: &DeviceData) -> ShareState {
272        self.iter_shares(Some(device.user_id()), Some(device.device_id()))
273            .map(|(_, _, info)| match info {
274                ShareInfo::Shared(info) => {
275                    if device.curve25519_key() == Some(info.sender_key) {
276                        ShareState::Shared {
277                            message_index: info.message_index,
278                            olm_wedging_index: info.olm_wedging_index,
279                        }
280                    } else {
281                        ShareState::SharedButChangedSenderKey
282                    }
283                }
284                ShareInfo::Withheld(_) => ShareState::NotShared,
285            })
286            // Return the most "definitive" ShareState found (in case there
287            // are multiple entries for the same device).
288            .max()
289            .unwrap_or(ShareState::NotShared)
290    }
291
292    /// Has the session been withheld for the given user/device pair (or if not,
293    /// is there such a request pending).
294    pub(crate) fn is_withheld_to(&self, device: &DeviceData, code: &WithheldCode) -> bool {
295        self.iter_shares(Some(device.user_id()), Some(device.device_id()))
296            .any(|(_, _, info)| matches!(info, ShareInfo::Withheld(c) if c == code))
297    }
298
299    /// Enumerate all sent or pending sharing requests for the given device (or
300    /// for all devices if not specified).  This can yield the same device
301    /// multiple times.
302    pub(crate) fn iter_shares<'b, 'c>(
303        &self,
304        user_id: Option<&'b UserId>,
305        device_id: Option<&'c DeviceId>,
306    ) -> impl Iterator<Item = (&UserId, &DeviceId, &ShareInfo)> + use<'_, 'b, 'c> {
307        fn iter_share_info_set<'a, 'b, 'c>(
308            set: &'a ShareInfoSet,
309            user_ids: (Bound<&'b UserId>, Bound<&'b UserId>),
310            device_ids: (Bound<&'c DeviceId>, Bound<&'c DeviceId>),
311        ) -> impl Iterator<Item = (&'a UserId, &'a DeviceId, &'a ShareInfo)> + use<'a, 'b, 'c>
312        {
313            set.range::<UserId, _>(user_ids).flat_map(move |(uid, d)| {
314                d.range::<DeviceId, _>(device_ids)
315                    .map(|(id, info)| (uid.as_ref(), id.as_ref(), info))
316            })
317        }
318
319        let user_ids = user_id
320            .map(|u| (Bound::Included(u), Bound::Included(u)))
321            .unwrap_or((Bound::Unbounded, Bound::Unbounded));
322        let device_ids = device_id
323            .map(|d| (Bound::Included(d), Bound::Included(d)))
324            .unwrap_or((Bound::Unbounded, Bound::Unbounded));
325
326        let already_shared = iter_share_info_set(&self.shared_with_set, user_ids, device_ids);
327        let pending = self
328            .to_share_with_set
329            .values()
330            .flat_map(move |(_, set)| iter_share_info_set(set, user_ids, device_ids));
331        already_shared.chain(pending)
332    }
333
334    /// Enumerate all users that have received the session, or have pending
335    /// requests to receive it.  This can yield the same user multiple times,
336    /// so you may want to `collect()` the result into a `BTreeSet`.
337    pub(crate) fn shared_with_users(&self) -> impl Iterator<Item = &UserId> {
338        self.iter_shares(None, None).filter_map(|(u, _, info)| match info {
339            ShareInfo::Shared(_) => Some(u),
340            ShareInfo::Withheld(_) => None,
341        })
342    }
343}
344
345impl OutboundGroupSession {
346    pub(super) fn session_config(
347        algorithm: &EventEncryptionAlgorithm,
348    ) -> Result<SessionConfig, SessionCreationError> {
349        match algorithm {
350            EventEncryptionAlgorithm::MegolmV1AesSha2 => Ok(SessionConfig::version_1()),
351            #[cfg(feature = "experimental-algorithms")]
352            EventEncryptionAlgorithm::MegolmV2AesSha2 => Ok(SessionConfig::version_2()),
353            _ => Err(SessionCreationError::Algorithm(algorithm.to_owned())),
354        }
355    }
356
357    /// Create a new outbound group session for the given room.
358    ///
359    /// Outbound group sessions are used to encrypt room messages.
360    ///
361    /// # Arguments
362    ///
363    /// * `device_id` - The id of the device that created this session.
364    ///
365    /// * `identity_keys` - The identity keys of the account that created this
366    ///   session.
367    ///
368    /// * `room_id` - The id of the room that the session is used in.
369    ///
370    /// * `settings` - Settings determining the algorithm and rotation period of
371    ///   the outbound group session.
372    pub fn new(
373        device_id: OwnedDeviceId,
374        identity_keys: Arc<IdentityKeys>,
375        room_id: &RoomId,
376        settings: EncryptionSettings,
377    ) -> Result<Self, SessionCreationError> {
378        let config = Self::session_config(&settings.algorithm)?;
379
380        let session = GroupSession::new(config);
381        let session_id = session.session_id();
382
383        Ok(OutboundGroupSession {
384            inner: RwLock::new(session).into(),
385            room_id: room_id.into(),
386            device_id,
387            account_identity_keys: identity_keys,
388            session_id: session_id.into(),
389            creation_time: SecondsSinceUnixEpoch::now(),
390            message_count: Arc::new(AtomicU64::new(0)),
391            shared: Arc::new(AtomicBool::new(false)),
392            invalidated: Arc::new(AtomicBool::new(false)),
393            settings: Arc::new(settings),
394            shared_with_set: Default::default(),
395            to_share_with_set: Default::default(),
396        })
397    }
398
399    /// Add a to-device request that is sending the session key (or room key)
400    /// belonging to this [`OutboundGroupSession`] to other members of the
401    /// group.
402    ///
403    /// The request will get persisted with the session which allows seamless
404    /// session reuse across application restarts.
405    ///
406    /// **Warning** this method is only exposed to be used in integration tests
407    /// of crypto-store implementations. **Do not use this outside of tests**.
408    pub fn add_request(
409        &self,
410        request_id: OwnedTransactionId,
411        request: Arc<ToDeviceRequest>,
412        share_infos: ShareInfoSet,
413    ) {
414        self.to_share_with_set.write().insert(request_id, (request, share_infos));
415    }
416
417    /// Create a new `m.room_key.withheld` event content with the given code for
418    /// this outbound group session.
419    pub fn withheld_code(&self, code: WithheldCode) -> RoomKeyWithheldContent {
420        RoomKeyWithheldContent::new(
421            self.settings().algorithm.to_owned(),
422            code,
423            self.room_id().to_owned(),
424            self.session_id().to_owned(),
425            self.sender_key().to_owned(),
426            self.device_id.clone(),
427        )
428    }
429
430    /// This should be called if an the user wishes to rotate this session.
431    pub fn invalidate_session(&self) {
432        self.invalidated.store(true, Ordering::Relaxed)
433    }
434
435    /// Get the encryption settings of this outbound session.
436    pub fn settings(&self) -> &EncryptionSettings {
437        &self.settings
438    }
439
440    /// Mark the request with the given request id as sent.
441    ///
442    /// This removes the request from the queue and marks the set of
443    /// users/devices that received the session.
444    pub fn mark_request_as_sent(
445        &self,
446        request_id: &TransactionId,
447    ) -> BTreeMap<OwnedUserId, BTreeSet<OwnedDeviceId>> {
448        let mut no_olm_devices = BTreeMap::new();
449
450        let removed = self.to_share_with_set.write().remove(request_id);
451        if let Some((to_device, request)) = removed {
452            let recipients: BTreeMap<&UserId, BTreeSet<&DeviceId>> = request
453                .iter()
454                .map(|(u, d)| (u.as_ref(), d.keys().map(|d| d.as_ref()).collect()))
455                .collect();
456
457            info!(
458                ?request_id,
459                ?recipients,
460                ?to_device.event_type,
461                "Marking to-device request carrying a room key or a withheld as sent"
462            );
463
464            for (user_id, info) in request {
465                let no_olms: BTreeSet<OwnedDeviceId> = info
466                    .iter()
467                    .filter(|(_, info)| matches!(info, ShareInfo::Withheld(WithheldCode::NoOlm)))
468                    .map(|(d, _)| d.to_owned())
469                    .collect();
470                no_olm_devices.insert(user_id.to_owned(), no_olms);
471
472                self.shared_with_set.write().entry(user_id).or_default().extend(info);
473            }
474
475            if self.to_share_with_set.read().is_empty() {
476                debug!(
477                    session_id = self.session_id(),
478                    room_id = ?self.room_id,
479                    "All m.room_key and withheld to-device requests were sent out, marking \
480                     session as shared.",
481                );
482
483                self.mark_as_shared();
484            }
485        } else {
486            let request_ids: Vec<String> =
487                self.to_share_with_set.read().keys().map(|k| k.to_string()).collect();
488
489            error!(
490                all_request_ids = ?request_ids,
491                ?request_id,
492                "Marking to-device request carrying a room key as sent but no \
493                 request found with the given id"
494            );
495        }
496
497        no_olm_devices
498    }
499
500    /// Encrypt the given plaintext using this session.
501    ///
502    /// Returns the encrypted ciphertext.
503    ///
504    /// # Arguments
505    ///
506    /// * `plaintext` - The plaintext that should be encrypted.
507    pub(crate) async fn encrypt_helper(&self, plaintext: String) -> MegolmMessage {
508        let mut session = self.inner.write().await;
509        self.message_count.fetch_add(1, Ordering::SeqCst);
510        session.encrypt(&plaintext)
511    }
512
513    /// Encrypt an arbitrary event for the given room.
514    ///
515    /// Beware that a room key needs to be shared before this method
516    /// can be called using the `share_room_key()` method.
517    ///
518    /// # Arguments
519    ///
520    /// * `payload` - The plaintext content of the event that should be
521    ///   serialized to JSON and encrypted.
522    ///
523    /// # Panics
524    ///
525    /// Panics if the content can't be serialized.
526    async fn encrypt_inner<T: Serialize>(
527        &self,
528        payload: &T,
529        relates_to: Option<serde_json::Value>,
530    ) -> OutboundGroupSessionEncryptionResult {
531        let ciphertext = self
532            .encrypt_helper(
533                serde_json::to_string(payload).expect("payload serialization never fails"),
534            )
535            .await;
536        let scheme: RoomEventEncryptionScheme = match self.settings.algorithm {
537            EventEncryptionAlgorithm::MegolmV1AesSha2 => MegolmV1AesSha2Content {
538                ciphertext,
539                sender_key: Some(self.account_identity_keys.curve25519),
540                session_id: self.session_id().to_owned(),
541                device_id: Some(self.device_id.clone()),
542            }
543            .into(),
544            #[cfg(feature = "experimental-algorithms")]
545            EventEncryptionAlgorithm::MegolmV2AesSha2 => {
546                MegolmV2AesSha2Content { ciphertext, session_id: self.session_id().to_owned() }
547                    .into()
548            }
549            _ => unreachable!(
550                "An outbound group session is always using one of the supported algorithms"
551            ),
552        };
553        let content = RoomEncryptedEventContent { scheme, relates_to, other: Default::default() };
554
555        OutboundGroupSessionEncryptionResult {
556            content: Raw::new(&content)
557                .expect("m.room.encrypted event content can always be serialized"),
558            algorithm: self.settings.algorithm.to_owned(),
559            session_id: self.session_id.clone(),
560        }
561    }
562
563    /// Encrypt a room message for the given room.
564    ///
565    /// Beware that a room key needs to be shared before this method
566    /// can be called using the `share_room_key()` method.
567    ///
568    /// # Arguments
569    ///
570    /// * `event_type` - The plaintext type of the event, the outer type of the
571    ///   event will become `m.room.encrypted`.
572    ///
573    /// * `content` - The plaintext content of the message that should be
574    ///   encrypted in raw JSON form.
575    ///
576    /// # Panics
577    ///
578    /// Panics if the content can't be serialized.
579    pub async fn encrypt(
580        &self,
581        event_type: &str,
582        content: &Raw<AnyMessageLikeEventContent>,
583    ) -> OutboundGroupSessionEncryptionResult {
584        #[derive(Serialize)]
585        struct Payload<'a> {
586            #[serde(rename = "type")]
587            event_type: &'a str,
588            content: &'a Raw<AnyMessageLikeEventContent>,
589            room_id: &'a RoomId,
590        }
591
592        let payload = Payload { event_type, content, room_id: &self.room_id };
593
594        let relates_to = content
595            .get_field::<serde_json::Value>("m.relates_to")
596            .expect("serde_json::Value deserialization with valid JSON input never fails");
597
598        self.encrypt_inner(&payload, relates_to).await
599    }
600
601    /// Encrypt a room state event for the given room.
602    ///
603    /// Beware that a room key needs to be shared before this method
604    /// can be called using the `share_room_key()` method.
605    ///
606    /// # Arguments
607    ///
608    /// * `event_type` - The plaintext type of the event, the outer type of the
609    ///   event will become `m.room.encrypted`.
610    ///
611    /// * `state_key` - The plaintext state key of the event, the outer state
612    ///   key will be derived from this and the event type.
613    ///
614    /// * `content` - The plaintext content of the message that should be
615    ///   encrypted in raw JSON form.
616    ///
617    /// # Panics
618    ///
619    /// Panics if the content can't be serialized.
620    #[cfg(feature = "experimental-encrypted-state-events")]
621    pub async fn encrypt_state(
622        &self,
623        event_type: &str,
624        state_key: &str,
625        content: &Raw<AnyStateEventContent>,
626    ) -> Raw<RoomEncryptedEventContent> {
627        #[derive(Serialize)]
628        struct Payload<'a> {
629            #[serde(rename = "type")]
630            event_type: &'a str,
631            state_key: &'a str,
632            content: &'a Raw<AnyStateEventContent>,
633            room_id: &'a RoomId,
634        }
635
636        let payload = Payload { event_type, state_key, content, room_id: &self.room_id };
637        self.encrypt_inner(&payload, None).await.content
638    }
639
640    fn elapsed(&self) -> bool {
641        let creation_time = Duration::from_secs(self.creation_time.get().into());
642        let now = Duration::from_secs(SecondsSinceUnixEpoch::now().get().into());
643        now.checked_sub(creation_time)
644            .map(|elapsed| elapsed >= self.safe_rotation_period())
645            .unwrap_or(true)
646    }
647
648    /// Returns the rotation_period_ms that was set for this session, clamped
649    /// to be no less than one hour.
650    ///
651    /// This is to prevent a malicious or careless user causing sessions to be
652    /// rotated very frequently.
653    ///
654    /// The feature flag `_disable-minimum-rotation-period-ms` can
655    /// be used to prevent this behaviour (which can be useful for tests).
656    fn safe_rotation_period(&self) -> Duration {
657        if cfg!(feature = "_disable-minimum-rotation-period-ms") {
658            self.settings.rotation_period
659        } else {
660            max(self.settings.rotation_period, ONE_HOUR)
661        }
662    }
663
664    /// Check if the session has expired and if it should be rotated.
665    ///
666    /// A session will expire after some time or if enough messages have been
667    /// encrypted using it.
668    pub fn expired(&self) -> bool {
669        let count = self.message_count.load(Ordering::SeqCst);
670        // We clamp the rotation period for message counts to be between 1 and
671        // 10000. The Megolm session should be usable for at least 1 message,
672        // and at most 10000 messages. Realistically Megolm uses u32 for it's
673        // internal counter and one could use the Megolm session for up to
674        // u32::MAX messages, but we're staying on the safe side of things.
675        let rotation_period_msgs = self.settings.rotation_period_msgs.clamp(1, 10_000);
676
677        count >= rotation_period_msgs || self.elapsed()
678    }
679
680    /// Has the session been invalidated.
681    pub fn invalidated(&self) -> bool {
682        self.invalidated.load(Ordering::Relaxed)
683    }
684
685    /// Mark the session as shared.
686    ///
687    /// Messages shouldn't be encrypted with the session before it has been
688    /// shared.
689    pub fn mark_as_shared(&self) {
690        self.shared.store(true, Ordering::Relaxed);
691    }
692
693    /// Check if the session has been marked as shared.
694    pub fn shared(&self) -> bool {
695        self.shared.load(Ordering::Relaxed)
696    }
697
698    /// Get the session key of this session.
699    ///
700    /// A session key can be used to to create an `InboundGroupSession`.
701    pub async fn session_key(&self) -> SessionKey {
702        let session = self.inner.read().await;
703        session.session_key()
704    }
705
706    /// Gets the Sender Key
707    pub fn sender_key(&self) -> Curve25519PublicKey {
708        self.account_identity_keys.as_ref().curve25519.to_owned()
709    }
710
711    /// Get the room id of the room this session belongs to.
712    pub fn room_id(&self) -> &RoomId {
713        &self.room_id
714    }
715
716    /// Returns the unique identifier for this session.
717    pub fn session_id(&self) -> &str {
718        &self.session_id
719    }
720
721    /// Get the current message index for this session.
722    ///
723    /// Each message is sent with an increasing index. This returns the
724    /// message index that will be used for the next encrypted message.
725    pub async fn message_index(&self) -> u32 {
726        let session = self.inner.read().await;
727        session.message_index()
728    }
729
730    pub(crate) async fn as_content(&self) -> RoomKeyContent {
731        let session_key = self.session_key().await;
732        let shared_history =
733            shared_history_from_history_visibility(&self.settings.history_visibility);
734
735        RoomKeyContent::MegolmV1AesSha2(
736            MegolmV1AesSha2RoomKeyContent::new(
737                self.room_id().to_owned(),
738                self.session_id().to_owned(),
739                session_key,
740                shared_history,
741            )
742            .into(),
743        )
744    }
745
746    /// Create a read-only view into the device sharing state of this session.
747    /// This view includes pending requests, so it is not guaranteed that the
748    /// represented state has been fully propagated yet.
749    pub(crate) fn sharing_view(&self) -> SharingView<'_> {
750        SharingView {
751            shared_with_set: self.shared_with_set.read(),
752            to_share_with_set: self.to_share_with_set.read(),
753        }
754    }
755
756    /// Mark the session as shared with the given user/device pair, starting
757    /// from some message index.
758    #[cfg(test)]
759    pub fn mark_shared_with_from_index(
760        &self,
761        user_id: &UserId,
762        device_id: &DeviceId,
763        sender_key: Curve25519PublicKey,
764        index: u32,
765    ) {
766        self.shared_with_set.write().entry(user_id.to_owned()).or_default().insert(
767            device_id.to_owned(),
768            ShareInfo::new_shared(sender_key, index, Default::default()),
769        );
770    }
771
772    /// Mark the session as shared with the given user/device pair, starting
773    /// from the current index.
774    #[cfg(test)]
775    pub async fn mark_shared_with(
776        &self,
777        user_id: &UserId,
778        device_id: &DeviceId,
779        sender_key: Curve25519PublicKey,
780    ) {
781        let share_info =
782            ShareInfo::new_shared(sender_key, self.message_index().await, Default::default());
783        self.shared_with_set
784            .write()
785            .entry(user_id.to_owned())
786            .or_default()
787            .insert(device_id.to_owned(), share_info);
788    }
789
790    /// Get the list of requests that need to be sent out for this session to be
791    /// marked as shared.
792    pub(crate) fn pending_requests(&self) -> Vec<Arc<ToDeviceRequest>> {
793        self.to_share_with_set.read().values().map(|(req, _)| req.clone()).collect()
794    }
795
796    /// Get the list of request ids this session is waiting for to be sent out.
797    pub(crate) fn pending_request_ids(&self) -> Vec<OwnedTransactionId> {
798        self.to_share_with_set.read().keys().cloned().collect()
799    }
800
801    /// Restore a Session from a previously pickled string.
802    ///
803    /// Returns the restored group session or a `OlmGroupSessionError` if there
804    /// was an error.
805    ///
806    /// # Arguments
807    ///
808    /// * `device_id` - The device ID of the device that created this session.
809    ///   Put differently, our own device ID.
810    ///
811    /// * `identity_keys` - The identity keys of the device that created this
812    ///   session, our own identity keys.
813    ///
814    /// * `pickle` - The pickled version of the `OutboundGroupSession`.
815    ///
816    /// * `pickle_mode` - The mode that was used to pickle the session, either
817    ///   an unencrypted mode or an encrypted using passphrase.
818    pub fn from_pickle(
819        device_id: OwnedDeviceId,
820        identity_keys: Arc<IdentityKeys>,
821        pickle: PickledOutboundGroupSession,
822    ) -> Result<Self, PickleError> {
823        let inner: GroupSession = pickle.pickle.into();
824        let session_id = inner.session_id();
825
826        Ok(Self {
827            inner: Arc::new(RwLock::new(inner)),
828            device_id,
829            account_identity_keys: identity_keys,
830            session_id: session_id.into(),
831            room_id: pickle.room_id,
832            creation_time: pickle.creation_time,
833            message_count: AtomicU64::from(pickle.message_count).into(),
834            shared: AtomicBool::from(pickle.shared).into(),
835            invalidated: AtomicBool::from(pickle.invalidated).into(),
836            settings: pickle.settings,
837            shared_with_set: Arc::new(StdRwLock::new(pickle.shared_with_set)),
838            to_share_with_set: Arc::new(StdRwLock::new(pickle.requests)),
839        })
840    }
841
842    /// Store the group session as a base64 encoded string and associated data
843    /// belonging to the session.
844    ///
845    /// # Arguments
846    ///
847    /// * `pickle_mode` - The mode that should be used to pickle the group
848    ///   session, either an unencrypted mode or an encrypted using passphrase.
849    pub async fn pickle(&self) -> PickledOutboundGroupSession {
850        let pickle = self.inner.read().await.pickle();
851
852        PickledOutboundGroupSession {
853            pickle,
854            room_id: self.room_id.clone(),
855            settings: self.settings.clone(),
856            creation_time: self.creation_time,
857            message_count: self.message_count.load(Ordering::SeqCst),
858            shared: self.shared(),
859            invalidated: self.invalidated(),
860            shared_with_set: self.shared_with_set.read().clone(),
861            requests: self.to_share_with_set.read().clone(),
862        }
863    }
864}
865
866#[cfg(not(tarpaulin_include))]
867impl fmt::Debug for OutboundGroupSession {
868    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
869        f.debug_struct("OutboundGroupSession")
870            .field("session_id", &self.session_id)
871            .field("room_id", &self.room_id)
872            .field("creation_time", &self.creation_time)
873            .field("message_count", &self.message_count)
874            .finish()
875    }
876}
877
878/// A pickled version of an `InboundGroupSession`.
879///
880/// Holds all the information that needs to be stored in a database to restore
881/// an InboundGroupSession.
882#[derive(Deserialize, Serialize)]
883#[allow(missing_debug_implementations)]
884pub struct PickledOutboundGroupSession {
885    /// The pickle string holding the OutboundGroupSession.
886    pub pickle: GroupSessionPickle,
887    /// The settings this session adheres to.
888    pub settings: Arc<EncryptionSettings>,
889    /// The room id this session is used for.
890    pub room_id: OwnedRoomId,
891    /// The timestamp when this session was created.
892    pub creation_time: SecondsSinceUnixEpoch,
893    /// The number of messages this session has already encrypted.
894    pub message_count: u64,
895    /// Is the session shared.
896    pub shared: bool,
897    /// Has the session been invalidated.
898    pub invalidated: bool,
899    /// The set of users the session has been already shared with.
900    pub shared_with_set: BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, ShareInfo>>,
901    /// Requests that need to be sent out to share the session.
902    pub requests: BTreeMap<OwnedTransactionId, (Arc<ToDeviceRequest>, ShareInfoSet)>,
903}
904
905#[cfg(test)]
906mod tests {
907    use std::time::Duration;
908
909    use ruma::{
910        EventEncryptionAlgorithm,
911        events::room::{
912            encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility,
913        },
914        uint,
915    };
916
917    use super::{EncryptionSettings, ROTATION_MESSAGES, ROTATION_PERIOD, ShareState};
918    use crate::CollectStrategy;
919
920    #[test]
921    fn test_encryption_settings_conversion() {
922        let mut content =
923            RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
924        let settings = EncryptionSettings::new(
925            content.clone(),
926            HistoryVisibility::Joined,
927            CollectStrategy::AllDevices,
928        );
929
930        assert_eq!(settings.rotation_period, ROTATION_PERIOD);
931        assert_eq!(settings.rotation_period_msgs, ROTATION_MESSAGES);
932
933        content.rotation_period_ms = Some(uint!(3600));
934        content.rotation_period_msgs = Some(uint!(500));
935
936        let settings = EncryptionSettings::new(
937            content,
938            HistoryVisibility::Shared,
939            CollectStrategy::AllDevices,
940        );
941
942        assert_eq!(settings.rotation_period, Duration::from_millis(3600));
943        assert_eq!(settings.rotation_period_msgs, 500);
944    }
945
946    /// Ensure that the `ShareState` PartialOrd instance orders according to
947    /// specificity of the value.
948    #[test]
949    fn test_share_state_ordering() {
950        let values = [
951            ShareState::NotShared,
952            ShareState::SharedButChangedSenderKey,
953            ShareState::Shared { message_index: 1, olm_wedging_index: Default::default() },
954        ];
955        // Make sure our test case of possible variants is exhaustive
956        match values[0] {
957            ShareState::NotShared
958            | ShareState::SharedButChangedSenderKey
959            | ShareState::Shared { .. } => {}
960        }
961        assert!(values.is_sorted());
962    }
963
964    #[cfg(any(target_os = "linux", target_os = "macos", target_family = "wasm"))]
965    mod expiration {
966        use std::{sync::atomic::Ordering, time::Duration};
967
968        use matrix_sdk_test::async_test;
969        use ruma::{
970            SecondsSinceUnixEpoch, device_id, events::room::message::RoomMessageEventContent,
971            room_id, serde::Raw, uint, user_id,
972        };
973
974        use crate::{
975            Account, EncryptionSettings, MegolmError,
976            olm::{OutboundGroupSession, SenderData},
977        };
978
979        const TWO_HOURS: Duration = Duration::from_secs(60 * 60 * 2);
980
981        #[async_test]
982        async fn test_session_is_not_expired_if_no_messages_sent_and_no_time_passed() {
983            // Given a session that expires after one message
984            let session = create_session(EncryptionSettings {
985                rotation_period_msgs: 1,
986                ..Default::default()
987            })
988            .await;
989
990            // When we send no messages at all
991
992            // Then it is not expired
993            assert!(!session.expired());
994        }
995
996        #[async_test]
997        async fn test_session_is_expired_if_we_rotate_every_message_and_one_was_sent()
998        -> Result<(), MegolmError> {
999            // Given a session that expires after one message
1000            let session = create_session(EncryptionSettings {
1001                rotation_period_msgs: 1,
1002                ..Default::default()
1003            })
1004            .await;
1005
1006            // When we send a message
1007            let _ = session
1008                .encrypt(
1009                    "m.room.message",
1010                    &Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
1011                )
1012                .await;
1013
1014            // Then the session is expired
1015            assert!(session.expired());
1016
1017            Ok(())
1018        }
1019
1020        #[async_test]
1021        async fn test_session_with_rotation_period_is_not_expired_after_no_time() {
1022            // Given a session with a 2h expiration
1023            let session = create_session(EncryptionSettings {
1024                rotation_period: TWO_HOURS,
1025                ..Default::default()
1026            })
1027            .await;
1028
1029            // When we don't allow any time to pass
1030
1031            // Then it is not expired
1032            assert!(!session.expired());
1033        }
1034
1035        #[async_test]
1036        async fn test_session_is_expired_after_rotation_period() {
1037            // Given a session with a 2h expiration
1038            let mut session = create_session(EncryptionSettings {
1039                rotation_period: TWO_HOURS,
1040                ..Default::default()
1041            })
1042            .await;
1043
1044            // When 3 hours have passed
1045            let now = SecondsSinceUnixEpoch::now();
1046            session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(10800));
1047
1048            // Then the session is expired
1049            assert!(session.expired());
1050        }
1051
1052        #[async_test]
1053        #[cfg(not(feature = "_disable-minimum-rotation-period-ms"))]
1054        async fn test_session_does_not_expire_under_one_hour_even_if_we_ask_for_shorter() {
1055            // Given a session with a 100ms expiration
1056            let mut session = create_session(EncryptionSettings {
1057                rotation_period: Duration::from_millis(100),
1058                ..Default::default()
1059            })
1060            .await;
1061
1062            // When less than an hour has passed
1063            let now = SecondsSinceUnixEpoch::now();
1064            session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(1800));
1065
1066            // Then the session is not expired: we enforce a minimum of 1 hour
1067            assert!(!session.expired());
1068
1069            // But when more than an hour has passed
1070            session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(3601));
1071
1072            // Then the session is expired
1073            assert!(session.expired());
1074        }
1075
1076        #[async_test]
1077        #[cfg(feature = "_disable-minimum-rotation-period-ms")]
1078        async fn test_with_disable_minrotperiod_feature_sessions_can_expire_quickly() {
1079            // Given a session with a 100ms expiration
1080            let mut session = create_session(EncryptionSettings {
1081                rotation_period: Duration::from_millis(100),
1082                ..Default::default()
1083            })
1084            .await;
1085
1086            // When less than an hour has passed
1087            let now = SecondsSinceUnixEpoch::now();
1088            session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(1800));
1089
1090            // Then the session is expired: the feature flag has prevented us enforcing a
1091            // minimum
1092            assert!(session.expired());
1093        }
1094
1095        #[async_test]
1096        async fn test_session_with_zero_msgs_rotation_is_not_expired_initially() {
1097            // Given a session that is supposed to expire after zero messages
1098            let session = create_session(EncryptionSettings {
1099                rotation_period_msgs: 0,
1100                ..Default::default()
1101            })
1102            .await;
1103
1104            // When we send no messages
1105
1106            // Then the session is not expired: we are protected against this nonsensical
1107            // setup
1108            assert!(!session.expired());
1109        }
1110
1111        #[async_test]
1112        async fn test_session_with_zero_msgs_rotation_expires_after_one_message()
1113        -> Result<(), MegolmError> {
1114            // Given a session that is supposed to expire after zero messages
1115            let session = create_session(EncryptionSettings {
1116                rotation_period_msgs: 0,
1117                ..Default::default()
1118            })
1119            .await;
1120
1121            // When we send a message
1122            let _ = session
1123                .encrypt(
1124                    "m.room.message",
1125                    &Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
1126                )
1127                .await;
1128
1129            // Then the session is expired: we treated rotation_period_msgs=0 as if it were
1130            // =1
1131            assert!(session.expired());
1132
1133            Ok(())
1134        }
1135
1136        #[async_test]
1137        async fn test_session_expires_after_10k_messages_even_if_we_ask_for_more() {
1138            // Given we asked to expire after 100K messages
1139            let session = create_session(EncryptionSettings {
1140                rotation_period_msgs: 100_000,
1141                ..Default::default()
1142            })
1143            .await;
1144
1145            // Sanity: it does not expire after <10K messages
1146            assert!(!session.expired());
1147            session.message_count.store(1000, Ordering::SeqCst);
1148            assert!(!session.expired());
1149            session.message_count.store(9999, Ordering::SeqCst);
1150            assert!(!session.expired());
1151
1152            // When we have sent >= 10K messages
1153            session.message_count.store(10_000, Ordering::SeqCst);
1154
1155            // Then it is considered expired: we enforce a maximum of 10K messages before
1156            // rotation.
1157            assert!(session.expired());
1158        }
1159
1160        async fn create_session(settings: EncryptionSettings) -> OutboundGroupSession {
1161            let account =
1162                Account::with_device_id(user_id!("@alice:example.org"), device_id!("DEVICEID"))
1163                    .static_data;
1164            let (session, _) = account
1165                .create_group_session_pair(
1166                    room_id!("!test_room:example.org"),
1167                    settings,
1168                    SenderData::unknown(),
1169                )
1170                .await
1171                .unwrap();
1172            session
1173        }
1174    }
1175}