diff --git a/crates/matrix-sdk/examples/cross_signing_bootstrap.rs b/crates/matrix-sdk/examples/cross_signing_bootstrap.rs index bd82faba4..7f726675b 100644 --- a/crates/matrix-sdk/examples/cross_signing_bootstrap.rs +++ b/crates/matrix-sdk/examples/cross_signing_bootstrap.rs @@ -14,7 +14,7 @@ async fn bootstrap(client: Client, user_id: Box, password: String) { io::stdin().read_line(&mut input).expect("error: unable to read user input"); - if let Err(e) = client.bootstrap_cross_signing(None).await { + if let Err(e) = client.encryption().bootstrap_cross_signing(None).await { use matrix_sdk::ruma::{api::client::uiaa, assign}; if let Some(response) = e.uiaa_response() { @@ -27,6 +27,7 @@ async fn bootstrap(client: Client, user_id: Box, password: String) { )); client + .encryption() .bootstrap_cross_signing(Some(auth_data)) .await .expect("Couldn't bootstrap cross signing") diff --git a/crates/matrix-sdk/examples/emoji_verification.rs b/crates/matrix-sdk/examples/emoji_verification.rs index d7afc23cf..c8f25a312 100644 --- a/crates/matrix-sdk/examples/emoji_verification.rs +++ b/crates/matrix-sdk/examples/emoji_verification.rs @@ -54,7 +54,7 @@ fn print_result(sas: &SasVerification) { async fn print_devices(user_id: &UserId, client: &Client) { println!("Devices of user {}", user_id); - for device in client.get_user_devices(user_id).await.unwrap().devices() { + for device in client.encryption().get_user_devices(user_id).await.unwrap().devices() { println!( " {:<10} {:<30} {:<}", device.device_id(), @@ -87,6 +87,7 @@ async fn login( match event { AnyToDeviceEvent::KeyVerificationStart(e) => { if let Some(Verification::SasV1(sas)) = client + .encryption() .get_verification(&e.sender, e.content.transaction_id.as_str()) .await { @@ -102,6 +103,7 @@ async fn login( AnyToDeviceEvent::KeyVerificationKey(e) => { if let Some(Verification::SasV1(sas)) = client + .encryption() .get_verification(&e.sender, e.content.transaction_id.as_str()) .await { @@ -111,6 +113,7 @@ async fn login( AnyToDeviceEvent::KeyVerificationMac(e) => { if let Some(Verification::SasV1(sas)) = client + .encryption() .get_verification(&e.sender, e.content.transaction_id.as_str()) .await { @@ -136,6 +139,7 @@ async fn login( if let MessageType::VerificationRequest(_) = &m.content.msgtype { let request = client + .encryption() .get_verification_request(&m.sender, &m.event_id) .await .expect("Request object wasn't created"); @@ -148,6 +152,7 @@ async fn login( } AnySyncMessageEvent::KeyVerificationKey(e) => { if let Some(Verification::SasV1(sas)) = client + .encryption() .get_verification( &e.sender, e.content.relates_to.event_id.as_str(), @@ -159,6 +164,7 @@ async fn login( } AnySyncMessageEvent::KeyVerificationMac(e) => { if let Some(Verification::SasV1(sas)) = client + .encryption() .get_verification( &e.sender, e.content.relates_to.event_id.as_str(), diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index aac63b522..e0045390e 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -65,6 +65,8 @@ use serde::de::DeserializeOwned; use tracing::{error, info, instrument, warn}; use url::Url; +#[cfg(feature = "encryption")] +use crate::encryption::Encryption; use crate::{ attachment::{AttachmentInfo, Thumbnail}, config::RequestConfig, @@ -318,6 +320,12 @@ impl Client { Account::new(self.clone()) } + /// Get the encryption manager of the client. + #[cfg(feature = "encryption")] + pub fn encryption(&self) -> Encryption { + Encryption::new(self.clone()) + } + /// Register a handler for a specific event type. /// /// The handler is a function or closure with one or more arguments. The diff --git a/crates/matrix-sdk/src/docs/encryption.md b/crates/matrix-sdk/src/docs/encryption.md index 34de706dd..28015b602 100644 --- a/crates/matrix-sdk/src/docs/encryption.md +++ b/crates/matrix-sdk/src/docs/encryption.md @@ -87,7 +87,7 @@ stored, otherwise we won't be able to decrypt historical messages. The SDK stores all room keys locally in a encrypted manner. Besides storing them as part of the SDK store, users can export room keys -using the [`Client::export_keys`] method. +using the [`Encryption::export_keys`] method. # Verification diff --git a/crates/matrix-sdk/src/encryption/identities/devices.rs b/crates/matrix-sdk/src/encryption/identities/devices.rs index 208279566..6086d4f7c 100644 --- a/crates/matrix-sdk/src/encryption/identities/devices.rs +++ b/crates/matrix-sdk/src/encryption/identities/devices.rs @@ -89,7 +89,7 @@ impl Device { /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// let device = client.get_device(alice, device_id!("DEVICEID")).await?; + /// let device = client.encryption().get_device(alice, device_id!("DEVICEID")).await?; /// /// if let Some(device) = device { /// let verification = device.request_verification().await?; @@ -137,7 +137,7 @@ impl Device { /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// let device = client.get_device(alice, device_id!("DEVICEID")).await?; + /// let device = client.encryption().get_device(alice, device_id!("DEVICEID")).await?; /// /// // We don't want to support showing a QR code, we only support SAS /// // verification @@ -179,7 +179,7 @@ impl Device { /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// let device = client.get_device(alice, device_id!("DEVICEID")).await?; + /// let device = client.encryption().get_device(alice, device_id!("DEVICEID")).await?; /// /// if let Some(device) = device { /// let verification = device.start_verification().await?; @@ -212,9 +212,9 @@ impl Device { /// key. /// /// The state of our private cross signing keys can be inspected using the - /// [`Client::cross_signing_status()`] method. + /// [`Encryption::cross_signing_status()`] method. /// - /// [`Client::cross_signing_status()`]: crate::Client::cross_signing_status + /// [`Encryption::cross_signing_status()`]: crate::encryption::Encryption::cross_signing_status /// /// ### Problems of manual verification /// @@ -243,7 +243,7 @@ impl Device { /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// let device = client.get_device(alice, device_id!("DEVICEID")).await?; + /// let device = client.encryption().get_device(alice, device_id!("DEVICEID")).await?; /// /// if let Some(device) = device { /// device.verify().await?; @@ -358,7 +358,7 @@ impl Device { /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// let device = client.get_device(alice, device_id!("DEVICEID")).await?; + /// let device = client.encryption().get_device(alice, device_id!("DEVICEID")).await?; /// /// if let Some(device) = device { /// if device.verified() { diff --git a/crates/matrix-sdk/src/encryption/identities/mod.rs b/crates/matrix-sdk/src/encryption/identities/mod.rs index e0beea868..3f95580f5 100644 --- a/crates/matrix-sdk/src/encryption/identities/mod.rs +++ b/crates/matrix-sdk/src/encryption/identities/mod.rs @@ -43,7 +43,7 @@ //! # let homeserver = Url::parse("http://example.com").unwrap(); //! # block_on(async { //! # let client = Client::new(homeserver).await.unwrap(); -//! let device = client.get_device(alice, device_id!("DEVICEID")).await?; +//! let device = client.encryption().get_device(alice, device_id!("DEVICEID")).await?; //! //! if let Some(device) = device { //! // Let's request the device to be verified. @@ -69,7 +69,7 @@ //! # let homeserver = Url::parse("http://example.com").unwrap(); //! # block_on(async { //! # let client = Client::new(homeserver).await.unwrap(); -//! let user = client.get_user_identity(alice).await?; +//! let user = client.encryption().get_user_identity(alice).await?; //! //! if let Some(user) = user { //! // Let's request the user to be verified. diff --git a/crates/matrix-sdk/src/encryption/identities/users.rs b/crates/matrix-sdk/src/encryption/identities/users.rs index b7424f6ca..c1d8bd42b 100644 --- a/crates/matrix-sdk/src/encryption/identities/users.rs +++ b/crates/matrix-sdk/src/encryption/identities/users.rs @@ -35,8 +35,8 @@ use crate::{encryption::verification::VerificationRequest, room::Joined, Client} /// /// The identity is backed by public [cross signing] keys that users upload. If /// our own user doesn't yet have such an identity, a new one can be created and -/// uploaded to the server using [`Client::bootstrap_cross_signing()`]. The user -/// identity can be also reset using the same method. +/// uploaded to the server using [`Encryption::bootstrap_cross_signing()`]. The +/// user identity can be also reset using the same method. /// /// The user identity consists of three separate `Ed25519` keypairs: /// @@ -63,6 +63,7 @@ use crate::{encryption::verification::VerificationRequest, room::Joined, Client} /// let us know whom the user verified. /// /// [cross signing]: https://spec.matrix.org/unstable/client-server-api/#cross-signing +/// [`Encryption::bootstrap_cross_signing()`]: crate::encryption::Encryption::bootstrap_cross_signing #[derive(Debug, Clone)] pub struct UserIdentity { inner: UserIdentities, @@ -97,7 +98,7 @@ impl UserIdentity { /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # futures::executor::block_on(async { /// # let client = Client::new(homeserver).await.unwrap(); - /// let user = client.get_user_identity(alice).await?; + /// let user = client.encryption().get_user_identity(alice).await?; /// /// if let Some(user) = user { /// println!("This user identity belongs to {}", user.user_id().as_str()); @@ -149,7 +150,7 @@ impl UserIdentity { /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # futures::executor::block_on(async { /// # let client = Client::new(homeserver).await.unwrap(); - /// let user = client.get_user_identity(alice).await?; + /// let user = client.encryption().get_user_identity(alice).await?; /// /// if let Some(user) = user { /// let verification = user.request_verification().await?; @@ -208,7 +209,7 @@ impl UserIdentity { /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { /// # let client = Client::new(homeserver).await.unwrap(); - /// let user = client.get_user_identity(alice).await?; + /// let user = client.encryption().get_user_identity(alice).await?; /// /// // We don't want to support showing a QR code, we only support SAS /// // verification @@ -252,7 +253,7 @@ impl UserIdentity { /// course fail if the private part of the User-signing key isn't available. /// /// The availability of the User-signing key can be checked using the - /// [`Client::cross_signing_status()`] method. + /// [`Encryption::cross_signing_status()`] method. /// /// ### Manually verifying our own user /// @@ -287,13 +288,14 @@ impl UserIdentity { /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { /// # let client = Client::new(homeserver).await.unwrap(); - /// let user = client.get_user_identity(alice).await?; + /// let user = client.encryption().get_user_identity(alice).await?; /// /// if let Some(user) = user { /// user.verify().await?; /// } /// # anyhow::Result::<()>::Ok(()) }); /// ``` + /// [`Encryption::cross_signing_status()`]: crate::encryption::Encryption::cross_signing_status pub async fn verify(&self) -> Result<(), ManualVerifyError> { match &self.inner { UserIdentities::Own(i) => i.verify().await, @@ -330,7 +332,7 @@ impl UserIdentity { /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { /// # let client = Client::new(homeserver).await.unwrap(); - /// let user = client.get_user_identity(alice).await?; + /// let user = client.encryption().get_user_identity(alice).await?; /// /// if let Some(user) = user { /// if user.verified() { @@ -370,7 +372,7 @@ impl UserIdentity { /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { /// # let client = Client::new(homeserver).await.unwrap(); - /// let user = client.get_user_identity(alice).await?; + /// let user = client.encryption().get_user_identity(alice).await?; /// /// if let Some(user) = user { /// // Let's verify the user after we confirm that the master key diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 58f7a54be..2f5810744 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -65,395 +65,6 @@ use crate::{ }; impl Client { - /// Get the public ed25519 key of our own device. This is usually what is - /// called the fingerprint of the device. - #[cfg(feature = "encryption")] - pub async fn ed25519_key(&self) -> Option { - self.olm_machine().await.map(|o| o.identity_keys().ed25519().to_owned()) - } - - /// Get the status of the private cross signing keys. - /// - /// This can be used to check which private cross signing keys we have - /// stored locally. - #[cfg(feature = "encryption")] - pub async fn cross_signing_status(&self) -> Option { - if let Some(machine) = self.olm_machine().await { - Some(machine.cross_signing_status().await) - } else { - None - } - } - - /// Get all the tracked users we know about - /// - /// Tracked users are users for which we keep the device list of E2EE - /// capable devices up to date. - #[cfg(feature = "encryption")] - pub async fn tracked_users(&self) -> HashSet> { - self.olm_machine().await.map(|o| o.tracked_users()).unwrap_or_default() - } - - /// Get a verification object with the given flow id. - #[cfg(feature = "encryption")] - pub async fn get_verification(&self, user_id: &UserId, flow_id: &str) -> Option { - let olm = self.olm_machine().await?; - olm.get_verification(user_id, flow_id).map(|v| match v { - matrix_sdk_base::crypto::Verification::SasV1(s) => { - SasVerification { inner: s, client: self.clone() }.into() - } - #[cfg(feature = "qrcode")] - matrix_sdk_base::crypto::Verification::QrV1(qr) => { - verification::QrVerification { inner: qr, client: self.clone() }.into() - } - }) - } - - /// Get a `VerificationRequest` object for the given user with the given - /// flow id. - #[cfg(feature = "encryption")] - pub async fn get_verification_request( - &self, - user_id: &UserId, - flow_id: impl AsRef, - ) -> Option { - let olm = self.olm_machine().await?; - - olm.get_verification_request(user_id, flow_id) - .map(|r| VerificationRequest { inner: r, client: self.clone() }) - } - - /// Get a specific device of a user. - /// - /// # Arguments - /// - /// * `user_id` - The unique id of the user that the device belongs to. - /// - /// * `device_id` - The unique id of the device. - /// - /// Returns a `Device` if one is found and the crypto store didn't throw an - /// error. - /// - /// This will always return None if the client hasn't been logged in. - /// - /// # Example - /// - /// ```no_run - /// # use std::convert::TryFrom; - /// # use matrix_sdk::{Client, ruma::{device_id, user_id}}; - /// # use url::Url; - /// # use futures::executor::block_on; - /// # block_on(async { - /// # let alice = user_id!("@alice:example.org"); - /// # let homeserver = Url::parse("http://example.com")?; - /// # let client = Client::new(homeserver).await?; - /// if let Some(device) = client.get_device(alice, device_id!("DEVICEID")).await? { - /// println!("{:?}", device.verified()); - /// - /// if !device.verified() { - /// let verification = device.request_verification().await?; - /// } - /// } - /// # anyhow::Result::<()>::Ok(()) }); - /// ``` - #[cfg(feature = "encryption")] - pub async fn get_device( - &self, - user_id: &UserId, - device_id: &DeviceId, - ) -> Result, CryptoStoreError> { - let device = self.base_client().get_device(user_id, device_id).await?; - - Ok(device.map(|d| Device { inner: d, client: self.clone() })) - } - - /// Get a map holding all the devices of an user. - /// - /// This will always return an empty map if the client hasn't been logged - /// in. - /// - /// # Arguments - /// - /// * `user_id` - The unique id of the user that the devices belong to. - /// - /// # Example - /// - /// ```no_run - /// # use std::convert::TryFrom; - /// # use matrix_sdk::{Client, ruma::user_id}; - /// # use url::Url; - /// # use futures::executor::block_on; - /// # block_on(async { - /// # let alice = user_id!("@alice:example.org"); - /// # let homeserver = Url::parse("http://example.com")?; - /// # let client = Client::new(homeserver).await?; - /// let devices = client.get_user_devices(alice).await?; - /// - /// for device in devices.devices() { - /// println!("{:?}", device); - /// } - /// # anyhow::Result::<()>::Ok(()) }); - /// ``` - #[cfg(feature = "encryption")] - pub async fn get_user_devices( - &self, - user_id: &UserId, - ) -> Result { - let devices = self.base_client().get_user_devices(user_id).await?; - - Ok(UserDevices { inner: devices, client: self.clone() }) - } - - /// Get a E2EE identity of an user. - /// - /// # Arguments - /// - /// * `user_id` - The unique id of the user that the identity belongs to. - /// - /// Returns a `UserIdentity` if one is found and the crypto store - /// didn't throw an error. - /// - /// This will always return None if the client hasn't been logged in. - /// - /// # Example - /// - /// ```no_run - /// # use std::convert::TryFrom; - /// # use matrix_sdk::{Client, ruma::user_id}; - /// # use url::Url; - /// # use futures::executor::block_on; - /// # block_on(async { - /// # let alice = user_id!("@alice:example.org"); - /// # let homeserver = Url::parse("http://example.com")?; - /// # let client = Client::new(homeserver).await?; - /// let user = client.get_user_identity(alice).await?; - /// - /// if let Some(user) = user { - /// println!("{:?}", user.verified()); - /// - /// let verification = user.request_verification().await?; - /// } - /// # anyhow::Result::<()>::Ok(()) }); - /// ``` - #[cfg(feature = "encryption")] - pub async fn get_user_identity( - &self, - user_id: &UserId, - ) -> Result, CryptoStoreError> { - use crate::encryption::identities::UserIdentity; - - if let Some(olm) = self.olm_machine().await { - let identity = olm.get_identity(user_id).await?; - - Ok(identity.map(|i| match i { - matrix_sdk_base::crypto::UserIdentities::Own(i) => { - UserIdentity::new_own(self.clone(), i) - } - matrix_sdk_base::crypto::UserIdentities::Other(i) => { - UserIdentity::new(self.clone(), i, self.get_dm_room(user_id)) - } - })) - } else { - Ok(None) - } - } - - /// Create and upload a new cross signing identity. - /// - /// # Arguments - /// - /// * `auth_data` - This request requires user interactive auth, the first - /// request needs to set this to `None` and will always fail with an - /// `UiaaResponse`. The response will contain information for the - /// interactive auth and the same request needs to be made but this time - /// with some `auth_data` provided. - /// - /// # Examples - /// ```no_run - /// # use std::{convert::TryFrom, collections::BTreeMap}; - /// # use matrix_sdk::{ - /// # ruma::{api::client::uiaa, assign}, - /// # Client, - /// # }; - /// # use url::Url; - /// # use futures::executor::block_on; - /// # use serde_json::json; - /// # block_on(async { - /// # let homeserver = Url::parse("http://example.com")?; - /// # let client = Client::new(homeserver).await?; - /// if let Err(e) = client.bootstrap_cross_signing(None).await { - /// if let Some(response) = e.uiaa_response() { - /// let auth_data = uiaa::AuthData::Password(assign!( - /// uiaa::Password::new( - /// uiaa::UserIdentifier::UserIdOrLocalpart("example"), - /// "wordpass", - /// ), { - /// session: response.session.as_deref(), - /// } - /// )); - /// - /// client - /// .bootstrap_cross_signing(Some(auth_data)) - /// .await - /// .expect("Couldn't bootstrap cross signing") - /// } else { - /// panic!("Error durign cross signing bootstrap {:#?}", e); - /// } - /// } - /// # anyhow::Result::<()>::Ok(()) }); - #[cfg(feature = "encryption")] - pub async fn bootstrap_cross_signing(&self, auth_data: Option>) -> Result<()> { - let olm = self.olm_machine().await.ok_or(Error::AuthenticationRequired)?; - - let (request, signature_request) = olm.bootstrap_cross_signing(false).await?; - - let to_raw = |k| Raw::new(&k).expect("Can't serialize newly created cross signing keys"); - - let request = assign!(UploadSigningKeysRequest::new(), { - auth: auth_data, - master_key: request.master_key.map(to_raw), - self_signing_key: request.self_signing_key.map(to_raw), - user_signing_key: request.user_signing_key.map(to_raw), - }); - - self.send(request, None).await?; - self.send(signature_request, None).await?; - - Ok(()) - } - - /// Export E2EE keys that match the given predicate encrypting them with the - /// given passphrase. - /// - /// # Arguments - /// - /// * `path` - The file path where the exported key file will be saved. - /// - /// * `passphrase` - The passphrase that will be used to encrypt the - /// exported - /// room keys. - /// - /// * `predicate` - A closure that will be called for every known - /// `InboundGroupSession`, which represents a room key. If the closure - /// returns `true` the `InboundGroupSessoin` will be included in the export, - /// if the closure returns `false` it will not be included. - /// - /// # Panics - /// - /// This method will panic if it isn't run on a Tokio runtime. - /// - /// This method will panic if it can't get enough randomness from the OS to - /// encrypt the exported keys securely. - /// - /// # Examples - /// - /// ```no_run - /// # use std::{path::PathBuf, time::Duration}; - /// # use matrix_sdk::{ - /// # Client, config::SyncSettings, - /// # ruma::room_id, - /// # }; - /// # use futures::executor::block_on; - /// # use url::Url; - /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080")?; - /// # let mut client = Client::new(homeserver).await?; - /// let path = PathBuf::from("/home/example/e2e-keys.txt"); - /// // Export all room keys. - /// client - /// .export_keys(path, "secret-passphrase", |_| true) - /// .await?; - /// - /// // Export only the room keys for a certain room. - /// let path = PathBuf::from("/home/example/e2e-room-keys.txt"); - /// let room_id = room_id!("!test:localhost"); - /// - /// client - /// .export_keys(path, "secret-passphrase", |s| s.room_id() == room_id) - /// .await?; - /// # anyhow::Result::<()>::Ok(()) }); - /// ``` - #[cfg(all(feature = "encryption", not(target_arch = "wasm32")))] - pub async fn export_keys( - &self, - path: PathBuf, - passphrase: &str, - predicate: impl FnMut(&matrix_sdk_base::crypto::olm::InboundGroupSession) -> bool, - ) -> Result<()> { - let olm = self.olm_machine().await.ok_or(Error::AuthenticationRequired)?; - - let keys = olm.export_keys(predicate).await?; - let passphrase = zeroize::Zeroizing::new(passphrase.to_owned()); - - let encrypt = move || -> Result<()> { - let export: String = - matrix_sdk_base::crypto::encrypt_key_export(&keys, &passphrase, 500_000)?; - let mut file = std::fs::File::create(path)?; - file.write_all(&export.into_bytes())?; - Ok(()) - }; - - let task = tokio::task::spawn_blocking(encrypt); - task.await.expect("Task join error") - } - - /// Import E2EE keys from the given file path. - /// - /// # Arguments - /// - /// * `path` - The file path where the exported key file will can be found. - /// - /// * `passphrase` - The passphrase that should be used to decrypt the - /// exported room keys. - /// - /// Returns a tuple of numbers that represent the number of sessions that - /// were imported and the total number of sessions that were found in the - /// key export. - /// - /// # Panics - /// - /// This method will panic if it isn't run on a Tokio runtime. - /// - /// ```no_run - /// # use std::{path::PathBuf, time::Duration}; - /// # use matrix_sdk::{ - /// # Client, config::SyncSettings, - /// # ruma::room_id, - /// # }; - /// # use futures::executor::block_on; - /// # use url::Url; - /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080")?; - /// # let mut client = Client::new(homeserver).await?; - /// let path = PathBuf::from("/home/example/e2e-keys.txt"); - /// let result = client.import_keys(path, "secret-passphrase").await?; - /// - /// println!( - /// "Imported {} room keys out of {}", - /// result.imported_count, result.total_count - /// ); - /// # anyhow::Result::<()>::Ok(()) }); - /// ``` - #[cfg(all(feature = "encryption", not(target_arch = "wasm32")))] - pub async fn import_keys( - &self, - path: PathBuf, - passphrase: &str, - ) -> Result { - let olm = self.olm_machine().await.ok_or(RoomKeyImportError::StoreClosed)?; - let passphrase = zeroize::Zeroizing::new(passphrase.to_owned()); - - let decrypt = move || { - let file = std::fs::File::open(path)?; - matrix_sdk_base::crypto::decrypt_key_export(file, &passphrase) - }; - - let task = tokio::task::spawn_blocking(decrypt); - let import = task.await.expect("Task join error")?; - - Ok(olm.import_keys(import, false, |_, _| {}).await?) - } - /// Tries to decrypt a `AnyRoomEvent`. Returns undecrypted room event when /// decryption fails. #[cfg(feature = "encryption")] @@ -877,3 +488,406 @@ impl Client { Ok(()) } } + +/// A high-level API to manage the client's encryption. +/// +/// To get this, use [`Client::encryption()`]. +#[cfg(feature = "encryption")] +#[derive(Debug, Clone)] +pub struct Encryption { + /// The underlying client. + client: Client, +} + +#[cfg(feature = "encryption")] +impl Encryption { + pub(crate) fn new(client: Client) -> Self { + Self { client } + } + + /// Get the public ed25519 key of our own device. This is usually what is + /// called the fingerprint of the device. + pub async fn ed25519_key(&self) -> Option { + self.client.olm_machine().await.map(|o| o.identity_keys().ed25519().to_owned()) + } + + /// Get the status of the private cross signing keys. + /// + /// This can be used to check which private cross signing keys we have + /// stored locally. + pub async fn cross_signing_status(&self) -> Option { + if let Some(machine) = self.client.olm_machine().await { + Some(machine.cross_signing_status().await) + } else { + None + } + } + + /// Get all the tracked users we know about + /// + /// Tracked users are users for which we keep the device list of E2EE + /// capable devices up to date. + pub async fn tracked_users(&self) -> HashSet> { + self.client.olm_machine().await.map(|o| o.tracked_users()).unwrap_or_default() + } + + /// Get a verification object with the given flow id. + pub async fn get_verification(&self, user_id: &UserId, flow_id: &str) -> Option { + let olm = self.client.olm_machine().await?; + olm.get_verification(user_id, flow_id).map(|v| match v { + matrix_sdk_base::crypto::Verification::SasV1(s) => { + SasVerification { inner: s, client: self.client.clone() }.into() + } + #[cfg(feature = "qrcode")] + matrix_sdk_base::crypto::Verification::QrV1(qr) => { + verification::QrVerification { inner: qr, client: self.client.clone() }.into() + } + }) + } + + /// Get a `VerificationRequest` object for the given user with the given + /// flow id. + pub async fn get_verification_request( + &self, + user_id: &UserId, + flow_id: impl AsRef, + ) -> Option { + let olm = self.client.olm_machine().await?; + + olm.get_verification_request(user_id, flow_id) + .map(|r| VerificationRequest { inner: r, client: self.client.clone() }) + } + + /// Get a specific device of a user. + /// + /// # Arguments + /// + /// * `user_id` - The unique id of the user that the device belongs to. + /// + /// * `device_id` - The unique id of the device. + /// + /// Returns a `Device` if one is found and the crypto store didn't throw an + /// error. + /// + /// This will always return None if the client hasn't been logged in. + /// + /// # Example + /// + /// ```no_run + /// # use std::convert::TryFrom; + /// # use matrix_sdk::{Client, ruma::{device_id, user_id}}; + /// # use url::Url; + /// # use futures::executor::block_on; + /// # block_on(async { + /// # let alice = user_id!("@alice:example.org"); + /// # let homeserver = Url::parse("http://example.com")?; + /// # let client = Client::new(homeserver).await?; + /// if let Some(device) = client + /// .encryption() + /// .get_device(alice, device_id!("DEVICEID")) + /// .await? { + /// println!("{:?}", device.verified()); + /// + /// if !device.verified() { + /// let verification = device.request_verification().await?; + /// } + /// } + /// # anyhow::Result::<()>::Ok(()) }); + /// ``` + pub async fn get_device( + &self, + user_id: &UserId, + device_id: &DeviceId, + ) -> Result, CryptoStoreError> { + let device = self.client.base_client().get_device(user_id, device_id).await?; + + Ok(device.map(|d| Device { inner: d, client: self.client.clone() })) + } + + /// Get a map holding all the devices of an user. + /// + /// This will always return an empty map if the client hasn't been logged + /// in. + /// + /// # Arguments + /// + /// * `user_id` - The unique id of the user that the devices belong to. + /// + /// # Example + /// + /// ```no_run + /// # use std::convert::TryFrom; + /// # use matrix_sdk::{Client, ruma::user_id}; + /// # use url::Url; + /// # use futures::executor::block_on; + /// # block_on(async { + /// # let alice = user_id!("@alice:example.org"); + /// # let homeserver = Url::parse("http://example.com")?; + /// # let client = Client::new(homeserver).await?; + /// let devices = client.encryption().get_user_devices(alice).await?; + /// + /// for device in devices.devices() { + /// println!("{:?}", device); + /// } + /// # anyhow::Result::<()>::Ok(()) }); + /// ``` + pub async fn get_user_devices( + &self, + user_id: &UserId, + ) -> Result { + let devices = self.client.base_client().get_user_devices(user_id).await?; + + Ok(UserDevices { inner: devices, client: self.client.clone() }) + } + + /// Get a E2EE identity of an user. + /// + /// # Arguments + /// + /// * `user_id` - The unique id of the user that the identity belongs to. + /// + /// Returns a `UserIdentity` if one is found and the crypto store + /// didn't throw an error. + /// + /// This will always return None if the client hasn't been logged in. + /// + /// # Example + /// + /// ```no_run + /// # use std::convert::TryFrom; + /// # use matrix_sdk::{Client, ruma::user_id}; + /// # use url::Url; + /// # use futures::executor::block_on; + /// # block_on(async { + /// # let alice = user_id!("@alice:example.org"); + /// # let homeserver = Url::parse("http://example.com")?; + /// # let client = Client::new(homeserver).await?; + /// let user = client.encryption().get_user_identity(alice).await?; + /// + /// if let Some(user) = user { + /// println!("{:?}", user.verified()); + /// + /// let verification = user.request_verification().await?; + /// } + /// # anyhow::Result::<()>::Ok(()) }); + /// ``` + pub async fn get_user_identity( + &self, + user_id: &UserId, + ) -> Result, CryptoStoreError> { + use crate::encryption::identities::UserIdentity; + + if let Some(olm) = self.client.olm_machine().await { + let identity = olm.get_identity(user_id).await?; + + Ok(identity.map(|i| match i { + matrix_sdk_base::crypto::UserIdentities::Own(i) => { + UserIdentity::new_own(self.client.clone(), i) + } + matrix_sdk_base::crypto::UserIdentities::Other(i) => { + UserIdentity::new(self.client.clone(), i, self.client.get_dm_room(user_id)) + } + })) + } else { + Ok(None) + } + } + + /// Create and upload a new cross signing identity. + /// + /// # Arguments + /// + /// * `auth_data` - This request requires user interactive auth, the first + /// request needs to set this to `None` and will always fail with an + /// `UiaaResponse`. The response will contain information for the + /// interactive auth and the same request needs to be made but this time + /// with some `auth_data` provided. + /// + /// # Examples + /// ```no_run + /// # use std::{convert::TryFrom, collections::BTreeMap}; + /// # use matrix_sdk::{ + /// # ruma::{api::client::uiaa, assign}, + /// # Client, + /// # }; + /// # use url::Url; + /// # use futures::executor::block_on; + /// # use serde_json::json; + /// # block_on(async { + /// # let homeserver = Url::parse("http://example.com")?; + /// # let client = Client::new(homeserver).await?; + /// if let Err(e) = client.encryption().bootstrap_cross_signing(None).await { + /// if let Some(response) = e.uiaa_response() { + /// let auth_data = uiaa::AuthData::Password(assign!( + /// uiaa::Password::new( + /// uiaa::UserIdentifier::UserIdOrLocalpart("example"), + /// "wordpass", + /// ), { + /// session: response.session.as_deref(), + /// } + /// )); + /// + /// client + /// .encryption() + /// .bootstrap_cross_signing(Some(auth_data)) + /// .await + /// .expect("Couldn't bootstrap cross signing") + /// } else { + /// panic!("Error durign cross signing bootstrap {:#?}", e); + /// } + /// } + /// # anyhow::Result::<()>::Ok(()) }); + pub async fn bootstrap_cross_signing(&self, auth_data: Option>) -> Result<()> { + let olm = self.client.olm_machine().await.ok_or(Error::AuthenticationRequired)?; + + let (request, signature_request) = olm.bootstrap_cross_signing(false).await?; + + let to_raw = |k| Raw::new(&k).expect("Can't serialize newly created cross signing keys"); + + let request = assign!(UploadSigningKeysRequest::new(), { + auth: auth_data, + master_key: request.master_key.map(to_raw), + self_signing_key: request.self_signing_key.map(to_raw), + user_signing_key: request.user_signing_key.map(to_raw), + }); + + self.client.send(request, None).await?; + self.client.send(signature_request, None).await?; + + Ok(()) + } + + /// Export E2EE keys that match the given predicate encrypting them with the + /// given passphrase. + /// + /// # Arguments + /// + /// * `path` - The file path where the exported key file will be saved. + /// + /// * `passphrase` - The passphrase that will be used to encrypt the + /// exported + /// room keys. + /// + /// * `predicate` - A closure that will be called for every known + /// `InboundGroupSession`, which represents a room key. If the closure + /// returns `true` the `InboundGroupSessoin` will be included in the export, + /// if the closure returns `false` it will not be included. + /// + /// # Panics + /// + /// This method will panic if it isn't run on a Tokio runtime. + /// + /// This method will panic if it can't get enough randomness from the OS to + /// encrypt the exported keys securely. + /// + /// # Examples + /// + /// ```no_run + /// # use std::{path::PathBuf, time::Duration}; + /// # use matrix_sdk::{ + /// # Client, config::SyncSettings, + /// # ruma::room_id, + /// # }; + /// # use futures::executor::block_on; + /// # use url::Url; + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let mut client = Client::new(homeserver).await?; + /// let path = PathBuf::from("/home/example/e2e-keys.txt"); + /// // Export all room keys. + /// client + /// .encryption() + /// .export_keys(path, "secret-passphrase", |_| true) + /// .await?; + /// + /// // Export only the room keys for a certain room. + /// let path = PathBuf::from("/home/example/e2e-room-keys.txt"); + /// let room_id = room_id!("!test:localhost"); + /// + /// client + /// .encryption() + /// .export_keys(path, "secret-passphrase", |s| s.room_id() == room_id) + /// .await?; + /// # anyhow::Result::<()>::Ok(()) }); + /// ``` + #[cfg(not(target_arch = "wasm32"))] + pub async fn export_keys( + &self, + path: PathBuf, + passphrase: &str, + predicate: impl FnMut(&matrix_sdk_base::crypto::olm::InboundGroupSession) -> bool, + ) -> Result<()> { + let olm = self.client.olm_machine().await.ok_or(Error::AuthenticationRequired)?; + + let keys = olm.export_keys(predicate).await?; + let passphrase = zeroize::Zeroizing::new(passphrase.to_owned()); + + let encrypt = move || -> Result<()> { + let export: String = + matrix_sdk_base::crypto::encrypt_key_export(&keys, &passphrase, 500_000)?; + let mut file = std::fs::File::create(path)?; + file.write_all(&export.into_bytes())?; + Ok(()) + }; + + let task = tokio::task::spawn_blocking(encrypt); + task.await.expect("Task join error") + } + + /// Import E2EE keys from the given file path. + /// + /// # Arguments + /// + /// * `path` - The file path where the exported key file will can be found. + /// + /// * `passphrase` - The passphrase that should be used to decrypt the + /// exported room keys. + /// + /// Returns a tuple of numbers that represent the number of sessions that + /// were imported and the total number of sessions that were found in the + /// key export. + /// + /// # Panics + /// + /// This method will panic if it isn't run on a Tokio runtime. + /// + /// ```no_run + /// # use std::{path::PathBuf, time::Duration}; + /// # use matrix_sdk::{ + /// # Client, config::SyncSettings, + /// # ruma::room_id, + /// # }; + /// # use futures::executor::block_on; + /// # use url::Url; + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let mut client = Client::new(homeserver).await?; + /// let path = PathBuf::from("/home/example/e2e-keys.txt"); + /// let result = client.encryption().import_keys(path, "secret-passphrase").await?; + /// + /// println!( + /// "Imported {} room keys out of {}", + /// result.imported_count, result.total_count + /// ); + /// # anyhow::Result::<()>::Ok(()) }); + /// ``` + #[cfg(not(target_arch = "wasm32"))] + pub async fn import_keys( + &self, + path: PathBuf, + passphrase: &str, + ) -> Result { + let olm = self.client.olm_machine().await.ok_or(RoomKeyImportError::StoreClosed)?; + let passphrase = zeroize::Zeroizing::new(passphrase.to_owned()); + + let decrypt = move || { + let file = std::fs::File::open(path)?; + matrix_sdk_base::crypto::decrypt_key_export(file, &passphrase) + }; + + let task = tokio::task::spawn_blocking(decrypt); + let import = task.await.expect("Task join error")?; + + Ok(olm.import_keys(import, false, |_, _| {}).await?) + } +} diff --git a/crates/matrix-sdk/src/encryption/verification/sas.rs b/crates/matrix-sdk/src/encryption/verification/sas.rs index c2cb6e9f9..a5ee0bf16 100644 --- a/crates/matrix-sdk/src/encryption/verification/sas.rs +++ b/crates/matrix-sdk/src/encryption/verification/sas.rs @@ -54,6 +54,7 @@ impl SasVerification { /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; /// let sas = client + /// .encryption() /// .get_verification(&user_id, flow_id) /// .await /// .and_then(|v| v.sas()); @@ -128,6 +129,7 @@ impl SasVerification { /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; /// let sas_verification = client + /// .encryption() /// .get_verification(&user_id, flow_id) /// .await /// .and_then(|v| v.sas()); diff --git a/crates/matrix-sdk/src/room/common.rs b/crates/matrix-sdk/src/room/common.rs index b0218114d..4d20437ff 100644 --- a/crates/matrix-sdk/src/room/common.rs +++ b/crates/matrix-sdk/src/room/common.rs @@ -667,7 +667,7 @@ impl Common { let user_ids = self.client.store().get_user_ids(self.room_id()).await?; for user_id in user_ids { - let devices = self.client.get_user_devices(&user_id).await?; + let devices = self.client.encryption().get_user_devices(&user_id).await?; let any_unverified = devices.devices().any(|d| !d.verified()); if any_unverified {