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#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
14#[enum_type(name = "CryptoIdentityState")]
15pub enum CryptoIdentityState {
16 #[default]
18 Unknown,
19 Missing,
23 LastManStanding,
25 OtherSessions,
27}
28
29#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
31#[enum_type(name = "SessionVerificationState")]
32pub enum SessionVerificationState {
33 #[default]
35 Unknown,
36 Verified,
38 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#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, glib::Enum)]
54#[enum_type(name = "RecoveryState")]
55pub enum RecoveryState {
56 #[default]
58 Unknown,
59 Disabled,
61 Enabled,
63 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 #[property(get, set = Self::set_session, explicit_notify, nullable)]
88 session: glib::WeakRef<Session>,
89 #[property(get, builder(CryptoIdentityState::default()))]
91 crypto_identity_state: Cell<CryptoIdentityState>,
92 #[property(get, builder(SessionVerificationState::default()))]
94 verification_state: Cell<SessionVerificationState>,
95 #[property(get, builder(RecoveryState::default()))]
97 recovery_state: Cell<RecoveryState>,
98 #[property(get)]
100 cross_signing_keys_available: Cell<bool>,
101 #[property(get)]
103 backup_enabled: Cell<bool>,
104 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub struct SessionSecurity(ObjectSubclass<imp::SessionSecurity>);
478}
479
480impl SessionSecurity {
481 pub fn new() -> Self {
483 glib::Object::new()
484 }
485}
486
487impl Default for SessionSecurity {
488 fn default() -> Self {
489 Self::new()
490 }
491}