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