Skip to main content

fractal/session/
security.rs

1use futures_util::StreamExt;
2use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
3use matrix_sdk::encryption::{
4    VerificationState as SdkVerificationState, recovery::RecoveryState as SdkRecoveryState,
5};
6use tokio::task::AbortHandle;
7use tracing::{debug, error, warn};
8
9use super::Session;
10use crate::{prelude::*, spawn, spawn_tokio};
11
12/// The state of the crypto identity.
13#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
14#[enum_type(name = "CryptoIdentityState")]
15pub enum CryptoIdentityState {
16    /// The state is not known yet.
17    #[default]
18    Unknown,
19    /// The crypto identity does not exist.
20    ///
21    /// It means that cross-signing is not set up.
22    Missing,
23    /// There are no other verified sessions.
24    LastManStanding,
25    /// There are other verified sessions.
26    OtherSessions,
27}
28
29/// The state of the verification of the session.
30#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
31#[enum_type(name = "SessionVerificationState")]
32pub enum SessionVerificationState {
33    /// The state is not known yet.
34    #[default]
35    Unknown,
36    /// The session is verified.
37    Verified,
38    /// The session is not verified.
39    Unverified,
40}
41
42impl From<SdkVerificationState> for SessionVerificationState {
43    fn from(value: SdkVerificationState) -> Self {
44        match value {
45            SdkVerificationState::Unknown => Self::Unknown,
46            SdkVerificationState::Verified => Self::Verified,
47            SdkVerificationState::Unverified => Self::Unverified,
48        }
49    }
50}
51
52/// The state of the recovery.
53#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, glib::Enum)]
54#[enum_type(name = "RecoveryState")]
55pub enum RecoveryState {
56    /// The state is not known yet.
57    #[default]
58    Unknown,
59    /// Recovery is disabled.
60    Disabled,
61    /// Recovery is enabled and we have all the keys.
62    Enabled,
63    /// Recovery is enabled and we are missing some keys.
64    Incomplete,
65}
66
67impl From<SdkRecoveryState> for RecoveryState {
68    fn from(value: SdkRecoveryState) -> Self {
69        match value {
70            SdkRecoveryState::Unknown => Self::Unknown,
71            SdkRecoveryState::Disabled => Self::Disabled,
72            SdkRecoveryState::Enabled => Self::Enabled,
73            SdkRecoveryState::Incomplete => Self::Incomplete,
74        }
75    }
76}
77
78mod imp {
79    use std::cell::{Cell, RefCell};
80
81    use super::*;
82
83    #[derive(Debug, Default, glib::Properties)]
84    #[properties(wrapper_type = super::SessionSecurity)]
85    pub struct SessionSecurity {
86        /// The current session.
87        #[property(get, set = Self::set_session, explicit_notify, nullable)]
88        session: glib::WeakRef<Session>,
89        /// The state of the crypto identity for the current session.
90        #[property(get, builder(CryptoIdentityState::default()))]
91        crypto_identity_state: Cell<CryptoIdentityState>,
92        /// The state of the verification for the current session.
93        #[property(get, builder(SessionVerificationState::default()))]
94        verification_state: Cell<SessionVerificationState>,
95        /// The state of recovery for the current session.
96        #[property(get, builder(RecoveryState::default()))]
97        recovery_state: Cell<RecoveryState>,
98        /// Whether all the cross-signing keys are available.
99        #[property(get)]
100        cross_signing_keys_available: Cell<bool>,
101        /// Whether the room keys backup is enabled.
102        #[property(get)]
103        backup_enabled: Cell<bool>,
104        /// Whether the room keys backup exists on the homeserver.
105        #[property(get)]
106        backup_exists_on_server: Cell<bool>,
107        abort_handles: RefCell<Vec<AbortHandle>>,
108    }
109
110    #[glib::object_subclass]
111    impl ObjectSubclass for SessionSecurity {
112        const NAME: &'static str = "SessionSecurity";
113        type Type = super::SessionSecurity;
114    }
115
116    #[glib::derived_properties]
117    impl ObjectImpl for SessionSecurity {
118        fn dispose(&self) {
119            for handle in self.abort_handles.take() {
120                handle.abort();
121            }
122        }
123    }
124
125    impl SessionSecurity {
126        /// Set the current session.
127        fn set_session(&self, session: Option<&Session>) {
128            if self.session.upgrade().as_ref() == session {
129                return;
130            }
131
132            self.session.set(session);
133            self.obj().notify_session();
134
135            self.watch_verification_state();
136            self.watch_recovery_state();
137
138            spawn!(clone!(
139                #[weak(rename_to = imp)]
140                self,
141                async move {
142                    imp.watch_crypto_identity_state().await;
143                }
144            ));
145        }
146
147        /// Set the crypto identity state of the current session.
148        pub(super) fn set_crypto_identity_state(&self, state: CryptoIdentityState) {
149            if self.crypto_identity_state.get() == state {
150                return;
151            }
152
153            self.crypto_identity_state.set(state);
154            self.obj().notify_crypto_identity_state();
155        }
156
157        /// Set the verification state of the current session.
158        pub(super) fn set_verification_state(&self, state: SessionVerificationState) {
159            if self.verification_state.get() == state {
160                return;
161            }
162
163            self.verification_state.set(state);
164            self.obj().notify_verification_state();
165        }
166
167        /// Set the recovery state of the current session.
168        pub(super) fn set_recovery_state(&self, state: RecoveryState) {
169            if self.recovery_state.get() == state {
170                return;
171            }
172
173            self.recovery_state.set(state);
174            self.obj().notify_recovery_state();
175        }
176
177        /// Set whether all the cross-signing keys are available.
178        pub(super) fn set_cross_signing_keys_available(&self, available: bool) {
179            if self.cross_signing_keys_available.get() == available {
180                return;
181            }
182
183            self.cross_signing_keys_available.set(available);
184            self.obj().notify_cross_signing_keys_available();
185        }
186
187        /// Set whether the room keys backup is enabled.
188        pub(super) fn set_backup_enabled(&self, enabled: bool) {
189            if self.backup_enabled.get() == enabled {
190                return;
191            }
192
193            self.backup_enabled.set(enabled);
194            self.obj().notify_backup_enabled();
195        }
196
197        /// Set whether the room keys backup exists on the homeserver.
198        pub(super) fn set_backup_exists_on_server(&self, exists: bool) {
199            if self.backup_exists_on_server.get() == exists {
200                return;
201            }
202
203            self.backup_exists_on_server.set(exists);
204            self.obj().notify_backup_exists_on_server();
205        }
206
207        /// Listen to crypto identity changes.
208        async fn watch_crypto_identity_state(&self) {
209            let Some(session) = self.session.upgrade() else {
210                return;
211            };
212
213            let client = session.client();
214            let encryption = client.encryption();
215
216            let encryption_clone = encryption.clone();
217            let handle =
218                spawn_tokio!(async move { encryption_clone.user_identities_stream().await });
219            let identities_stream = match handle.await.unwrap() {
220                Ok(stream) => stream,
221                Err(error) => {
222                    error!("Could not get user identities stream: {error}");
223                    // All method calls here have the same error, so we can return early.
224                    return;
225                }
226            };
227
228            let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
229            let fut = identities_stream.for_each(move |updates| {
230                let obj_weak = obj_weak.clone();
231
232                async move {
233                    let ctx = glib::MainContext::default();
234                    ctx.spawn(async move {
235                        spawn!(async move {
236                            let Some(obj) = obj_weak.upgrade() else {
237                                return;
238                            };
239                            let Some(session) = obj.session() else {
240                                return;
241                            };
242
243                            let own_user_id = session.user_id();
244                            if updates.new.contains_key(own_user_id)
245                                || updates.changed.contains_key(own_user_id)
246                            {
247                                obj.imp().load_crypto_identity_state().await;
248                            }
249                        });
250                    });
251                }
252            });
253            let identities_abort_handle = spawn_tokio!(fut).abort_handle();
254
255            let handle = spawn_tokio!(async move { encryption.devices_stream().await });
256            let devices_stream = match handle.await.unwrap() {
257                Ok(stream) => stream,
258                Err(error) => {
259                    error!("Could not get devices stream: {error}");
260                    // All method calls here have the same error, so we can return early.
261                    return;
262                }
263            };
264
265            let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
266            let fut = devices_stream.for_each(move |updates| {
267                let obj_weak = obj_weak.clone();
268
269                async move {
270                    let ctx = glib::MainContext::default();
271                    ctx.spawn(async move {
272                        spawn!(async move {
273                            let Some(obj) = obj_weak.upgrade() else {
274                                return;
275                            };
276                            let Some(session) = obj.session() else {
277                                return;
278                            };
279
280                            let own_user_id = session.user_id();
281                            if updates.new.contains_key(own_user_id)
282                                || updates.changed.contains_key(own_user_id)
283                            {
284                                obj.imp().load_crypto_identity_state().await;
285                            }
286                        });
287                    });
288                }
289            });
290            let devices_abort_handle = spawn_tokio!(fut).abort_handle();
291
292            self.abort_handles
293                .borrow_mut()
294                .extend([identities_abort_handle, devices_abort_handle]);
295
296            self.load_crypto_identity_state().await;
297        }
298
299        /// Load the crypto identity state.
300        async fn load_crypto_identity_state(&self) {
301            let Some(session) = self.session.upgrade() else {
302                return;
303            };
304
305            let client = session.client();
306
307            let client_clone = client.clone();
308            let user_identity_handle = spawn_tokio!(async move {
309                let user_id = client_clone.user_id().unwrap();
310                client_clone.encryption().get_user_identity(user_id).await
311            });
312
313            let has_identity = match user_identity_handle.await.unwrap() {
314                Ok(Some(_)) => true,
315                Ok(None) => {
316                    debug!("No crypto user identity found");
317                    false
318                }
319                Err(error) => {
320                    error!("Could not get crypto user identity: {error}");
321                    false
322                }
323            };
324
325            if !has_identity {
326                self.set_crypto_identity_state(CryptoIdentityState::Missing);
327                return;
328            }
329
330            let devices_handle = spawn_tokio!(async move {
331                let user_id = client.user_id().unwrap();
332                client.encryption().get_user_devices(user_id).await
333            });
334
335            let own_device = session.device_id();
336            let has_other_sessions = match devices_handle.await.unwrap() {
337                Ok(devices) => devices
338                    .devices()
339                    .any(|d| d.device_id() != own_device && d.is_cross_signed_by_owner()),
340                Err(error) => {
341                    error!("Could not get user devices: {error}");
342                    // If there are actually no other devices, the user can still
343                    // reset the crypto identity.
344                    true
345                }
346            };
347
348            let state = if has_other_sessions {
349                CryptoIdentityState::OtherSessions
350            } else {
351                CryptoIdentityState::LastManStanding
352            };
353
354            self.set_crypto_identity_state(state);
355        }
356
357        /// Listen to verification state changes.
358        fn watch_verification_state(&self) {
359            let Some(session) = self.session.upgrade() else {
360                return;
361            };
362
363            let client = session.client();
364            let mut stream = client.encryption().verification_state();
365            // Get the current value right away.
366            stream.reset();
367
368            let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
369            let fut = stream.for_each(move |state| {
370                let obj_weak = obj_weak.clone();
371
372                async move {
373                    let ctx = glib::MainContext::default();
374                    ctx.spawn(async move {
375                        spawn!(async move {
376                            if let Some(obj) = obj_weak.upgrade() {
377                                obj.imp().set_verification_state(state.into());
378                            }
379                        });
380                    });
381                }
382            });
383            let verification_abort_handle = spawn_tokio!(fut).abort_handle();
384
385            self.abort_handles
386                .borrow_mut()
387                .push(verification_abort_handle);
388        }
389
390        /// Listen to recovery state changes.
391        fn watch_recovery_state(&self) {
392            let Some(session) = self.session.upgrade() else {
393                return;
394            };
395
396            let client = session.client();
397
398            let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
399            let stream = client.encryption().recovery().state_stream();
400
401            let fut = stream.for_each(move |state| {
402                let obj_weak = obj_weak.clone();
403
404                async move {
405                    let ctx = glib::MainContext::default();
406                    ctx.spawn(async move {
407                        spawn!(async move {
408                            if let Some(obj) = obj_weak.upgrade() {
409                                obj.imp().update_recovery_state(state.into()).await;
410                            }
411                        });
412                    });
413                }
414            });
415
416            let abort_handle = spawn_tokio!(fut).abort_handle();
417            self.abort_handles.borrow_mut().push(abort_handle);
418        }
419
420        /// Update the session for the given recovery state.
421        async fn update_recovery_state(&self, state: RecoveryState) {
422            let Some(session) = self.session.upgrade() else {
423                return;
424            };
425
426            let (cross_signing_keys_available, backup_enabled, backup_exists_on_server) = if matches!(
427                state,
428                RecoveryState::Enabled
429            ) {
430                (true, true, true)
431            } else {
432                let encryption = session.client().encryption();
433                let backups = encryption.backups();
434
435                let handle = spawn_tokio!(async move { encryption.cross_signing_status().await });
436                let cross_signing_keys_available =
437                    handle.await.unwrap().is_some_and(|s| s.is_complete());
438
439                let handle = spawn_tokio!(async move {
440                    if backups.are_enabled().await {
441                        (true, true)
442                    } else {
443                        let backup_exists_on_server = match backups.exists_on_server().await {
444                            Ok(exists) => exists,
445                            Err(error) => {
446                                warn!(
447                                    "Could not request whether recovery backup exists on homeserver: {error}"
448                                );
449                                // If the request failed, we have to try to delete the backup to
450                                // avoid unsolvable errors.
451                                true
452                            }
453                        };
454                        (false, backup_exists_on_server)
455                    }
456                });
457                let (backup_enabled, backup_exists_on_server) = handle.await.unwrap();
458
459                (
460                    cross_signing_keys_available,
461                    backup_enabled,
462                    backup_exists_on_server,
463                )
464            };
465
466            self.set_cross_signing_keys_available(cross_signing_keys_available);
467            self.set_backup_enabled(backup_enabled);
468            self.set_backup_exists_on_server(backup_exists_on_server);
469
470            self.set_recovery_state(state);
471        }
472    }
473}
474
475glib::wrapper! {
476    /// Information about the security of a Matrix session.
477    pub struct SessionSecurity(ObjectSubclass<imp::SessionSecurity>);
478}
479
480impl SessionSecurity {
481    /// Construct a new empty `SessionSecurity`.
482    pub fn new() -> Self {
483        glib::Object::new()
484    }
485}
486
487impl Default for SessionSecurity {
488    fn default() -> Self {
489        Self::new()
490    }
491}