diff --git a/src/components/crypto/identity_setup_view.rs b/src/components/crypto/identity_setup_view.rs index 1e30fe17..0c3b3fe4 100644 --- a/src/components/crypto/identity_setup_view.rs +++ b/src/components/crypto/identity_setup_view.rs @@ -209,17 +209,18 @@ mod imp { let Some(session) = self.session.upgrade() else { return; }; + let security = session.security(); // If the session is already verified, offer to reset it. - let verification_state = session.verification_state(); + let verification_state = security.verification_state(); if verification_state == SessionVerificationState::Verified { self.navigation .replace_with_tags(&[CryptoIdentitySetupPage::Reset.as_ref()]); return; } - let crypto_identity_state = session.crypto_identity_state(); - let recovery_state = session.recovery_state(); + let crypto_identity_state = security.crypto_identity_state(); + let recovery_state = security.recovery_state(); // If there is no crypto identity, we need to bootstrap it. if crypto_identity_state == CryptoIdentityState::Missing { @@ -257,7 +258,7 @@ mod imp { return; }; - let can_recover = session.recovery_state() != RecoveryState::Disabled; + let can_recover = session.security().recovery_state() != RecoveryState::Disabled; self.use_recovery_btn.set_visible(can_recover); } @@ -412,7 +413,7 @@ impl CryptoIdentitySetupView { }; let imp = self.imp(); - let can_recover = session.recovery_state() != RecoveryState::Disabled; + let can_recover = session.security().recovery_state() != RecoveryState::Disabled; if can_recover { let recovery_view = imp.recovery_page(CryptoRecoverySetupInitialPage::Reset); diff --git a/src/components/crypto/recovery_setup_view.rs b/src/components/crypto/recovery_setup_view.rs index 1e69d5ce..c53e51eb 100644 --- a/src/components/crypto/recovery_setup_view.rs +++ b/src/components/crypto/recovery_setup_view.rs @@ -149,10 +149,11 @@ mod imp { fn set_session(&self, session: &Session) { self.session.set(Some(session)); - let recovery_state = session.recovery_state(); + let security = session.security(); + let recovery_state = security.recovery_state(); let initial_page = match recovery_state { RecoveryState::Unknown | RecoveryState::Disabled - if !session.backup_exists_on_server() => + if !security.backup_exists_on_server() => { CryptoRecoverySetupInitialPage::Enable } @@ -172,7 +173,8 @@ mod imp { return; }; - let (required, description) = if session.cross_signing_keys_available() { + let security = session.security(); + let (required, description) = if security.cross_signing_keys_available() { ( false, gettext("Invalidates the verifications of all users and sessions"), @@ -187,7 +189,7 @@ mod imp { self.reset_identity_row.set_is_active(required); self.reset_identity_row.set_subtitle(&description); - let (required, description) = if session.backup_enabled() { + let (required, description) = if security.backup_enabled() { ( false, gettext("You might not be able to read your past encrypted messages anymore"), @@ -396,7 +398,7 @@ impl CryptoRecoverySetupView { match handle.await.unwrap() { Ok(key) => { let imp = self.imp(); - let key = if has_passphrase { None } else { Some(key) }; + let key = (!has_passphrase).then_some(key); imp.update_success(key); imp.navigation @@ -431,7 +433,7 @@ impl CryptoRecoverySetupView { match handle.await.unwrap() { Ok(key) => { let imp = self.imp(); - let key = if has_passphrase { None } else { Some(key) }; + let key = (!has_passphrase).then_some(key); imp.update_success(key); imp.navigation diff --git a/src/login/session_setup_view.rs b/src/login/session_setup_view.rs index e1de36f6..03b0bcc4 100644 --- a/src/login/session_setup_view.rs +++ b/src/login/session_setup_view.rs @@ -47,6 +47,7 @@ mod imp { /// The recovery view. recovery_view: OnceCell, session_handler: RefCell>, + security_handler: RefCell>, } #[glib::object_subclass] @@ -84,6 +85,9 @@ mod imp { if let Some(handler) = self.session_handler.take() { session.disconnect(handler); } + if let Some(handler) = self.security_handler.take() { + session.security().disconnect(handler); + } } } } @@ -200,51 +204,55 @@ mod imp { let Some(session) = self.session.upgrade() else { return; }; + let security = session.security(); // Stop listening to notifications. if let Some(handler) = self.session_handler.take() { session.disconnect(handler); } + if let Some(handler) = self.security_handler.take() { + security.disconnect(handler); + } // Wait if we don't know the crypto identity state. - let crypto_identity_state = session.crypto_identity_state(); + let crypto_identity_state = security.crypto_identity_state(); if crypto_identity_state == CryptoIdentityState::Unknown { - let handler = session.connect_crypto_identity_state_notify(clone!( + let handler = security.connect_crypto_identity_state_notify(clone!( #[weak(rename_to = imp)] self, move |_| { imp.check_session_setup(); } )); - self.session_handler.replace(Some(handler)); + self.security_handler.replace(Some(handler)); return; } // Wait if we don't know the verification state. - let verification_state = session.verification_state(); + let verification_state = security.verification_state(); if verification_state == SessionVerificationState::Unknown { - let handler = session.connect_verification_state_notify(clone!( + let handler = security.connect_verification_state_notify(clone!( #[weak(rename_to = imp)] self, move |_| { imp.check_session_setup(); } )); - self.session_handler.replace(Some(handler)); + self.security_handler.replace(Some(handler)); return; } // Wait if we don't know the recovery state. - let recovery_state = session.recovery_state(); + let recovery_state = security.recovery_state(); if recovery_state == RecoveryState::Unknown { - let handler = session.connect_recovery_state_notify(clone!( + let handler = security.connect_recovery_state_notify(clone!( #[weak(rename_to = imp)] self, move |_| { imp.check_session_setup(); } )); - self.session_handler.replace(Some(handler)); + self.security_handler.replace(Some(handler)); return; } @@ -265,7 +273,7 @@ mod imp { return; }; - let verification_state = session.verification_state(); + let verification_state = session.security().verification_state(); if verification_state == SessionVerificationState::Unverified { let crypto_identity_view = self.crypto_identity_view(); @@ -286,7 +294,7 @@ mod imp { return; }; - match session.recovery_state() { + match session.security().recovery_state() { RecoveryState::Disabled => { self.switch_to_recovery(); } diff --git a/src/session/model/mod.rs b/src/session/model/mod.rs index 81c08172..08704f1c 100644 --- a/src/session/model/mod.rs +++ b/src/session/model/mod.rs @@ -4,6 +4,7 @@ mod remote_room; mod remote_user; mod room; mod room_list; +mod security; mod session; mod session_settings; mod sidebar_data; @@ -20,6 +21,7 @@ pub use self::{ remote_user::RemoteUser, room::*, room_list::RoomList, + security::*, session::*, session_settings::{SessionSettings, StoredSessionSettings}, sidebar_data::{ diff --git a/src/session/model/security.rs b/src/session/model/security.rs new file mode 100644 index 00000000..0729e6eb --- /dev/null +++ b/src/session/model/security.rs @@ -0,0 +1,487 @@ +use futures_util::StreamExt; +use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*}; +use matrix_sdk::encryption::{ + recovery::RecoveryState as SdkRecoveryState, VerificationState as SdkVerificationState, +}; +use tokio::task::AbortHandle; +use tracing::{debug, error, warn}; + +use super::Session; +use crate::{prelude::*, spawn, spawn_tokio}; + +/// The state of the crypto identity. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)] +#[enum_type(name = "CryptoIdentityState")] +pub enum CryptoIdentityState { + /// The state is not known yet. + #[default] + Unknown, + /// The crypto identity does not exist. + /// + /// It means that cross-signing is not set up. + Missing, + /// There are no other verified sessions. + LastManStanding, + /// There are other verified sessions. + OtherSessions, +} + +/// The state of the verification of the session. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)] +#[enum_type(name = "SessionVerificationState")] +pub enum SessionVerificationState { + /// The state is not known yet. + #[default] + Unknown, + /// The session is verified. + Verified, + /// The session is not verified. + Unverified, +} + +impl From for SessionVerificationState { + fn from(value: SdkVerificationState) -> Self { + match value { + SdkVerificationState::Unknown => Self::Unknown, + SdkVerificationState::Verified => Self::Verified, + SdkVerificationState::Unverified => Self::Unverified, + } + } +} + +/// The state of the recovery. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, glib::Enum)] +#[enum_type(name = "RecoveryState")] +pub enum RecoveryState { + /// The state is not known yet. + #[default] + Unknown, + /// Recovery is disabled. + Disabled, + /// Recovery is enabled and we have all the keys. + Enabled, + /// Recovery is enabled and we are missing some keys. + Incomplete, +} + +impl From for RecoveryState { + fn from(value: SdkRecoveryState) -> Self { + match value { + SdkRecoveryState::Unknown => Self::Unknown, + SdkRecoveryState::Disabled => Self::Disabled, + SdkRecoveryState::Enabled => Self::Enabled, + SdkRecoveryState::Incomplete => Self::Incomplete, + } + } +} + +mod imp { + use std::cell::{Cell, RefCell}; + + use super::*; + + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::SessionSecurity)] + pub struct SessionSecurity { + /// The current session. + #[property(get, set = Self::set_session, explicit_notify, nullable)] + session: glib::WeakRef, + /// The state of the crypto identity for the current session. + #[property(get, builder(CryptoIdentityState::default()))] + crypto_identity_state: Cell, + /// The state of the verification for the current session. + #[property(get, builder(SessionVerificationState::default()))] + verification_state: Cell, + /// The state of recovery for the current session. + #[property(get, builder(RecoveryState::default()))] + recovery_state: Cell, + /// Whether all the cross-signing keys are available. + #[property(get)] + cross_signing_keys_available: Cell, + /// Whether the room keys backup is enabled. + #[property(get)] + backup_enabled: Cell, + /// Whether the room keys backup exists on the homeserver. + #[property(get)] + backup_exists_on_server: Cell, + abort_handles: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for SessionSecurity { + const NAME: &'static str = "SessionSecurity"; + type Type = super::SessionSecurity; + } + + #[glib::derived_properties] + impl ObjectImpl for SessionSecurity { + fn dispose(&self) { + for handle in self.abort_handles.take() { + handle.abort(); + } + } + } + + impl SessionSecurity { + /// Set the current session. + fn set_session(&self, session: Option<&Session>) { + if self.session.upgrade().as_ref() == session { + return; + } + + self.session.set(session); + self.obj().notify_session(); + + self.watch_verification_state(); + self.watch_recovery_state(); + + spawn!(clone!( + #[weak(rename_to = imp)] + self, + async move { + imp.watch_crypto_identity_state().await; + } + )); + } + + /// Set the crypto identity state of the current session. + pub(super) fn set_crypto_identity_state(&self, state: CryptoIdentityState) { + if self.crypto_identity_state.get() == state { + return; + } + + self.crypto_identity_state.set(state); + self.obj().notify_crypto_identity_state(); + } + + /// Set the verification state of the current session. + pub(super) fn set_verification_state(&self, state: SessionVerificationState) { + if self.verification_state.get() == state { + return; + } + + self.verification_state.set(state); + self.obj().notify_verification_state(); + } + + /// Set the recovery state of the current session. + pub(super) fn set_recovery_state(&self, state: RecoveryState) { + if self.recovery_state.get() == state { + return; + } + + self.recovery_state.set(state); + self.obj().notify_recovery_state(); + } + + /// Set whether all the cross-signing keys are available. + pub(super) fn set_cross_signing_keys_available(&self, available: bool) { + if self.cross_signing_keys_available.get() == available { + return; + } + + self.cross_signing_keys_available.set(available); + self.obj().notify_cross_signing_keys_available(); + } + + /// Set whether the room keys backup is enabled. + pub(super) fn set_backup_enabled(&self, enabled: bool) { + if self.backup_enabled.get() == enabled { + return; + } + + self.backup_enabled.set(enabled); + self.obj().notify_backup_enabled(); + } + + /// Set whether the room keys backup existson the homeserver. + pub(super) fn set_backup_exists_on_server(&self, exists: bool) { + if self.backup_exists_on_server.get() == exists { + return; + } + + self.backup_exists_on_server.set(exists); + self.obj().notify_backup_exists_on_server(); + } + + /// Listen to crypto identity changes. + async fn watch_crypto_identity_state(&self) { + let Some(session) = self.session.upgrade() else { + return; + }; + + let client = session.client(); + let encryption = client.encryption(); + + let encryption_clone = encryption.clone(); + let handle = + spawn_tokio!(async move { encryption_clone.user_identities_stream().await }); + let identities_stream = match handle.await.unwrap() { + Ok(stream) => stream, + Err(error) => { + error!("Could not get user identities stream: {error}"); + // All method calls here have the same error, so we can return early. + return; + } + }; + + let obj_weak = glib::SendWeakRef::from(self.obj().downgrade()); + let fut = identities_stream.for_each(move |updates| { + let obj_weak = obj_weak.clone(); + + async move { + let ctx = glib::MainContext::default(); + ctx.spawn(async move { + spawn!(async move { + let Some(obj) = obj_weak.upgrade() else { + return; + }; + let Some(session) = obj.session() else { + return; + }; + + let own_user_id = session.user_id(); + if updates.new.contains_key(own_user_id) + || updates.changed.contains_key(own_user_id) + { + obj.imp().load_crypto_identity_state().await; + } + }); + }); + } + }); + let identities_abort_handle = spawn_tokio!(fut).abort_handle(); + + let handle = spawn_tokio!(async move { encryption.devices_stream().await }); + let devices_stream = match handle.await.unwrap() { + Ok(stream) => stream, + Err(error) => { + error!("Could not get devices stream: {error}"); + // All method calls here have the same error, so we can return early. + return; + } + }; + + let obj_weak = glib::SendWeakRef::from(self.obj().downgrade()); + let fut = devices_stream.for_each(move |updates| { + let obj_weak = obj_weak.clone(); + + async move { + let ctx = glib::MainContext::default(); + ctx.spawn(async move { + spawn!(async move { + let Some(obj) = obj_weak.upgrade() else { + return; + }; + let Some(session) = obj.session() else { + return; + }; + + let own_user_id = session.user_id(); + if updates.new.contains_key(own_user_id) + || updates.changed.contains_key(own_user_id) + { + obj.imp().load_crypto_identity_state().await; + } + }); + }); + } + }); + let devices_abort_handle = spawn_tokio!(fut).abort_handle(); + + self.abort_handles + .borrow_mut() + .extend([identities_abort_handle, devices_abort_handle]); + + self.load_crypto_identity_state().await; + } + + /// Load the crypto identity state. + async fn load_crypto_identity_state(&self) { + let Some(session) = self.session.upgrade() else { + return; + }; + + let client = session.client(); + + let client_clone = client.clone(); + let user_identity_handle = spawn_tokio!(async move { + let user_id = client_clone.user_id().unwrap(); + client_clone.encryption().get_user_identity(user_id).await + }); + + let has_identity = match user_identity_handle.await.unwrap() { + Ok(Some(_)) => true, + Ok(None) => { + debug!("No crypto user identity found"); + false + } + Err(error) => { + error!("Could not get crypto user identity: {error}"); + false + } + }; + + if !has_identity { + self.set_crypto_identity_state(CryptoIdentityState::Missing); + return; + } + + let devices_handle = spawn_tokio!(async move { + let user_id = client.user_id().unwrap(); + client.encryption().get_user_devices(user_id).await + }); + + let own_device = session.device_id(); + let has_other_sessions = match devices_handle.await.unwrap() { + Ok(devices) => devices + .devices() + .any(|d| d.device_id() != own_device && d.is_cross_signed_by_owner()), + Err(error) => { + error!("Could not get user devices: {error}"); + // If there are actually no other devices, the user can still + // reset the crypto identity. + true + } + }; + + let state = if has_other_sessions { + CryptoIdentityState::OtherSessions + } else { + CryptoIdentityState::LastManStanding + }; + + self.set_crypto_identity_state(state); + } + + /// Listen to verification state changes. + fn watch_verification_state(&self) { + let Some(session) = self.session.upgrade() else { + return; + }; + + let client = session.client(); + let mut stream = client.encryption().verification_state(); + // Get the current value right away. + stream.reset(); + + let obj_weak = glib::SendWeakRef::from(self.obj().downgrade()); + let fut = stream.for_each(move |state| { + let obj_weak = obj_weak.clone(); + + async move { + let ctx = glib::MainContext::default(); + ctx.spawn(async move { + spawn!(async move { + if let Some(obj) = obj_weak.upgrade() { + obj.imp().set_verification_state(state.into()); + } + }); + }); + } + }); + let verification_abort_handle = spawn_tokio!(fut).abort_handle(); + + self.abort_handles + .borrow_mut() + .push(verification_abort_handle); + } + + /// Listen to recovery state changes. + fn watch_recovery_state(&self) { + let Some(session) = self.session.upgrade() else { + return; + }; + + let client = session.client(); + + let obj_weak = glib::SendWeakRef::from(self.obj().downgrade()); + let stream = client.encryption().recovery().state_stream(); + + let fut = stream.for_each(move |state| { + let obj_weak = obj_weak.clone(); + + async move { + let ctx = glib::MainContext::default(); + ctx.spawn(async move { + spawn!(async move { + if let Some(obj) = obj_weak.upgrade() { + obj.imp().update_recovery_state(state.into()).await; + } + }); + }); + } + }); + + let abort_handle = spawn_tokio!(fut).abort_handle(); + self.abort_handles.borrow_mut().push(abort_handle); + } + + /// Update the session for the given recovery state. + async fn update_recovery_state(&self, state: RecoveryState) { + let Some(session) = self.session.upgrade() else { + return; + }; + + let (cross_signing_keys_available, backup_enabled, backup_exists_on_server) = if matches!( + state, + RecoveryState::Enabled + ) { + (true, true, true) + } else { + let encryption = session.client().encryption(); + let backups = encryption.backups(); + + let handle = spawn_tokio!(async move { encryption.cross_signing_status().await }); + let cross_signing_keys_available = + handle.await.unwrap().is_some_and(|s| s.is_complete()); + + let handle = spawn_tokio!(async move { + if backups.are_enabled().await { + (true, true) + } else { + let backup_exists_on_server = match backups.exists_on_server().await { + Ok(exists) => exists, + Err(error) => { + warn!("Could not request if recovery backup exists on homeserver: {error}"); + false + } + }; + (false, backup_exists_on_server) + } + }); + let (backup_enabled, backup_exists_on_server) = handle.await.unwrap(); + + ( + cross_signing_keys_available, + backup_enabled, + backup_exists_on_server, + ) + }; + + self.set_cross_signing_keys_available(cross_signing_keys_available); + self.set_backup_enabled(backup_enabled); + self.set_backup_exists_on_server(backup_exists_on_server); + + self.set_recovery_state(state); + } + } +} + +glib::wrapper! { + /// Information about the security of a Matrix session. + pub struct SessionSecurity(ObjectSubclass); +} + +impl SessionSecurity { + /// Construct a new empty `SessionSecurity`. + pub fn new() -> Self { + glib::Object::new() + } +} + +impl Default for SessionSecurity { + fn default() -> Self { + Self::new() + } +} diff --git a/src/session/model/session.rs b/src/session/model/session.rs index 4ecc86b5..2dfbcd9b 100644 --- a/src/session/model/session.rs +++ b/src/session/model/session.rs @@ -8,15 +8,7 @@ use gtk::{ prelude::*, subclass::prelude::*, }; -use matrix_sdk::{ - config::SyncSettings, - encryption::{ - recovery::RecoveryState as SdkRecoveryState, VerificationState as SdkVerificationState, - }, - matrix_auth::MatrixSession, - sync::SyncResponse, - Client, -}; +use matrix_sdk::{config::SyncSettings, matrix_auth::MatrixSession, sync::SyncResponse, Client}; use ruma::{ api::client::{ error::ErrorKind, @@ -27,12 +19,12 @@ use ruma::{ assign, }; use tokio::task::AbortHandle; -use tracing::{debug, error, warn}; +use tracing::{debug, error}; use url::Url; use super::{ - IgnoredUsers, Notifications, RoomList, SessionSettings, SidebarItemList, SidebarListModel, - User, UserSessionsList, VerificationList, + IgnoredUsers, Notifications, RoomList, SessionSecurity, SessionSettings, SidebarItemList, + SidebarListModel, User, UserSessionsList, VerificationList, }; use crate::{ components::AvatarData, @@ -69,72 +61,6 @@ pub enum SessionState { #[boxed_type(name = "BoxedClient")] pub struct BoxedClient(Client); -/// The state of the crypto identity. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)] -#[enum_type(name = "CryptoIdentityState")] -pub enum CryptoIdentityState { - /// The state is not known yet. - #[default] - Unknown, - /// The crypto identity does not exist. - /// - /// It means that cross-signing is not set up. - Missing, - /// There are no other verified sessions. - LastManStanding, - /// There are other verified sessions. - OtherSessions, -} - -/// The state of the verification of the session. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)] -#[enum_type(name = "SessionVerificationState")] -pub enum SessionVerificationState { - /// The state is not known yet. - #[default] - Unknown, - /// The session is verified. - Verified, - /// The session is not verified. - Unverified, -} - -impl From for SessionVerificationState { - fn from(value: SdkVerificationState) -> Self { - match value { - SdkVerificationState::Unknown => Self::Unknown, - SdkVerificationState::Verified => Self::Verified, - SdkVerificationState::Unverified => Self::Unverified, - } - } -} - -/// The state of the recovery. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, glib::Enum)] -#[enum_type(name = "RecoveryState")] -pub enum RecoveryState { - /// The state is not known yet. - #[default] - Unknown, - /// Recovery is disabled. - Disabled, - /// Recovery is enabled and we have all the keys. - Enabled, - /// Recovery is enabled and we are missing some keys. - Incomplete, -} - -impl From for RecoveryState { - fn from(value: SdkRecoveryState) -> Self { - match value { - SdkRecoveryState::Unknown => Self::Unknown, - SdkRecoveryState::Disabled => Self::Disabled, - SdkRecoveryState::Enabled => Self::Enabled, - SdkRecoveryState::Incomplete => Self::Incomplete, - } - } -} - mod imp { use std::cell::{Cell, OnceCell, RefCell}; @@ -145,54 +71,38 @@ mod imp { pub struct Session { /// The Matrix client. #[property(construct_only)] - pub client: TokioDrop, + client: TokioDrop, /// The list model of the sidebar. #[property(get = Self::sidebar_list_model)] - pub sidebar_list_model: OnceCell, + sidebar_list_model: OnceCell, /// The user of this session. #[property(get = Self::user)] - pub user: OnceCell, + user: OnceCell, /// The current state of the session. #[property(get, builder(SessionState::default()))] - pub state: Cell, + pub(super) state: Cell, /// Whether this session has a connection to the homeserver. #[property(get)] - pub is_homeserver_reachable: Cell, + is_homeserver_reachable: Cell, /// Whether this session is synchronized with the homeserver. #[property(get)] - pub is_offline: Cell, + is_offline: Cell, /// The current settings for this session. #[property(get, construct_only)] - pub settings: OnceCell, + pub(super) settings: OnceCell, /// The notifications API for this session. #[property(get)] - pub notifications: Notifications, + notifications: Notifications, /// The ignored users API for this session. #[property(get)] - pub ignored_users: IgnoredUsers, + ignored_users: IgnoredUsers, /// The list of sessions for this session's user. #[property(get)] - pub user_sessions: UserSessionsList, - /// The state of the crypto identity for this session. - #[property(get, builder(CryptoIdentityState::default()))] - pub crypto_identity_state: Cell, - /// The state of the verification for this session. - #[property(get, builder(SessionVerificationState::default()))] - pub verification_state: Cell, - /// The state of recovery for this session. - #[property(get, builder(RecoveryState::default()))] - pub recovery_state: Cell, - /// Whether all the cross-signing keys are available. - #[property(get)] - pub cross_signing_keys_available: Cell, - /// Whether the room keys backup is enabled. - #[property(get)] - pub backup_enabled: Cell, - /// Whether the room keys backup exists on the homeserver. + user_sessions: UserSessionsList, + /// Information about security for this session. #[property(get)] - pub backup_exists_on_server: Cell, + security: SessionSecurity, pub sync_handle: RefCell>, - pub abort_handles: RefCell>, pub network_monitor_handler_id: RefCell>, /// The number of missed synchonizations in a row. /// @@ -239,10 +149,6 @@ mod imp { if let Some(handle) = self.sync_handle.take() { handle.abort(); } - - for handle in self.abort_handles.take() { - handle.abort(); - } } } @@ -278,66 +184,6 @@ mod imp { .clone() } - /// Set the crypto identity state of this session. - pub(super) fn set_crypto_identity_state(&self, state: CryptoIdentityState) { - if self.crypto_identity_state.get() == state { - return; - } - - self.crypto_identity_state.set(state); - self.obj().notify_crypto_identity_state(); - } - - /// Set the verification state of this session. - pub(super) fn set_verification_state(&self, state: SessionVerificationState) { - if self.verification_state.get() == state { - return; - } - - self.verification_state.set(state); - self.obj().notify_verification_state(); - } - - /// Set the recovery state of this session. - pub(super) fn set_recovery_state(&self, state: RecoveryState) { - if self.recovery_state.get() == state { - return; - } - - self.recovery_state.set(state); - self.obj().notify_recovery_state(); - } - - /// Set whether all the cross-signing keys are available. - pub(super) fn set_cross_signing_keys_available(&self, available: bool) { - if self.cross_signing_keys_available.get() == available { - return; - } - - self.cross_signing_keys_available.set(available); - self.obj().notify_cross_signing_keys_available(); - } - - /// Set whether the room keys backup is enabled. - pub(super) fn set_backup_enabled(&self, enabled: bool) { - if self.backup_enabled.get() == enabled { - return; - } - - self.backup_enabled.set(enabled); - self.obj().notify_backup_enabled(); - } - - /// Set whether the room keys backup existson the homeserver. - pub(super) fn set_backup_exists_on_server(&self, exists: bool) { - if self.backup_exists_on_server.get() == exists { - return; - } - - self.backup_exists_on_server.set(exists); - self.obj().notify_backup_exists_on_server(); - } - /// Update whether the homeserver is reachable. pub(super) async fn update_homeserver_reachable(&self) { let obj = self.obj(); @@ -470,16 +316,7 @@ impl Session { self.room_list().load().await; self.verification_list().init(); - self.init_verification_state(); - self.init_recovery_state(); - - spawn!(clone!( - #[weak(rename_to = obj)] - self, - async move { - obj.init_crypto_identity_state().await; - } - )); + self.security().set_session(Some(self)); let client = self.client(); spawn_tokio!(async move { @@ -536,243 +373,6 @@ impl Session { self.imp().sync_handle.replace(Some(handle)); } - /// Listen to crypto identity changes. - async fn init_crypto_identity_state(&self) { - let client = self.client(); - - let encryption = client.encryption(); - - let encryption_clone = encryption.clone(); - let handle = spawn_tokio!(async move { encryption_clone.user_identities_stream().await }); - let identities_stream = match handle.await.unwrap() { - Ok(stream) => stream, - Err(error) => { - error!("Could not get user identities stream: {error}"); - // All method calls here have the same error, so we can return early. - return; - } - }; - - let obj_weak: glib::SendWeakRef = self.downgrade().into(); - let fut = identities_stream.for_each(move |updates| { - let obj_weak = obj_weak.clone(); - - async move { - let ctx = glib::MainContext::default(); - ctx.spawn(async move { - spawn!(async move { - if let Some(obj) = obj_weak.upgrade() { - let own_user_id = obj.user_id(); - if updates.new.contains_key(own_user_id) - || updates.changed.contains_key(own_user_id) - { - obj.load_crypto_identity_state().await; - } - } - }); - }); - } - }); - let identities_abort_handle = spawn_tokio!(fut).abort_handle(); - - let handle = spawn_tokio!(async move { encryption.devices_stream().await }); - let devices_stream = match handle.await.unwrap() { - Ok(stream) => stream, - Err(error) => { - error!("Could not get devices stream: {error}"); - // All method calls here have the same error, so we can return early. - return; - } - }; - - let obj_weak: glib::SendWeakRef = self.downgrade().into(); - let fut = devices_stream.for_each(move |updates| { - let obj_weak = obj_weak.clone(); - - async move { - let ctx = glib::MainContext::default(); - ctx.spawn(async move { - spawn!(async move { - if let Some(obj) = obj_weak.upgrade() { - let own_user_id = obj.user_id(); - if updates.new.contains_key(own_user_id) - || updates.changed.contains_key(own_user_id) - { - obj.load_crypto_identity_state().await; - } - } - }); - }); - } - }); - let devices_abort_handle = spawn_tokio!(fut).abort_handle(); - - self.imp() - .abort_handles - .borrow_mut() - .extend([identities_abort_handle, devices_abort_handle]); - - self.load_crypto_identity_state().await; - } - - /// Load the crypto identity state. - async fn load_crypto_identity_state(&self) { - let imp = self.imp(); - let client = self.client(); - - let client_clone = client.clone(); - let user_identity_handle = spawn_tokio!(async move { - let user_id = client_clone.user_id().unwrap(); - client_clone.encryption().get_user_identity(user_id).await - }); - - let has_identity = match user_identity_handle.await.unwrap() { - Ok(Some(_)) => true, - Ok(None) => { - debug!("No crypto user identity found"); - false - } - Err(error) => { - error!("Could not get crypto user identity: {error}"); - false - } - }; - - if !has_identity { - imp.set_crypto_identity_state(CryptoIdentityState::Missing); - return; - } - - let devices_handle = spawn_tokio!(async move { - let user_id = client.user_id().unwrap(); - client.encryption().get_user_devices(user_id).await - }); - - let own_device = self.device_id(); - let has_other_sessions = match devices_handle.await.unwrap() { - Ok(devices) => devices - .devices() - .any(|d| d.device_id() != own_device && d.is_cross_signed_by_owner()), - Err(error) => { - error!("Could not get user devices: {error}"); - // If there are actually no other devices, the user can still - // reset the crypto identity. - true - } - }; - - let state = if has_other_sessions { - CryptoIdentityState::OtherSessions - } else { - CryptoIdentityState::LastManStanding - }; - - imp.set_crypto_identity_state(state); - } - - /// Listen to verification state changes. - fn init_verification_state(&self) { - let client = self.client(); - let mut stream = client.encryption().verification_state(); - // Get the current value right away. - stream.reset(); - - let obj_weak: glib::SendWeakRef = self.downgrade().into(); - let fut = stream.for_each(move |state| { - let obj_weak = obj_weak.clone(); - - async move { - let ctx = glib::MainContext::default(); - ctx.spawn(async move { - spawn!(async move { - if let Some(obj) = obj_weak.upgrade() { - obj.imp().set_verification_state(state.into()); - } - }); - }); - } - }); - let verification_abort_handle = spawn_tokio!(fut).abort_handle(); - - self.imp() - .abort_handles - .borrow_mut() - .push(verification_abort_handle); - } - - /// Listen to recovery state changes. - fn init_recovery_state(&self) { - let client = self.client(); - - let obj_weak: glib::SendWeakRef = self.downgrade().into(); - let stream = client.encryption().recovery().state_stream(); - - let fut = stream.for_each(move |state| { - let obj_weak = obj_weak.clone(); - - async move { - let ctx = glib::MainContext::default(); - ctx.spawn(async move { - spawn!(async move { - if let Some(obj) = obj_weak.upgrade() { - obj.update_recovery_state(state.into()).await; - } - }); - }); - } - }); - - let abort_handle = spawn_tokio!(fut).abort_handle(); - self.imp().abort_handles.borrow_mut().push(abort_handle); - } - - /// Update the session for the given recovery state. - async fn update_recovery_state(&self, state: RecoveryState) { - let imp = self.imp(); - - let (cross_signing_keys_available, backup_enabled, backup_exists_on_server) = if matches!( - state, - RecoveryState::Enabled - ) { - (true, true, true) - } else { - let encryption = self.client().encryption(); - let backups = encryption.backups(); - - let handle = spawn_tokio!(async move { encryption.cross_signing_status().await }); - let cross_signing_keys_available = - handle.await.unwrap().is_some_and(|s| s.is_complete()); - - let handle = spawn_tokio!(async move { - if backups.are_enabled().await { - (true, true) - } else { - let backup_exists_on_server = match backups.exists_on_server().await { - Ok(exists) => exists, - Err(error) => { - warn!("Could not request if recovery backup exists on homeserver: {error}"); - false - } - }; - (false, backup_exists_on_server) - } - }); - let (backup_enabled, backup_exists_on_server) = handle.await.unwrap(); - - ( - cross_signing_keys_available, - backup_enabled, - backup_exists_on_server, - ) - }; - - imp.set_cross_signing_keys_available(cross_signing_keys_available); - imp.set_backup_enabled(backup_enabled); - imp.set_backup_exists_on_server(backup_exists_on_server); - - imp.set_recovery_state(state); - } - /// Start listening to notifications. pub async fn init_notifications(&self) { let obj_weak = glib::SendWeakRef::from(self.downgrade()); diff --git a/src/session/view/account_settings/general_page/log_out_subpage.rs b/src/session/view/account_settings/general_page/log_out_subpage.rs index d3318ce9..c4ddcc53 100644 --- a/src/session/view/account_settings/general_page/log_out_subpage.rs +++ b/src/session/view/account_settings/general_page/log_out_subpage.rs @@ -77,8 +77,9 @@ mod imp { return; }; - let verification_state = session.verification_state(); - let recovery_state = session.recovery_state(); + let security = session.security(); + let verification_state = security.verification_state(); + let recovery_state = security.recovery_state(); if verification_state != SessionVerificationState::Verified || recovery_state != RecoveryState::Enabled @@ -88,7 +89,7 @@ mod imp { return; } - let crypto_identity_state = session.crypto_identity_state(); + let crypto_identity_state = security.crypto_identity_state(); if crypto_identity_state == CryptoIdentityState::LastManStanding { self.warning_description.set_label(&gettext("This is your last connected session. Make sure that you can still access your recovery key or passphrase, or to backup your encryption keys before logging out.")); diff --git a/src/session/view/account_settings/security_page/mod.rs b/src/session/view/account_settings/security_page/mod.rs index cacf7ac1..10c700c3 100644 --- a/src/session/view/account_settings/security_page/mod.rs +++ b/src/session/view/account_settings/security_page/mod.rs @@ -12,7 +12,6 @@ pub use self::{ use crate::{ components::ButtonCountRow, session::model::{CryptoIdentityState, RecoveryState, Session, SessionVerificationState}, - utils::BoundObjectWeakRef, }; mod imp { @@ -52,8 +51,9 @@ mod imp { pub recovery_btn: TemplateChild, /// The current session. #[property(get, set = Self::set_session, nullable)] - pub session: BoundObjectWeakRef, - pub ignored_users_count_handler: RefCell>, + pub session: glib::WeakRef, + ignored_users_count_handler: RefCell>, + security_handlers: RefCell>, bindings: RefCell>, } @@ -75,10 +75,15 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for SecurityPage { fn dispose(&self) { - if let Some(session) = self.session.obj() { + if let Some(session) = self.session.upgrade() { if let Some(handler) = self.ignored_users_count_handler.take() { session.ignored_users().disconnect(handler); } + + let security = session.security(); + for handler in self.security_handlers.take() { + security.disconnect(handler); + } } for binding in self.bindings.take() { @@ -93,7 +98,7 @@ mod imp { impl SecurityPage { /// Set the current session. fn set_session(&self, session: Option) { - let prev_session = self.session.obj(); + let prev_session = self.session.upgrade(); if prev_session == session { return; @@ -104,11 +109,15 @@ mod imp { if let Some(handler) = self.ignored_users_count_handler.take() { session.ignored_users().disconnect(handler); } + + let security = session.security(); + for handler in self.security_handlers.take() { + security.disconnect(handler); + } } for binding in self.bindings.take() { binding.unbind(); } - self.session.disconnect_signals(); if let Some(session) = &session { let ignored_users = session.ignored_users(); @@ -146,22 +155,24 @@ mod imp { self.bindings .replace(vec![public_read_receipts_binding, typing_binding]); + let security = session.security(); let crypto_identity_state_handler = - session.connect_crypto_identity_state_notify(clone!( + security.connect_crypto_identity_state_notify(clone!( #[weak] obj, move |_| { obj.update_crypto_identity(); } )); - let verification_state_handler = session.connect_verification_state_notify(clone!( - #[weak] - obj, - move |_| { - obj.update_crypto_identity(); - } - )); - let recovery_state_handler = session.connect_recovery_state_notify(clone!( + let verification_state_handler = + security.connect_verification_state_notify(clone!( + #[weak] + obj, + move |_| { + obj.update_crypto_identity(); + } + )); + let recovery_state_handler = security.connect_recovery_state_notify(clone!( #[weak] obj, move |_| { @@ -169,16 +180,15 @@ mod imp { } )); - self.session.set( - session, - vec![ - crypto_identity_state_handler, - verification_state_handler, - recovery_state_handler, - ], - ); + self.security_handlers.replace(vec![ + crypto_identity_state_handler, + verification_state_handler, + recovery_state_handler, + ]); } + self.session.set(session.as_ref()); + obj.update_crypto_identity(); obj.update_recovery(); @@ -204,8 +214,9 @@ impl SecurityPage { return; }; let imp = self.imp(); + let security = session.security(); - let crypto_identity_state = session.crypto_identity_state(); + let crypto_identity_state = security.crypto_identity_state(); if matches!( crypto_identity_state, CryptoIdentityState::Unknown | CryptoIdentityState::Missing @@ -232,7 +243,7 @@ impl SecurityPage { return; } - let verification_state = session.verification_state(); + let verification_state = security.verification_state(); if verification_state == SessionVerificationState::Verified { imp.crypto_identity_icon .set_icon_name(Some("verified-symbolic")); @@ -281,7 +292,7 @@ impl SecurityPage { }; let imp = self.imp(); - let recovery_state = session.recovery_state(); + let recovery_state = session.security().recovery_state(); match recovery_state { RecoveryState::Unknown | RecoveryState::Disabled => { imp.recovery_icon.set_icon_name(Some("sync-off-symbolic")); diff --git a/src/session/view/sidebar/mod.rs b/src/session/view/sidebar/mod.rs index 2c5ba5ac..4ac0bdd0 100644 --- a/src/session/view/sidebar/mod.rs +++ b/src/session/view/sidebar/mod.rs @@ -66,7 +66,8 @@ mod imp { #[property(get, set = Self::set_list_model, explicit_notify, nullable)] pub list_model: glib::WeakRef, pub expr_watch: RefCell>, - session_handlers: RefCell>, + session_handler: RefCell>, + security_handlers: RefCell>, } #[glib::object_subclass] @@ -159,9 +160,14 @@ mod imp { if let Some(user) = self.user.take() { let session = user.session(); - for handler in self.session_handlers.take() { + if let Some(handler) = self.session_handler.take() { session.disconnect(handler); } + + let security = session.security(); + for handler in self.security_handlers.take() { + security.disconnect(handler); + } } } } @@ -179,9 +185,14 @@ mod imp { if let Some(user) = prev_user { let session = user.session(); - for handler in self.session_handlers.take() { + if let Some(handler) = self.session_handler.take() { session.disconnect(handler); } + + let security = session.security(); + for handler in self.security_handlers.take() { + security.disconnect(handler); + } } if let Some(user) = &user { @@ -194,21 +205,25 @@ mod imp { imp.update_security_banner(); } )); - let crypto_identity_handler = session.connect_crypto_identity_state_notify(clone!( - #[weak(rename_to = imp)] - self, - move |_| { - imp.update_security_banner(); - } - )); - let verification_handler = session.connect_verification_state_notify(clone!( + self.session_handler.replace(Some(offline_handler)); + + let security = session.security(); + let crypto_identity_handler = + security.connect_crypto_identity_state_notify(clone!( + #[weak(rename_to = imp)] + self, + move |_| { + imp.update_security_banner(); + } + )); + let verification_handler = security.connect_verification_state_notify(clone!( #[weak(rename_to = imp)] self, move |_| { imp.update_security_banner(); } )); - let recovery_handler = session.connect_recovery_state_notify(clone!( + let recovery_handler = security.connect_recovery_state_notify(clone!( #[weak(rename_to = imp)] self, move |_| { @@ -216,8 +231,7 @@ mod imp { } )); - self.session_handlers.replace(vec![ - offline_handler, + self.security_handlers.replace(vec![ crypto_identity_handler, verification_handler, recovery_handler, @@ -266,9 +280,10 @@ mod imp { return; } - let crypto_identity_state = session.crypto_identity_state(); - let verification_state = session.verification_state(); - let recovery_state = session.recovery_state(); + let security = session.security(); + let crypto_identity_state = security.crypto_identity_state(); + let verification_state = security.verification_state(); + let recovery_state = security.recovery_state(); if crypto_identity_state == CryptoIdentityState::Unknown || verification_state == SessionVerificationState::Unknown @@ -337,8 +352,9 @@ impl Sidebar { // Show the security tab if the user uses the back button. dialog.set_visible_page_name("security"); - let crypto_identity_state = session.crypto_identity_state(); - let verification_state = session.verification_state(); + let security = session.security(); + let crypto_identity_state = security.crypto_identity_state(); + let verification_state = security.verification_state(); let subpage = if crypto_identity_state == CryptoIdentityState::Missing || verification_state == SessionVerificationState::Unverified