matrix_sdk_base/room/
encryption.rs

1// Copyright 2025 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 ruma::events::room::encryption::RoomEncryptionEventContent;
16
17use super::Room;
18
19impl Room {
20    /// Get the encryption state of this room.
21    pub fn encryption_state(&self) -> EncryptionState {
22        self.inner.read().encryption_state()
23    }
24
25    /// Get the `m.room.encryption` content that enabled end to end encryption
26    /// in the room.
27    pub fn encryption_settings(&self) -> Option<RoomEncryptionEventContent> {
28        self.inner.read().base_info.encryption.clone()
29    }
30}
31
32/// Represents the state of a room encryption.
33#[derive(Debug)]
34#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
35pub enum EncryptionState {
36    /// The room is encrypted.
37    Encrypted,
38
39    /// The room is not encrypted.
40    NotEncrypted,
41
42    /// The state of the room encryption is unknown, probably because the
43    /// `/sync` did not provide all data needed to decide.
44    Unknown,
45}
46
47impl EncryptionState {
48    /// Check whether `EncryptionState` is [`Encrypted`][Self::Encrypted].
49    pub fn is_encrypted(&self) -> bool {
50        matches!(self, Self::Encrypted)
51    }
52
53    /// Check whether `EncryptionState` is [`Unknown`][Self::Unknown].
54    pub fn is_unknown(&self) -> bool {
55        matches!(self, Self::Unknown)
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use std::{
62        ops::{Not, Sub},
63        str::FromStr,
64        sync::Arc,
65        time::Duration,
66    };
67
68    use assert_matches::assert_matches;
69    use matrix_sdk_test::ALICE;
70    use ruma::{
71        EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, OwnedEventId,
72        events::{
73            AnySyncStateEvent, EmptyStateKey, StateUnsigned, SyncStateEvent,
74            room::encryption::{OriginalSyncRoomEncryptionEvent, RoomEncryptionEventContent},
75        },
76        room_id,
77        time::SystemTime,
78        user_id,
79    };
80
81    use super::{EncryptionState, Room};
82    use crate::{RoomState, store::MemoryStore};
83
84    fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
85        let store = Arc::new(MemoryStore::new());
86        let user_id = user_id!("@me:example.org");
87        let room_id = room_id!("!test:localhost");
88        let (sender, _receiver) = tokio::sync::broadcast::channel(1);
89
90        (store.clone(), Room::new(user_id, store, room_id, room_type, sender))
91    }
92
93    fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
94        MilliSecondsSinceUnixEpoch::from_system_time(
95            SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
96        )
97        .expect("date out of range")
98    }
99
100    fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
101        room.inner.update_if(|info| {
102            let mut res = false;
103            for ev in events {
104                res |= info.handle_state_event(ev);
105            }
106            res
107        });
108    }
109
110    #[test]
111    fn test_encryption_is_set_when_encryption_event_is_received_encrypted() {
112        let (_store, room) = make_room_test_helper(RoomState::Joined);
113
114        assert_matches!(room.encryption_state(), EncryptionState::Unknown);
115
116        let encryption_content =
117            RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
118        let encryption_event = AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(
119            OriginalSyncRoomEncryptionEvent {
120                content: encryption_content,
121                event_id: OwnedEventId::from_str("$1234_1").unwrap(),
122                sender: ALICE.to_owned(),
123                // we can simply use now here since this will be dropped when using a
124                // MinimalStateEvent in the roomInfo
125                origin_server_ts: timestamp(0),
126                state_key: EmptyStateKey,
127                unsigned: StateUnsigned::new(),
128            },
129        ));
130        receive_state_events(&room, vec![&encryption_event]);
131
132        assert_matches!(room.encryption_state(), EncryptionState::Encrypted);
133    }
134
135    #[test]
136    fn test_encryption_is_set_when_encryption_event_is_received_not_encrypted() {
137        let (_store, room) = make_room_test_helper(RoomState::Joined);
138
139        assert_matches!(room.encryption_state(), EncryptionState::Unknown);
140        room.inner.update_if(|info| {
141            info.mark_encryption_state_synced();
142
143            false
144        });
145
146        assert_matches!(room.encryption_state(), EncryptionState::NotEncrypted);
147    }
148
149    #[test]
150    fn test_encryption_state() {
151        assert!(EncryptionState::Unknown.is_unknown());
152        assert!(EncryptionState::Encrypted.is_unknown().not());
153        assert!(EncryptionState::NotEncrypted.is_unknown().not());
154
155        assert!(EncryptionState::Unknown.is_encrypted().not());
156        assert!(EncryptionState::Encrypted.is_encrypted());
157        assert!(EncryptionState::NotEncrypted.is_encrypted().not());
158    }
159}