matrix_sdk_ffi/
encryption.rs

1use std::sync::Arc;
2
3use futures_util::StreamExt;
4use matrix_sdk::{
5    encryption,
6    encryption::{backups, recovery},
7};
8use thiserror::Error;
9use tracing::{error, info};
10use zeroize::Zeroize;
11
12use super::RUNTIME;
13use crate::{client::Client, error::ClientError, ruma::AuthData, task_handle::TaskHandle};
14
15#[derive(uniffi::Object)]
16pub struct Encryption {
17    pub(crate) inner: matrix_sdk::encryption::Encryption,
18
19    /// A reference to the FFI client.
20    ///
21    /// Note: we do this to make it so that the FFI `NotificationClient` keeps
22    /// the FFI `Client` and thus the SDK `Client` alive. Otherwise, we
23    /// would need to repeat the hack done in the FFI `Client::drop` method.
24    pub(crate) _client: Arc<Client>,
25}
26
27#[matrix_sdk_ffi_macros::export(callback_interface)]
28pub trait BackupStateListener: Sync + Send {
29    fn on_update(&self, status: BackupState);
30}
31
32#[matrix_sdk_ffi_macros::export(callback_interface)]
33pub trait BackupSteadyStateListener: Sync + Send {
34    fn on_update(&self, status: BackupUploadState);
35}
36
37#[matrix_sdk_ffi_macros::export(callback_interface)]
38pub trait RecoveryStateListener: Sync + Send {
39    fn on_update(&self, status: RecoveryState);
40}
41
42#[matrix_sdk_ffi_macros::export(callback_interface)]
43pub trait VerificationStateListener: Sync + Send {
44    fn on_update(&self, status: VerificationState);
45}
46
47#[derive(uniffi::Enum)]
48pub enum BackupUploadState {
49    Waiting,
50    Uploading { backed_up_count: u32, total_count: u32 },
51    Error,
52    Done,
53}
54
55#[derive(Debug, Error, uniffi::Error)]
56#[uniffi(flat_error)]
57pub enum SteadyStateError {
58    #[error("The backup got disabled while waiting for the room keys to be uploaded.")]
59    BackupDisabled,
60    #[error("There was a connection error.")]
61    Connection,
62    #[error("We couldn't read status updates from the upload task quickly enough.")]
63    Lagged,
64}
65
66#[derive(Debug, Error, uniffi::Error)]
67pub enum RecoveryError {
68    /// A backup already exists on the homeserver, the recovery subsystem does
69    /// not allow backups to be overwritten, disable recovery first.
70    #[error(
71        "A backup already exists on the homeserver and the method does not allow to overwrite it"
72    )]
73    BackupExistsOnServer,
74
75    /// A typical SDK error.
76    #[error(transparent)]
77    Client { source: crate::ClientError },
78
79    /// Error in the secret storage subsystem.
80    #[error("Error in the secret-storage subsystem: {error_message}")]
81    SecretStorage { error_message: String },
82}
83
84impl From<matrix_sdk::encryption::recovery::RecoveryError> for RecoveryError {
85    fn from(value: matrix_sdk::encryption::recovery::RecoveryError) -> Self {
86        match value {
87            recovery::RecoveryError::BackupExistsOnServer => Self::BackupExistsOnServer,
88            recovery::RecoveryError::Sdk(e) => Self::Client { source: ClientError::from(e) },
89            recovery::RecoveryError::SecretStorage(e) => {
90                Self::SecretStorage { error_message: e.to_string() }
91            }
92        }
93    }
94}
95
96pub type Result<A, E = RecoveryError> = std::result::Result<A, E>;
97
98impl From<matrix_sdk::encryption::backups::futures::SteadyStateError> for SteadyStateError {
99    fn from(value: matrix_sdk::encryption::backups::futures::SteadyStateError) -> Self {
100        match value {
101            backups::futures::SteadyStateError::BackupDisabled => Self::BackupDisabled,
102            backups::futures::SteadyStateError::Connection => Self::Connection,
103            backups::futures::SteadyStateError::Lagged => Self::Lagged,
104        }
105    }
106}
107
108#[derive(uniffi::Enum)]
109pub enum BackupState {
110    Unknown,
111    Creating,
112    Enabling,
113    Resuming,
114    Enabled,
115    Downloading,
116    Disabling,
117}
118
119impl From<backups::BackupState> for BackupState {
120    fn from(value: backups::BackupState) -> Self {
121        match value {
122            backups::BackupState::Unknown => Self::Unknown,
123            backups::BackupState::Creating => Self::Creating,
124            backups::BackupState::Enabling => Self::Enabling,
125            backups::BackupState::Resuming => Self::Resuming,
126            backups::BackupState::Enabled => Self::Enabled,
127            backups::BackupState::Downloading => Self::Downloading,
128            backups::BackupState::Disabling => Self::Disabling,
129        }
130    }
131}
132
133impl From<backups::UploadState> for BackupUploadState {
134    fn from(value: backups::UploadState) -> Self {
135        match value {
136            backups::UploadState::Idle => Self::Waiting,
137            backups::UploadState::Uploading(count) => Self::Uploading {
138                backed_up_count: count.backed_up.try_into().unwrap_or(u32::MAX),
139                total_count: count.total.try_into().unwrap_or(u32::MAX),
140            },
141            backups::UploadState::Error => Self::Error,
142            backups::UploadState::Done => Self::Done,
143        }
144    }
145}
146
147#[derive(uniffi::Enum)]
148pub enum RecoveryState {
149    Unknown,
150    Enabled,
151    Disabled,
152    Incomplete,
153}
154
155impl From<recovery::RecoveryState> for RecoveryState {
156    fn from(value: recovery::RecoveryState) -> Self {
157        match value {
158            recovery::RecoveryState::Unknown => Self::Unknown,
159            recovery::RecoveryState::Enabled => Self::Enabled,
160            recovery::RecoveryState::Disabled => Self::Disabled,
161            recovery::RecoveryState::Incomplete => Self::Incomplete,
162        }
163    }
164}
165
166#[matrix_sdk_ffi_macros::export(callback_interface)]
167pub trait EnableRecoveryProgressListener: Sync + Send {
168    fn on_update(&self, status: EnableRecoveryProgress);
169}
170
171#[derive(uniffi::Enum)]
172pub enum EnableRecoveryProgress {
173    Starting,
174    CreatingBackup,
175    CreatingRecoveryKey,
176    BackingUp { backed_up_count: u32, total_count: u32 },
177    RoomKeyUploadError,
178    Done { recovery_key: String },
179}
180
181impl From<recovery::EnableProgress> for EnableRecoveryProgress {
182    fn from(value: recovery::EnableProgress) -> Self {
183        match &value {
184            recovery::EnableProgress::Starting => Self::Starting,
185            recovery::EnableProgress::CreatingBackup => Self::CreatingBackup,
186            recovery::EnableProgress::CreatingRecoveryKey => Self::CreatingRecoveryKey,
187            recovery::EnableProgress::BackingUp(counts) => Self::BackingUp {
188                backed_up_count: counts.backed_up.try_into().unwrap_or(u32::MAX),
189                total_count: counts.backed_up.try_into().unwrap_or(u32::MAX),
190            },
191            recovery::EnableProgress::RoomKeyUploadError => Self::RoomKeyUploadError,
192            recovery::EnableProgress::Done { recovery_key } => {
193                Self::Done { recovery_key: recovery_key.to_owned() }
194            }
195        }
196    }
197}
198
199#[derive(uniffi::Enum)]
200pub enum VerificationState {
201    Unknown,
202    Verified,
203    Unverified,
204}
205
206impl From<encryption::VerificationState> for VerificationState {
207    fn from(value: encryption::VerificationState) -> Self {
208        match &value {
209            encryption::VerificationState::Unknown => Self::Unknown,
210            encryption::VerificationState::Verified => Self::Verified,
211            encryption::VerificationState::Unverified => Self::Unverified,
212        }
213    }
214}
215
216#[matrix_sdk_ffi_macros::export]
217impl Encryption {
218    /// Get the public ed25519 key of our own device. This is usually what is
219    /// called the fingerprint of the device.
220    pub async fn ed25519_key(&self) -> Option<String> {
221        self.inner.ed25519_key().await
222    }
223
224    /// Get the public curve25519 key of our own device in base64. This is
225    /// usually what is called the identity key of the device.
226    pub async fn curve25519_key(&self) -> Option<String> {
227        self.inner.curve25519_key().await.map(|k| k.to_base64())
228    }
229
230    pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
231        let mut stream = self.inner.backups().state_stream();
232
233        let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
234            while let Some(state) = stream.next().await {
235                let Ok(state) = state else { continue };
236                listener.on_update(state.into());
237            }
238        }));
239
240        stream_task.into()
241    }
242
243    pub fn backup_state(&self) -> BackupState {
244        self.inner.backups().state().into()
245    }
246
247    /// Does a backup exist on the server?
248    ///
249    /// Because the homeserver doesn't notify us about changes to the backup
250    /// version, the [`BackupState`] and its listener are a bit crippled.
251    /// The `BackupState::Unknown` state might mean there is no backup at all or
252    /// a backup exists but we don't have access to it.
253    ///
254    /// Therefore it is necessary to poll the server for an answer every time
255    /// you want to differentiate between those two states.
256    pub async fn backup_exists_on_server(&self) -> Result<bool, ClientError> {
257        Ok(self.inner.backups().fetch_exists_on_server().await?)
258    }
259
260    pub fn recovery_state(&self) -> RecoveryState {
261        self.inner.recovery().state().into()
262    }
263
264    pub fn recovery_state_listener(
265        &self,
266        listener: Box<dyn RecoveryStateListener>,
267    ) -> Arc<TaskHandle> {
268        let mut stream = self.inner.recovery().state_stream();
269
270        let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
271            while let Some(state) = stream.next().await {
272                listener.on_update(state.into());
273            }
274        }));
275
276        stream_task.into()
277    }
278
279    pub async fn enable_backups(&self) -> Result<()> {
280        Ok(self.inner.recovery().enable_backup().await?)
281    }
282
283    pub async fn is_last_device(&self) -> Result<bool> {
284        Ok(self.inner.recovery().is_last_device().await?)
285    }
286
287    pub async fn wait_for_backup_upload_steady_state(
288        &self,
289        progress_listener: Option<Box<dyn BackupSteadyStateListener>>,
290    ) -> Result<(), SteadyStateError> {
291        let backups = self.inner.backups();
292        let wait_for_steady_state = backups.wait_for_steady_state();
293
294        let task = if let Some(listener) = progress_listener {
295            let mut progress_stream = wait_for_steady_state.subscribe_to_progress();
296
297            Some(RUNTIME.spawn(async move {
298                while let Some(progress) = progress_stream.next().await {
299                    let Ok(progress) = progress else { continue };
300                    listener.on_update(progress.into());
301                }
302            }))
303        } else {
304            None
305        };
306
307        let result = wait_for_steady_state.await;
308
309        if let Some(task) = task {
310            task.abort();
311        }
312
313        Ok(result?)
314    }
315
316    pub async fn enable_recovery(
317        &self,
318        wait_for_backups_to_upload: bool,
319        mut passphrase: Option<String>,
320        progress_listener: Box<dyn EnableRecoveryProgressListener>,
321    ) -> Result<String> {
322        let recovery = self.inner.recovery();
323
324        let enable = if wait_for_backups_to_upload {
325            recovery.enable().wait_for_backups_to_upload()
326        } else {
327            recovery.enable()
328        };
329
330        let enable = if let Some(passphrase) = &passphrase {
331            enable.with_passphrase(passphrase)
332        } else {
333            enable
334        };
335
336        let mut progress_stream = enable.subscribe_to_progress();
337
338        let task = RUNTIME.spawn(async move {
339            while let Some(progress) = progress_stream.next().await {
340                let Ok(progress) = progress else { continue };
341                progress_listener.on_update(progress.into());
342            }
343        });
344
345        let ret = enable.await?;
346
347        task.abort();
348        passphrase.zeroize();
349
350        Ok(ret)
351    }
352
353    pub async fn disable_recovery(&self) -> Result<()> {
354        Ok(self.inner.recovery().disable().await?)
355    }
356
357    pub async fn reset_recovery_key(&self) -> Result<String> {
358        Ok(self.inner.recovery().reset_key().await?)
359    }
360
361    pub async fn recover_and_reset(&self, mut old_recovery_key: String) -> Result<String> {
362        let result = self.inner.recovery().recover_and_reset(&old_recovery_key).await;
363
364        old_recovery_key.zeroize();
365
366        Ok(result?)
367    }
368
369    /// Completely reset the current user's crypto identity: reset the cross
370    /// signing keys, delete the existing backup and recovery key.
371    pub async fn reset_identity(&self) -> Result<Option<Arc<IdentityResetHandle>>, ClientError> {
372        if let Some(reset_handle) = self
373            .inner
374            .recovery()
375            .reset_identity()
376            .await
377            .map_err(|e| ClientError::Generic { msg: e.to_string() })?
378        {
379            return Ok(Some(Arc::new(IdentityResetHandle { inner: reset_handle })));
380        }
381
382        Ok(None)
383    }
384
385    pub async fn recover(&self, mut recovery_key: String) -> Result<()> {
386        let result = self.inner.recovery().recover(&recovery_key).await;
387
388        recovery_key.zeroize();
389
390        Ok(result?)
391    }
392
393    pub fn verification_state(&self) -> VerificationState {
394        self.inner.verification_state().get().into()
395    }
396
397    pub fn verification_state_listener(
398        self: Arc<Self>,
399        listener: Box<dyn VerificationStateListener>,
400    ) -> Arc<TaskHandle> {
401        let mut subscriber = self.inner.verification_state();
402
403        Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
404            while let Some(verification_state) = subscriber.next().await {
405                listener.on_update(verification_state.into());
406            }
407        })))
408    }
409
410    /// Waits for end-to-end encryption initialization tasks to finish, if any
411    /// was running in the background.
412    pub async fn wait_for_e2ee_initialization_tasks(&self) {
413        self.inner.wait_for_e2ee_initialization_tasks().await;
414    }
415
416    /// Get the E2EE identity of a user.
417    ///
418    /// This method always tries to fetch the identity from the store, which we
419    /// only have if the user is tracked, meaning that we are both members
420    /// of the same encrypted room. If no user is found locally, a request will
421    /// be made to the homeserver.
422    ///
423    /// # Arguments
424    ///
425    /// * `user_id` - The ID of the user that the identity belongs to.
426    ///
427    /// Returns a `UserIdentity` if one is found. Returns an error if there
428    /// was an issue with the crypto store or with the request to the
429    /// homeserver.
430    ///
431    /// This will always return `None` if the client hasn't been logged in.
432    pub async fn user_identity(
433        &self,
434        user_id: String,
435    ) -> Result<Option<Arc<UserIdentity>>, ClientError> {
436        match self.inner.get_user_identity(user_id.as_str().try_into()?).await {
437            Ok(Some(identity)) => {
438                return Ok(Some(Arc::new(UserIdentity { inner: identity })));
439            }
440            Ok(None) => {
441                info!("No identity found in the store.");
442            }
443            Err(error) => {
444                error!("Failed fetching identity from the store: {}", error);
445            }
446        };
447
448        info!("Requesting identity from the server.");
449
450        let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?;
451        Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity })))
452    }
453}
454
455/// The E2EE identity of a user.
456#[derive(uniffi::Object)]
457pub struct UserIdentity {
458    inner: matrix_sdk::encryption::identities::UserIdentity,
459}
460
461#[matrix_sdk_ffi_macros::export]
462impl UserIdentity {
463    /// Remember this identity, ensuring it does not result in a pin violation.
464    ///
465    /// When we first see a user, we assume their cryptographic identity has not
466    /// been tampered with by the homeserver or another entity with
467    /// man-in-the-middle capabilities. We remember this identity and call this
468    /// action "pinning".
469    ///
470    /// If the identity presented for the user changes later on, the newly
471    /// presented identity is considered to be in "pin violation". This
472    /// method explicitly accepts the new identity, allowing it to replace
473    /// the previously pinned one and bringing it out of pin violation.
474    ///
475    /// UIs should display a warning to the user when encountering an identity
476    /// which is not verified and is in pin violation.
477    pub(crate) async fn pin(&self) -> Result<(), ClientError> {
478        Ok(self.inner.pin().await?)
479    }
480
481    /// Get the public part of the Master key of this user identity.
482    ///
483    /// The public part of the Master key is usually used to uniquely identify
484    /// the identity.
485    ///
486    /// Returns None if the master key does not actually contain any keys.
487    pub(crate) fn master_key(&self) -> Option<String> {
488        self.inner.master_key().get_first_key().map(|k| k.to_base64())
489    }
490
491    /// Is the user identity considered to be verified.
492    ///
493    /// If the identity belongs to another user, our own user identity needs to
494    /// be verified as well for the identity to be considered to be verified.
495    pub fn is_verified(&self) -> bool {
496        self.inner.is_verified()
497    }
498
499    /// True if we verified this identity at some point in the past.
500    ///
501    /// To reset this latch back to `false`, one must call
502    /// [`UserIdentity::withdraw_verification()`].
503    pub fn was_previously_verified(&self) -> bool {
504        self.inner.was_previously_verified()
505    }
506
507    /// Remove the requirement for this identity to be verified.
508    ///
509    /// If an identity was previously verified and is not anymore it will be
510    /// reported to the user. In order to remove this notice users have to
511    /// verify again or to withdraw the verification requirement.
512    pub(crate) async fn withdraw_verification(&self) -> Result<(), ClientError> {
513        Ok(self.inner.withdraw_verification().await?)
514    }
515
516    /// Was this identity previously verified, and is no longer?
517    pub fn has_verification_violation(&self) -> bool {
518        self.inner.has_verification_violation()
519    }
520}
521
522#[derive(uniffi::Object)]
523pub struct IdentityResetHandle {
524    pub(crate) inner: matrix_sdk::encryption::recovery::IdentityResetHandle,
525}
526
527#[matrix_sdk_ffi_macros::export]
528impl IdentityResetHandle {
529    /// Get the underlying [`CrossSigningResetAuthType`] this identity reset
530    /// process is using.
531    pub fn auth_type(&self) -> CrossSigningResetAuthType {
532        self.inner.auth_type().into()
533    }
534
535    /// This method starts the identity reset process and
536    /// will go through the following steps:
537    ///
538    /// 1. Disable backing up room keys and delete the active backup
539    /// 2. Disable recovery and delete secret storage
540    /// 3. Go through the cross-signing key reset flow
541    /// 4. Finally, re-enable key backups only if they were enabled before
542    pub async fn reset(&self, auth: Option<AuthData>) -> Result<(), ClientError> {
543        if let Some(auth) = auth {
544            self.inner
545                .reset(Some(auth.into()))
546                .await
547                .map_err(|e| ClientError::Generic { msg: e.to_string() })
548        } else {
549            self.inner.reset(None).await.map_err(|e| ClientError::Generic { msg: e.to_string() })
550        }
551    }
552
553    pub async fn cancel(&self) {
554        self.inner.cancel().await;
555    }
556}
557
558#[derive(uniffi::Enum)]
559pub enum CrossSigningResetAuthType {
560    /// The homeserver requires user-interactive authentication.
561    Uiaa,
562    // /// OIDC is used for authentication and the user needs to open a URL to
563    // /// approve the upload of cross-signing keys.
564    Oidc {
565        info: OidcCrossSigningResetInfo,
566    },
567}
568
569impl From<&matrix_sdk::encryption::CrossSigningResetAuthType> for CrossSigningResetAuthType {
570    fn from(value: &matrix_sdk::encryption::CrossSigningResetAuthType) -> Self {
571        match value {
572            encryption::CrossSigningResetAuthType::Uiaa(_) => Self::Uiaa,
573            encryption::CrossSigningResetAuthType::Oidc(info) => Self::Oidc { info: info.into() },
574        }
575    }
576}
577
578#[derive(uniffi::Record)]
579pub struct OidcCrossSigningResetInfo {
580    /// The URL where the user can approve the reset of the cross-signing keys.
581    pub approval_url: String,
582}
583
584impl From<&matrix_sdk::encryption::OidcCrossSigningResetInfo> for OidcCrossSigningResetInfo {
585    fn from(value: &matrix_sdk::encryption::OidcCrossSigningResetInfo) -> Self {
586        Self { approval_url: value.approval_url.to_string() }
587    }
588}