Skip to content
1 change: 1 addition & 0 deletions bindings/matrix-sdk-ffi/src/authentication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ impl SsoHandler {
let builder =
auth.login_with_sso_callback(url.into()).map_err(|_| SsoError::CallbackUrlInvalid)?;
builder.await.map_err(|_| SsoError::LoginWithTokenFailed)?;

Ok(())
}
}
Expand Down
5 changes: 5 additions & 0 deletions bindings/matrix-sdk-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,13 +489,17 @@ impl Client {
device_id: Option<String>,
) -> Result<(), ClientError> {
let mut builder = self.inner.matrix_auth().login_username(&username, &password);

if let Some(initial_device_name) = initial_device_name.as_ref() {
builder = builder.initial_device_display_name(initial_device_name);
}

if let Some(device_id) = device_id.as_ref() {
builder = builder.device_id(device_id);
}

builder.send().await?;

Ok(())
}

Expand All @@ -521,6 +525,7 @@ impl Client {
}

builder.send().await?;

Ok(())
}

Expand Down
138 changes: 133 additions & 5 deletions bindings/matrix-sdk-ffi/src/encryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@
// See the License for that specific language governing permissions and
// limitations under the License.

use std::sync::Arc;
use std::{str::FromStr, sync::Arc};

use futures_util::StreamExt;
use matrix_sdk::{
encryption,
encryption::{backups, recovery},
};
use matrix_sdk::encryption::{self, backups, recovery};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::OwnedUserId;
use thiserror::Error;
use tracing::{error, info};
use zeroize::Zeroize;
Expand Down Expand Up @@ -238,6 +236,111 @@ impl From<encryption::VerificationState> for VerificationState {
}
}

/// Struct containing the bundle of secrets to fully activate a new devices for
/// end-to-end encryption.
#[derive(uniffi::Object)]
pub struct SecretsBundle {
user_id: OwnedUserId,
inner: matrix_sdk_base::crypto::types::SecretsBundle,
}

/// Error type describinfg failures that can happen while exporting a
/// [`SecretsBundle`] from a SQLite store.
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum BundleExportError {
/// The SQLite store couldn't be opened.
#[error("the store coulnd't be opened: {msg}")]
OpenStoreError { msg: String },
/// Data from the SQLite store coulnd't be exported.
#[error("the bundle coulnd't be exported due to a storage error: {msg}")]
StoreError { msg: String },
/// The store doesn't contain a secrets bundle or it couldn't be read from
/// the store.
#[error("the bundle coulnt' be exported: {msg}")]
SecretError { msg: String },
/// The store is empty and doesn't contain a secrets bundle.
#[error("the store is completely empty")]
StoreEmpty,
}

impl From<matrix_sdk::encryption::BundleExportError> for BundleExportError {
fn from(value: matrix_sdk::encryption::BundleExportError) -> Self {
match value {
matrix_sdk::encryption::BundleExportError::OpenStoreError(e) => {
BundleExportError::OpenStoreError { msg: e.to_string() }
}
matrix_sdk::encryption::BundleExportError::StoreError(e) => {
BundleExportError::StoreError { msg: e.to_string() }
}
matrix_sdk::encryption::BundleExportError::SecretExport(e) => {
BundleExportError::SecretError { msg: e.to_string() }
}
}
}
}

#[matrix_sdk_ffi_macros::export]
impl SecretsBundle {
/// Attempt to create a [`SecretsBundle`] from a previously JSON serialized
/// bundle.
#[uniffi::constructor]
pub fn from_str(user_id: &str, bundle: &str) -> Result<Arc<Self>, ClientError> {
let user_id = OwnedUserId::from_str(user_id)?;
let bundle = serde_json::from_str(bundle)?;

Ok(Self { user_id, inner: bundle }.into())
}

/// Attempt to export a [`SecretsBundle`] from a crypto store.
///
/// This method can be used to retrieve a [`SecretsBundle`] from an existing
/// `matrix-sdk`-based client in order to import the [`SecretsBundle`] in
/// another [`Client`] instance.
///
/// This can be useful for migration purposes or to allow existing client
/// instances create new ones that will be fully verified.
#[uniffi::constructor]
pub async fn from_database(
database_path: &str,
mut passphrase: Option<String>,
) -> Result<Arc<Self>, BundleExportError> {
let ret = if let Some((user_id, inner)) =
matrix_sdk::encryption::export_secrets_bundle_from_store(
database_path,
passphrase.as_deref(),
)
.await?
{
Ok(SecretsBundle { user_id, inner }.into())
} else {
Err(BundleExportError::StoreEmpty)
};

passphrase.zeroize();

ret
}
}

/// Check if a crypto store contains a valid [`SecretsBundle`].
#[matrix_sdk_ffi_macros::export]
pub async fn database_contains_secrets_bundle(
database_path: &str,
mut passphrase: Option<String>,
) -> Result<bool, ClientError> {
let ret = matrix_sdk::encryption::export_secrets_bundle_from_store(
database_path,
passphrase.as_deref(),
)
.await
.map_err(ClientError::from_err)?
.is_some();

passphrase.zeroize();

Ok(ret)
}

#[matrix_sdk_ffi_macros::export]
impl Encryption {
/// Get the public ed25519 key of our own device. This is usually what is
Expand Down Expand Up @@ -505,6 +608,31 @@ impl Encryption {
Ok(None)
}
}

pub async fn import_secrets_bundle(
&self,
secrets_bundle: &SecretsBundle,
) -> Result<(), ClientError> {
let user_id = self._client.inner.user_id().expect(
"We should have a user ID available now, this is only called once we're logged in",
);

if user_id == secrets_bundle.user_id {
self.inner
.import_secrets_bundle(&secrets_bundle.inner)
.await
.map_err(ClientError::from_err)?;

self.inner.wait_for_e2ee_initialization_tasks().await;

Ok(())
} else {
Err(ClientError::Generic {
msg: "Secrets bundle does not belong to the user which was logged in".to_owned(),
details: None,
})
}
}
}

/// The E2EE identity of a user.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ async fn finish_login<Q>(

// Import the secrets bundle, this will allow us to sign the device keys with
// the master key when we upload them.
client.encryption().import_secrets_bundle(&bundle).await?;
client.encryption().import_secrets_bundle_impl(&bundle).await?;

// Upload the device keys, this will ensure that other devices see us as a fully
// verified device ass soon as this method returns.
Expand Down
104 changes: 97 additions & 7 deletions crates/matrix-sdk/src/encryption/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use std::{
collections::{BTreeMap, HashSet},
io::{Cursor, Read, Write},
iter,
path::PathBuf,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
Expand All @@ -40,9 +40,12 @@ use matrix_sdk_base::{
cross_process_lock::CrossProcessLockError,
crypto::{
CrossSigningBootstrapRequests, OlmMachine,
store::types::{RoomKeyBundleInfo, RoomKeyInfo},
store::{
SecretImportError,
types::{RoomKeyBundleInfo, RoomKeyInfo},
},
types::{
SignedKey,
SecretsBundle, SignedKey,
requests::{
OutgoingRequest, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest,
},
Expand Down Expand Up @@ -104,8 +107,8 @@ pub mod verification;

pub use matrix_sdk_base::crypto::{
CrossSigningStatus, CryptoStoreError, DecryptorError, EventError, KeyExportError, LocalTrust,
MediaEncryptionInfo, MegolmError, OlmError, RoomKeyImportResult, SecretImportError,
SessionCreationError, SignatureError, VERSION,
MediaEncryptionInfo, MegolmError, OlmError, RoomKeyImportResult, SessionCreationError,
SignatureError, VERSION,
olm::{
SessionCreationError as MegolmSessionCreationError,
SessionExportError as OlmSessionExportError,
Expand All @@ -118,6 +121,67 @@ use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
use crate::config::RequestConfig;
pub use crate::error::RoomKeyImportError;

/// Error type describinfg failures that can happen while exporting a
/// [`SecretsBundle`] from a SQLite store.
#[cfg(feature = "sqlite")]
#[derive(Debug, thiserror::Error)]
pub enum BundleExportError {
/// The SQLite store couldn't be opened.
#[error(transparent)]
OpenStoreError(#[from] matrix_sdk_sqlite::OpenStoreError),
/// Data from the SQLite store coulnd't be exported.
#[error(transparent)]
StoreError(#[from] CryptoStoreError),
/// The store doesn't contain a secrets bundle or it couldn't be read from
/// the store.
#[error(transparent)]
SecretExport(#[from] matrix_sdk_base::crypto::store::SecretsBundleExportError),
}

/// Error type describinfg failures that can happen while importing a
/// [`SecretsBundle`].
#[derive(Debug, thiserror::Error)]
pub enum BundleImportError {
/// The bundle coulnd't be imported.
#[error(transparent)]
SecretImport(#[from] SecretImportError),
/// The cross-signed device keys coulnd't been uploaded.
#[error(transparent)]
DeviceKeys(#[from] Error),
}

/// Attempt to export a [`SecretsBundle`] from a crypto store.
///
/// This method can be used to retrieve a [`SecretsBundle`] from an existing
/// `matrix-sdk`-based client in order to import the [`SecretsBundle`] in
/// another [`Client`] instance.
///
/// This can be useful for migration purposes or to allow existing client
/// instances create new ones that will be fully verified.
#[cfg(feature = "sqlite")]
pub async fn export_secrets_bundle_from_store(
database_path: impl AsRef<Path>,
passphrase: Option<&str>,
) -> std::result::Result<Option<(OwnedUserId, SecretsBundle)>, BundleExportError> {
use matrix_sdk_base::crypto::store::CryptoStore;

let store = matrix_sdk_sqlite::SqliteCryptoStore::open(database_path, passphrase).await?;
let account =
store.load_account().await.map_err(|e| BundleExportError::StoreError(e.into()))?;

if let Some(account) = account {
let machine = OlmMachine::with_store(&account.user_id, &account.device_id, store, None)
.await
.map_err(BundleExportError::StoreError)?;

let bundle = machine.store().export_secrets_bundle().await?;

Ok(Some((account.user_id.to_owned(), bundle)))
} else {
Ok(None)
}
}

/// All the data related to the encryption state.
pub(crate) struct EncryptionData {
/// Background tasks related to encryption (key backup, initialization
Expand Down Expand Up @@ -834,9 +898,35 @@ impl Encryption {
}
}

pub(crate) async fn import_secrets_bundle(
/// This method will import all the private cross-signing keys and, if
/// available, the private part of a backup key and its accompanying
/// version into the store.
///
/// Importing all the secrets will mark the device as verified and enable
/// backups if a backup key was available in the bundle.
///
/// **Warning**: Only import this from a trusted source, i.e. if an existing
/// device is sharing this with a new device.
///
/// **Warning*: Only call this method before right after logging in and
/// before the initial sync has been started.
pub async fn import_secrets_bundle(
&self,
bundle: &SecretsBundle,
) -> Result<(), BundleImportError> {
self.import_secrets_bundle_impl(bundle).await?;

// Upload the device keys, this will ensure that other devices see us as a fully
// verified device as soon as this method returns.
self.ensure_device_keys_upload().await?;
self.wait_for_e2ee_initialization_tasks().await;

Ok(())
}

pub(crate) async fn import_secrets_bundle_impl(
&self,
bundle: &matrix_sdk_base::crypto::types::SecretsBundle,
bundle: &SecretsBundle,
) -> Result<(), SecretImportError> {
let olm_machine = self.client.olm_machine().await;
let olm_machine =
Expand Down
Loading