diff --git a/matrix_sdk/Cargo.toml b/matrix_sdk/Cargo.toml index 4165c43d..15d8e788 100644 --- a/matrix_sdk/Cargo.toml +++ b/matrix_sdk/Cargo.toml @@ -20,7 +20,7 @@ sqlite-cryptostore = ["matrix-sdk-base/sqlite-cryptostore"] http = "0.2.1" reqwest = "0.10.4" serde_json = "1.0.53" -thiserror = "1.0.18" +thiserror = "1.0.19" tracing = "0.1.14" url = "2.1.1" futures-timer = { version = "3.0.2", features = ["wasm-bindgen"] } diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index 322a51c7..1a801db9 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -17,6 +17,7 @@ use std::collections::BTreeMap; use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; +use std::path::Path; use std::result::Result as StdResult; use std::sync::Arc; @@ -47,10 +48,7 @@ use crate::api; #[cfg(not(target_arch = "wasm32"))] use crate::VERSION; use crate::{Error, EventEmitter, Result}; -use matrix_sdk_base::BaseClient; -use matrix_sdk_base::Room; -use matrix_sdk_base::Session; -use matrix_sdk_base::StateStore; +use matrix_sdk_base::{BaseClient, BaseClientConfig, Room, Session, StateStore}; const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(30); @@ -104,7 +102,7 @@ pub struct ClientConfig { proxy: Option, user_agent: Option, disable_ssl_verification: bool, - state_store: Option>, + base_config: BaseClientConfig, } #[cfg_attr(tarpaulin, skip)] @@ -166,7 +164,36 @@ impl ClientConfig { /// /// The state store should be opened before being set. pub fn state_store(mut self, store: Box) -> Self { - self.state_store = Some(store); + self.base_config = self.base_config.state_store(store); + self + } + + /// Set the path for storage. + /// + /// # Arguments + /// + /// * `path` - The path where the stores should save data in. It is the + /// callers responsibility to make sure that the path exists. + /// + /// In the default configuration the client will open default + /// implementations for the crypto store and the state store. It will use + /// the given path to open the stores. If no path is provided no store will + /// be opened + pub fn store_path>(mut self, path: P) -> Self { + self.base_config = self.base_config.store_path(path); + self + } + + /// Set the passphrase to encrypt the crypto store. + /// + /// # Argument + /// + /// * `passphrase` - The passphrase that will be used to encrypt the data in + /// the cryptostore. + /// + /// This is only used if no custom cryptostore is set. + pub fn passphrase(mut self, passphrase: String) -> Self { + self.base_config = self.base_config.passphrase(passphrase); self } } @@ -293,11 +320,7 @@ impl Client { let http_client = http_client.build()?; - let base_client = if let Some(store) = config.state_store { - BaseClient::new_with_state_store(store)? - } else { - BaseClient::new()? - }; + let base_client = BaseClient::new_with_config(config.base_config)?; Ok(Self { homeserver, @@ -371,38 +394,6 @@ impl Client { self.base_client.get_left_room(room_id).await } - /// This allows `Client` to manually sync state with the provided `StateStore`. - /// - /// Returns true when a successful `StateStore` sync has completed. - /// - /// # Examples - /// - /// ```no_run - /// use matrix_sdk::{Client, ClientConfig, JsonStore, RoomBuilder}; - /// # use matrix_sdk::api::r0::room::Visibility; - /// # use url::Url; - /// - /// # let homeserver = Url::parse("http://example.com").unwrap(); - /// let store = JsonStore::open("path/to/store").unwrap(); - /// let config = ClientConfig::new().state_store(Box::new(store)); - /// let mut client = Client::new(homeserver).unwrap(); - /// # use futures::executor::block_on; - /// # block_on(async { - /// let _ = client.login("name", "password", None, None).await.unwrap(); - /// // returns true when a state store sync is successful - /// assert!(client.sync_with_state_store().await.unwrap()); - /// // now state is restored without a request to the server - /// let mut names = vec![]; - /// for r in client.joined_rooms().read().await.values() { - /// names.push(r.read().await.display_name()); - /// } - /// assert_eq!(vec!["room".to_string(), "names".to_string()], names) - /// # }); - /// ``` - pub async fn sync_with_state_store(&self) -> Result { - Ok(self.base_client.sync_with_state_store().await?) - } - /// This allows `Client` to manually store `Room` state with the provided /// `StateStore`. /// @@ -477,8 +468,6 @@ impl Client { self.send(request).await } - // TODO enable this once Ruma supports proper serialization of the query - // string. /// Join a room by `RoomId`. /// /// Returns a `join_room_by_id_or_alias::Response` consisting of the @@ -1441,8 +1430,6 @@ mod test { ); } - // TODO enable this once Ruma supports proper serialization of the query - // string. #[tokio::test] async fn join_room_by_id_or_alias() { let homeserver = Url::from_str(&mockito::server_url()).unwrap(); @@ -1798,7 +1785,7 @@ mod test { .unwrap(); assert_eq!( - EventId::try_from("$h29iv0s8:example.com").ok(), + EventId::try_from("$h29iv0s8:example.com").unwrap(), response.event_id ) } diff --git a/matrix_sdk_base/Cargo.toml b/matrix_sdk_base/Cargo.toml index cb7ec9f4..389e98ac 100644 --- a/matrix_sdk_base/Cargo.toml +++ b/matrix_sdk_base/Cargo.toml @@ -20,12 +20,13 @@ sqlite-cryptostore = ["matrix-sdk-crypto/sqlite-cryptostore"] async-trait = "0.1.31" serde = "1.0.110" serde_json = "1.0.53" +zeroize = "1.1.0" matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" } matrix-sdk-crypto = { version = "0.1.0", path = "../matrix_sdk_crypto", optional = true } # Misc dependencies -thiserror = "1.0.18" +thiserror = "1.0.19" [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] version = "0.2.21" diff --git a/matrix_sdk_base/src/client.rs b/matrix_sdk_base/src/client.rs index 78abde52..875302e1 100644 --- a/matrix_sdk_base/src/client.rs +++ b/matrix_sdk_base/src/client.rs @@ -17,10 +17,10 @@ use std::collections::HashMap; #[cfg(feature = "encryption")] use std::collections::{BTreeMap, HashSet}; use std::fmt; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::path::{Path, PathBuf}; use std::sync::Arc; +use zeroize::Zeroizing; -#[cfg(feature = "encryption")] use std::result::Result as StdResult; use crate::api::r0 as api; @@ -55,8 +55,10 @@ use crate::api::r0::to_device::send_event_to_device; use crate::events::room::{encrypted::EncryptedEventContent, message::MessageEventContent}; #[cfg(feature = "encryption")] use crate::identifiers::DeviceId; +#[cfg(not(target_arch = "wasm32"))] +use crate::JsonStore; #[cfg(feature = "encryption")] -use matrix_sdk_crypto::{OlmMachine, OneTimeKeys}; +use matrix_sdk_crypto::{CryptoStore, OlmMachine, OneTimeKeys}; pub type Token = String; @@ -115,11 +117,13 @@ pub struct BaseClient { /// /// There is a default implementation `JsonStore` that saves JSON to disk. state_store: Arc>>>, - /// Does the `Client` need to sync with the state store. - needs_state_store_sync: Arc, #[cfg(feature = "encryption")] olm: Arc>>, + #[cfg(feature = "encryption")] + cryptostore: Arc>>>, + store_path: Arc>, + store_passphrase: Arc>, } #[cfg_attr(tarpaulin, skip)] @@ -136,31 +140,99 @@ impl fmt::Debug for BaseClient { } } +/// Configuration for the creation of the `BaseClient`. +/// +/// # Example +/// +/// ``` +/// # use matrix_sdk_base::BaseClientConfig; +/// +/// let client_config = BaseClientConfig::new() +/// .store_path("/home/example/matrix-sdk-client") +/// .passphrase("test-passphrase".to_owned()); +/// ``` +#[derive(Default)] +pub struct BaseClientConfig { + state_store: Option>, + #[cfg(feature = "encryption")] + crypto_store: Option>, + store_path: Option, + passphrase: Option>, +} + +#[cfg_attr(tarpaulin, skip)] +impl std::fmt::Debug for BaseClientConfig { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> { + fmt.debug_struct("BaseClientConfig").finish() + } +} + +impl BaseClientConfig { + /// Create a new default `BaseClientConfig`. + pub fn new() -> Self { + Default::default() + } + + /// Set a custom implementation of a `StateStore`. + /// + /// The state store should be opened before being set. + pub fn state_store(mut self, store: Box) -> Self { + self.state_store = Some(store); + self + } + + /// Set a custom implementation of a `CryptoStore`. + /// + /// The crypto store should be opened before being set. + #[cfg(feature = "encryption")] + pub fn crypto_store(mut self, store: Box) -> Self { + self.crypto_store = Some(store); + self + } + + /// Set the path for storage. + /// + /// # Arguments + /// + /// * `path` - The path where the stores should save data in. It is the + /// callers responsibility to make sure that the path exists. + /// + /// In the default configuration the client will open default + /// implementations for the crypto store and the state store. It will use + /// the given path to open the stores. If no path is provided no store will + /// be opened + pub fn store_path>(mut self, path: P) -> Self { + self.store_path = Some(path.as_ref().into()); + self + } + + /// Set the passphrase to encrypt the crypto store. + /// + /// # Argument + /// + /// * `passphrase` - The passphrase that will be used to encrypt the data in + /// the cryptostore. + /// + /// This is only used if no custom cryptostore is set. + pub fn passphrase(mut self, passphrase: String) -> Self { + self.passphrase = Some(Zeroizing::new(passphrase)); + self + } +} + impl BaseClient { - /// Create a new client. - /// - /// # Arguments - /// - /// * `session` - An optional session if the user already has one from a - /// previous login call. + /// Create a new default client. pub fn new() -> Result { - BaseClient::new_helper(None) + BaseClient::new_with_config(BaseClientConfig::default()) } /// Create a new client. /// /// # Arguments /// - /// * `session` - An optional session if the user already has one from a + /// * `config` - An optional session if the user already has one from a /// previous login call. - /// - /// * `store` - An open state store implementation that will be used through - /// the lifetime of the client. - pub fn new_with_state_store(store: Box) -> Result { - BaseClient::new_helper(Some(store)) - } - - fn new_helper(store: Option>) -> Result { + pub fn new_with_config(config: BaseClientConfig) -> Result { Ok(BaseClient { session: Arc::new(RwLock::new(None)), sync_token: Arc::new(RwLock::new(None)), @@ -170,10 +242,17 @@ impl BaseClient { ignored_users: Arc::new(RwLock::new(Vec::new())), push_ruleset: Arc::new(RwLock::new(None)), event_emitter: Arc::new(RwLock::new(None)), - state_store: Arc::new(RwLock::new(store)), - needs_state_store_sync: Arc::new(AtomicBool::from(true)), + state_store: Arc::new(RwLock::new(config.state_store)), #[cfg(feature = "encryption")] olm: Arc::new(Mutex::new(None)), + #[cfg(feature = "encryption")] + cryptostore: Arc::new(Mutex::new(config.crypto_store)), + store_path: Arc::new(config.store_path), + store_passphrase: Arc::new( + config + .passphrase + .unwrap_or_else(|| Zeroizing::new("DEFAULT_PASSPHRASE".to_owned())), + ), }) } @@ -197,55 +276,55 @@ impl BaseClient { *self.event_emitter.write().await = Some(emitter); } - /// Returns true if the state store has been loaded into the client. - pub fn is_state_store_synced(&self) -> bool { - !self.needs_state_store_sync.load(Ordering::Relaxed) - } - /// When a client is provided the state store will load state from the `StateStore`. /// /// Returns `true` when a state store sync has successfully completed. - pub async fn sync_with_state_store(&self) -> Result { + async fn sync_with_state_store(&self, session: &Session) -> Result { let store = self.state_store.read().await; - if let Some(store) = store.as_ref() { - if let Some(sess) = self.session.read().await.as_ref() { - if let Some(client_state) = store.load_client_state(sess).await? { - let ClientState { - sync_token, - ignored_users, - push_ruleset, - } = client_state; - *self.sync_token.write().await = sync_token; - *self.ignored_users.write().await = ignored_users; - *self.push_ruleset.write().await = push_ruleset; - } else { - // return false and continues with a sync request then save the state and create - // and populate the files during the sync - return Ok(false); - } - let AllRooms { - mut joined, - mut invited, - mut left, - } = store.load_all_rooms().await?; - *self.joined_rooms.write().await = joined - .drain() - .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) - .collect(); - *self.invited_rooms.write().await = invited - .drain() - .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) - .collect(); - *self.left_rooms.write().await = left - .drain() - .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) - .collect(); - - self.needs_state_store_sync.store(false, Ordering::Relaxed); + let loaded = if let Some(store) = store.as_ref() { + if let Some(client_state) = store.load_client_state(session).await? { + let ClientState { + sync_token, + ignored_users, + push_ruleset, + } = client_state; + *self.sync_token.write().await = sync_token; + *self.ignored_users.write().await = ignored_users; + *self.push_ruleset.write().await = push_ruleset; + } else { + // return false and continues with a sync request then save the state and create + // and populate the files during the sync + return Ok(false); } - } - Ok(!self.needs_state_store_sync.load(Ordering::Relaxed)) + + let AllRooms { + mut joined, + mut invited, + mut left, + } = store.load_all_rooms().await?; + + *self.joined_rooms.write().await = joined + .drain() + .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) + .collect(); + + *self.invited_rooms.write().await = invited + .drain() + .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) + .collect(); + + *self.left_rooms.write().await = left + .drain() + .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) + .collect(); + + true + } else { + false + }; + + Ok(loaded) } /// When a client is provided the state store will load state from the `StateStore`. @@ -303,9 +382,54 @@ impl BaseClient { #[cfg(feature = "encryption")] { let mut olm = self.olm.lock().await; - *olm = Some(OlmMachine::new(&session.user_id, &session.device_id)); + let store = self.cryptostore.lock().await.take(); + + if let Some(store) = store { + *olm = Some( + OlmMachine::new_with_store( + session.user_id.to_owned(), + session.device_id.to_owned(), + store, + ) + .await + .unwrap(), + ); + } else if let Some(path) = self.store_path.as_ref() { + #[cfg(feature = "sqlite-cryptostore")] + { + *olm = Some( + OlmMachine::new_with_default_store( + &session.user_id, + &session.device_id, + path, + &self.store_passphrase, + ) + .await + .unwrap(), + ); + } + #[cfg(not(feature = "sqlite-cryptostore"))] + { + *olm = Some(OlmMachine::new(&session.user_id, &session.device_id)); + } + } else { + *olm = Some(OlmMachine::new(&session.user_id, &session.device_id)); + } } - self.sync_with_state_store().await?; + + // If there wasn't a state store opened, try to open the default one if + // a store path was provided. + if self.state_store.read().await.is_none() { + #[cfg(not(target_arch = "wasm32"))] + { + if let Some(path) = &*self.store_path { + let store = JsonStore::open(path)?; + *self.state_store.write().await = Some(Box::new(store)); + } + } + } + + self.sync_with_state_store(&session).await?; *self.session.write().await = Some(session); @@ -713,8 +837,6 @@ impl BaseClient { /// # Arguments /// /// * `response` - The response that we received after a successful sync. - /// - /// * `did_update` - Signals to the `StateStore` if the client state needs updating. pub async fn receive_sync_response( &self, response: &mut api::sync::sync_events::Response, diff --git a/matrix_sdk_base/src/lib.rs b/matrix_sdk_base/src/lib.rs index da31f38a..52b78049 100644 --- a/matrix_sdk_base/src/lib.rs +++ b/matrix_sdk_base/src/lib.rs @@ -45,7 +45,7 @@ mod models; mod session; mod state; -pub use client::{BaseClient, RoomState, RoomStateType}; +pub use client::{BaseClient, BaseClientConfig, RoomState, RoomStateType}; pub use event_emitter::{EventEmitter, SyncRoom}; #[cfg(feature = "encryption")] pub use matrix_sdk_crypto::{Device, TrustState}; diff --git a/matrix_sdk_base/src/state/json_store.rs b/matrix_sdk_base/src/state/json_store.rs index b7b08d4a..743040ff 100644 --- a/matrix_sdk_base/src/state/json_store.rs +++ b/matrix_sdk_base/src/state/json_store.rs @@ -205,7 +205,7 @@ mod test { use crate::api::r0::sync::sync_events::Response as SyncResponse; use crate::identifiers::{RoomId, UserId}; - use crate::{BaseClient, Session}; + use crate::{BaseClient, BaseClientConfig, Session}; fn sync_response(file: &str) -> SyncResponse { let mut file = File::open(file).unwrap(); @@ -360,7 +360,8 @@ mod test { // a sync response to populate our JSON store let store = Box::new(JsonStore::open(path).unwrap()); - let client = BaseClient::new_with_state_store(store).unwrap(); + let client = + BaseClient::new_with_config(BaseClientConfig::new().state_store(store)).unwrap(); client.restore_login(session.clone()).await.unwrap(); let mut response = sync_response("../test_data/sync.json"); @@ -370,9 +371,9 @@ mod test { // now syncing the client will update from the state store let store = Box::new(JsonStore::open(path).unwrap()); - let client = BaseClient::new_with_state_store(store).unwrap(); + let client = + BaseClient::new_with_config(BaseClientConfig::new().state_store(store)).unwrap(); client.restore_login(session.clone()).await.unwrap(); - client.sync_with_state_store().await.unwrap(); // assert the synced client and the logged in client are equal assert_eq!(*client.session().read().await, Some(session)); diff --git a/matrix_sdk_common/Cargo.toml b/matrix_sdk_common/Cargo.toml index ea7fdd6b..cb34e411 100644 --- a/matrix_sdk_common/Cargo.toml +++ b/matrix_sdk_common/Cargo.toml @@ -13,7 +13,7 @@ version = "0.1.0" [dependencies] js_int = "0.1.5" ruma-api = "0.16.1" -ruma-client-api = "0.8.0" +ruma-client-api = "0.9.0" ruma-events = "0.21.2" ruma-identifiers = "0.16.1" instant = { version = "0.1.4", features = ["wasm-bindgen", "now"] } diff --git a/matrix_sdk_crypto/Cargo.toml b/matrix_sdk_crypto/Cargo.toml index 64a432a4..fe154516 100644 --- a/matrix_sdk_crypto/Cargo.toml +++ b/matrix_sdk_crypto/Cargo.toml @@ -27,10 +27,10 @@ zeroize = { version = "1.1.0", features = ["zeroize_derive"] } url = "2.1.1" # Misc dependencies -thiserror = "1.0.18" +thiserror = "1.0.19" tracing = "0.1.14" atomic = "0.4.5" -dashmap = "3.11.1" +dashmap = "3.11.2" [dependencies.tracing-futures] version = "0.2.4" diff --git a/matrix_sdk_crypto/src/machine.rs b/matrix_sdk_crypto/src/machine.rs index fe90d175..38069017 100644 --- a/matrix_sdk_crypto/src/machine.rs +++ b/matrix_sdk_crypto/src/machine.rs @@ -141,9 +141,9 @@ impl OlmMachine { /// * `store` - A `Cryptostore` implementation that will be used to store /// the encryption keys. pub async fn new_with_store( - user_id: &UserId, - device_id: &str, - mut store: impl CryptoStore + 'static, + user_id: UserId, + device_id: String, + mut store: Box, ) -> StoreError { let account = match store.load_account().await? { Some(a) => { @@ -161,7 +161,7 @@ impl OlmMachine { device_id: device_id.to_owned(), account, uploaded_signed_key_count: None, - store: Box::new(store), + store, outbound_group_sessions: HashMap::new(), }) } @@ -181,12 +181,12 @@ impl OlmMachine { user_id: &UserId, device_id: &str, path: P, - passphrase: String, + passphrase: &str, ) -> StoreError { let store = SqliteStore::open_with_passphrase(&user_id, device_id, path, passphrase).await?; - OlmMachine::new_with_store(user_id, device_id, store).await + OlmMachine::new_with_store(user_id.to_owned(), device_id.to_owned(), Box::new(store)).await } /// The unique user id that owns this identity. diff --git a/matrix_sdk_crypto/src/store/sqlite.rs b/matrix_sdk_crypto/src/store/sqlite.rs index bd6d1ff7..f9b2aad1 100644 --- a/matrix_sdk_crypto/src/store/sqlite.rs +++ b/matrix_sdk_crypto/src/store/sqlite.rs @@ -91,9 +91,15 @@ impl SqliteStore { user_id: &UserId, device_id: &str, path: P, - passphrase: String, + passphrase: &str, ) -> Result { - SqliteStore::open_helper(user_id, device_id, path, Some(Zeroizing::new(passphrase))).await + SqliteStore::open_helper( + user_id, + device_id, + path, + Some(Zeroizing::new(passphrase.to_owned())), + ) + .await } fn path_to_url(path: &Path) -> Result { @@ -801,14 +807,9 @@ mod test { let user_id = &UserId::try_from(USER_ID).unwrap(); let store = if let Some(passphrase) = passphrase { - SqliteStore::open_with_passphrase( - &user_id, - DEVICE_ID, - tmpdir_path, - passphrase.to_owned(), - ) - .await - .expect("Can't create a passphrase protected store") + SqliteStore::open_with_passphrase(&user_id, DEVICE_ID, tmpdir_path, passphrase) + .await + .expect("Can't create a passphrase protected store") } else { SqliteStore::open(&user_id, DEVICE_ID, tmpdir_path) .await