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