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