diff --git a/.github/workflows/appservice.yml b/.github/workflows/appservice.yml index 7b17444a973..de970ca629c 100644 --- a/.github/workflows/appservice.yml +++ b/.github/workflows/appservice.yml @@ -5,12 +5,18 @@ on: branches: [main] pull_request: branches: [main] + types: + - opened + - reopened + - synchronize + - ready_for_review env: CARGO_TERM_COLOR: always jobs: test-appservice: + if: github.event_name == 'push' || !github.event.pull_request.draft name: ${{ matrix.name }} runs-on: ${{ matrix.os || 'ubuntu-latest' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c68c3d299c..bc83da2bb53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,16 @@ name: CI on: + workflow_dispatch: push: branches: [main] pull_request: branches: [main] + types: + - opened + - reopened + - synchronize + - ready_for_review env: CARGO_TERM_COLOR: always @@ -13,6 +19,7 @@ jobs: style: name: Check style runs-on: ubuntu-latest + if: github.event_name == 'push' || !github.event.pull_request.draft steps: - name: Checkout the repo @@ -36,6 +43,7 @@ jobs: name: Spell Check with Typos needs: [style] runs-on: ubuntu-latest + if: github.event_name == 'push' || !github.event.pull_request.draft steps: - name: Checkout Actions Repository @@ -48,6 +56,7 @@ jobs: name: Run clippy needs: [style] runs-on: ubuntu-latest + if: github.event_name == 'push' || !github.event.pull_request.draft steps: - name: Checkout the repo @@ -72,6 +81,7 @@ jobs: test-features: name: ${{ matrix.name }} + if: github.event_name == 'push' || !github.event.pull_request.draft runs-on: ${{ matrix.os || 'ubuntu-latest' }} strategy: @@ -141,6 +151,7 @@ jobs: test: name: ${{ matrix.name }} + if: github.event_name == 'push' || !github.event.pull_request.draft runs-on: ${{ matrix.os || 'ubuntu-latest' }} strategy: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b33a94df51e..c8731a22808 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,6 +9,7 @@ jobs: docs: name: Docs runs-on: ubuntu-latest + if: github.event_name == 'push' || !github.event.pull_request.draft steps: - name: Checkout repository diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index d6f54327f3e..0887c3eb699 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -5,6 +5,11 @@ on: branches: [main] pull_request: branches: [main] + types: + - opened + - reopened + - synchronize + - ready_for_review env: CARGO_TERM_COLOR: always @@ -13,10 +18,10 @@ jobs: check-wasm: name: Build test runs-on: ubuntu-latest + if: github.event_name == 'push' || !github.event.pull_request.draft strategy: -# fail-fast: true - + fail-fast: true matrix: name: - matrix-qrcode @@ -24,14 +29,13 @@ jobs: - matrix-sdk-base - matrix-sdk-common - matrix-sdk-crypto - - matrix-sdk-base / indexeddb_state_store - - matrix-sdk-crypto / indexeddb_cryptostore + - matrix-sdk / indexeddb_stores include: - name: matrix-qrcode cargo_args: --package matrix-qrcode - name: matrix-sdk (no-default, wasm-flags) - cargo_args: --no-default-features --features qrcode,encryption,indexeddb_stores,rustls-tls --package matrix-sdk + cargo_args: --no-default-features --features qrcode,encryption,indexeddb_stores,rustls-tls --package matrix-sdk --lib - name: matrix-sdk-base cargo_args: --package matrix-sdk-base - name: matrix-sdk-common @@ -40,10 +44,8 @@ jobs: cargo_args: --package matrix-sdk-crypto # special check for specific features - - name: matrix-sdk-base / indexeddb_state_store - cargo_args: --package matrix-sdk-base --features indexeddb_state_store - - name: matrix-sdk-crypto / indexeddb_cryptostore - cargo_args: --package matrix-sdk-crypto --features indexeddb_cryptostore + - name: matrix-sdk / indexeddb_stores + cargo_args: --package matrix-sdk --no-default-features --features indexeddb_stores,encryption,rustls-tls --lib steps: - name: Checkout the repo @@ -59,7 +61,6 @@ jobs: - name: Load cache uses: Swatinem/rust-cache@v1 - # needed for libolm-sys compilation - name: Install emscripten uses: mymindstorm/setup-emsdk@v11 @@ -76,26 +77,26 @@ jobs: # building wasm is not enough, we've seen runtime errors before, # hence the tests name: ${{ matrix.name }} test + if: github.event_name == 'push' || !github.event.pull_request.draft runs-on: ubuntu-latest continue-on-error: ${{ matrix.experimental }} strategy: matrix: include: - - name: "matrix-sdk-crypto @Node14 emcc2.0" - experimental: false - emcc_version: 2.0.27 - base_dir: matrix-sdk-crypto - cargo_args: --features indexeddb_cryptostore - - name: matrix-sdk-base - experimental: false - base_dir: matrix-sdk-base - cargo_args: --features indexeddb_state_store,indexeddb_cryptostore - name: matrix-sdk experimental: false base_dir: matrix-sdk - # FIXME: we have to skip all examples b/c of https://github.com/rustwasm/wasm-pack/issues/666 - cargo_args: --no-default-features --features indexeddb_stores,rustls-tls --lib + cargo_args: --no-default-features --features indexeddb_stores,rustls-tls --lib + - name: matrix-sdk-encryption + experimental: false + emcc_version: 2.0.27 + node_version: '14' + base_dir: matrix-sdk + cargo_args: --no-default-features --features indexeddb_stores,rustls-tls,encryption --lib + - name: "matrix-sdk-indexeddb" + base_dir: matrix-sdk-indexeddb + experimental: false - name: "matrix-sdk-example-wasm_command_bot @Node14" experimental: false emcc_version: 2.0.27 @@ -106,11 +107,6 @@ jobs: npm install npm test wasm-pack test --firefox --headless - - # these are _known_ broken on latest node - # - name: matrix-sdk-crypto - # experimental: true - # base_dir: matrix-sdk-crypto - name: matrix-sdk-example-wasm_command_bot base_dir: matrix-sdk/examples/wasm_command_bot # this might fail @@ -160,14 +156,14 @@ jobs: emcc -v - name: Default wasm-pack tests - if: ${{!matrix.cmd}} + if: ${{ !matrix.cmd }} run: | cd crates/${{matrix.base_dir}} wasm-pack test --node -- ${{ matrix.cargo_args }} - wasm-pack test --firefox --headless -- --features ${{ matrix.cargo_args }} + wasm-pack test --firefox --headless -- ${{ matrix.cargo_args }} - name: Testing with custom command - if: ${{matrix.cmd}} + if: ${{ matrix.cmd }} run: | cd crates/${{matrix.base_dir}} ${{matrix.cmd}} diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index aa0d4288da3..ed7ed3552bf 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -17,21 +17,19 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] -encryption = ["matrix-sdk-crypto"] +encryption = ["matrix-sdk-crypto", "store_key"] qrcode = ["matrix-sdk-crypto/qrcode"] -sled_state_store = [ - "sled", - "tokio", + +# Store Key is a helper feature for state store implementations +store_key = [ "pbkdf2", "hmac", "sha2", "rand", "chacha20poly1305", ] -sled_cryptostore = ["matrix-sdk-crypto/sled_cryptostore"] - -indexeddb_state_store = ["indexed_db_futures", "wasm-bindgen", "pbkdf2", "hmac", "sha2", "rand", "chacha20poly1305"] -indexeddb_cryptostore = ["matrix-sdk-crypto/indexeddb_cryptostore"] +# helpers for testing features build upon this +testing = [ "http" ] [dependencies] async-stream = "0.3.2" @@ -50,38 +48,17 @@ ruma = { version = "0.5.0", features = ["client-api-c", "signatures", "unstable- serde = { version = "1.0.126", features = ["rc"] } serde_json = "1.0.64" sha2 = { version = "0.10.1", optional = true } -sled = { version = "0.34.6", optional = true } thiserror = "1.0.25" tracing = "0.1.26" zeroize = { version = "1.3.0", features = ["zeroize_derive"] } - -## Feature sled_state_store -tokio = { version = "1.7.1", optional = true, default-features = false, features = ["sync", "fs"] } - -## Feature indexeddb-state-store -indexed_db_futures = { version = "0.2.0", optional = true } -wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true } +anyhow = "1" +http = { version = "0.2.4", optional = true } [dev-dependencies] futures = { version = "0.3.15", default-features = false, features = ["executor"] } http = "0.2.4" matrix-sdk-test = { version = "0.4.0", path = "../matrix-sdk-test" } - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -atty = "0.2.14" -clap = "3.1.0" -rustyline = "9.0.0" -rustyline-derive = "0.6.0" -syntect = "4.5.0" tokio = { version = "1.7.1", default-features = false, features = [ "rt-multi-thread", "macros", ] } -tempfile = "3.2.0" - -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.24" - -[[example]] -name = "state_inspector" -required-features = ["sled_state_store"] diff --git a/crates/matrix-sdk-base/README.md b/crates/matrix-sdk-base/README.md index f266252df8f..e5c1ecf4f1b 100644 --- a/crates/matrix-sdk-base/README.md +++ b/crates/matrix-sdk-base/README.md @@ -6,9 +6,6 @@ library. The following crate feature flags are available: * `encryption`: Enables end-to-end encryption support in the library. -* `sled_cryptostore`: Enables a Sled based store for the encryption - keys. If this is disabled and `encryption` support is enabled the keys will - by default be stored only in memory and thus lost after the client is - destroyed. -* `sled_state_store`: Enables a Sled based store for the storage of the local - client state. +* `qrcode`: Enbles QRcode generation and reading code +* `store_key`: Provides extra facilities for StateStores to manage store keys +* `testing`: provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider. \ No newline at end of file diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index be25ac6dbbd..e34e3deb1a3 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -20,7 +20,6 @@ use std::{ collections::{BTreeMap, BTreeSet}, convert::TryFrom, fmt, - path::{Path, PathBuf}, result::Result as StdResult, sync::Arc, }; @@ -38,7 +37,7 @@ use matrix_sdk_common::{ }; #[cfg(feature = "encryption")] use matrix_sdk_crypto::{ - store::{CryptoStore, CryptoStoreError}, + store::{CryptoStore, CryptoStoreError, MemoryStore as MemoryCryptoStore}, Device, EncryptionSettings, IncomingResponse, MegolmError, OlmError, OlmMachine, OutgoingRequest, ToDeviceRequest, UserDevices, }; @@ -63,13 +62,16 @@ use ruma::{ RoomId, UInt, UserId, }; use tracing::{info, trace, warn}; -use zeroize::Zeroizing; +#[cfg(feature = "encryption")] +use crate::error::Error; use crate::{ error::Result, rooms::{Room, RoomInfo, RoomType}, session::Session, - store::{ambiguity_map::AmbiguityCache, Result as StoreResult, StateChanges, Store}, + store::{ + ambiguity_map::AmbiguityCache, Result as StoreResult, StateChanges, StateStore, Store, + }, }; pub type Token = String; @@ -87,14 +89,8 @@ pub struct BaseClient { pub(crate) sync_token: Arc>>, /// Database store: Store, - #[allow(dead_code)] - store_path: Option, #[cfg(feature = "encryption")] - olm: Arc>>, - #[cfg(feature = "encryption")] - cryptostore: Arc>>>, - #[allow(dead_code)] - store_passphrase: Arc>>, + olm: Arc>, } #[cfg(not(tarpaulin_include))] @@ -114,18 +110,13 @@ impl fmt::Debug for BaseClient { /// ``` /// # use matrix_sdk_base::BaseClientConfig; /// -/// let client_config = BaseClientConfig::new() -/// .store_path("/home/example/matrix-sdk-client") -/// .passphrase("test-passphrase".to_owned()); +/// let client_config = BaseClientConfig::new(); /// ``` #[derive(Default)] pub struct BaseClientConfig { #[cfg(feature = "encryption")] crypto_store: Option>, - #[cfg(feature = "indexeddb_state_store")] - name: String, - store_path: Option, - passphrase: Option>, + state_store: Option>, } #[cfg(not(tarpaulin_include))] @@ -146,159 +137,62 @@ impl BaseClientConfig { /// /// The crypto store should be opened before being set. #[cfg(feature = "encryption")] - #[must_use] pub fn crypto_store(mut self, store: Box) -> Self { self.crypto_store = Some(store); self } - /// Set the indexeddb database name for storage. - /// - /// # Arguments - /// - /// * `name` - The name where the stores should save data in. Indexeddb - //// separates database through these nanmes - #[cfg(feature = "indexeddb_state_store")] - pub fn name(mut self, name: String) -> Self { - self.name = name; + /// Set a custom implementation of a `StateStore`. + pub fn state_store(mut self, store: Box) -> Self { + self.state_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 - #[must_use] - pub fn store_path>(mut self, path: P) -> Self { - self.store_path = Some(path.as_ref().into()); - self - } +#[cfg(feature = "encryption")] +enum CryptoHolder { + PreSetupStore(Option>), + Olm(Box), +} - /// 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. - #[must_use] - pub fn passphrase(mut self, passphrase: String) -> Self { - self.passphrase = Some(Zeroizing::new(passphrase)); - self +#[cfg(feature = "encryption")] +impl Default for CryptoHolder { + fn default() -> Self { + CryptoHolder::PreSetupStore(Some(Box::new(MemoryCryptoStore::default()))) } } -#[cfg(feature = "sled_state_store")] -impl BaseClient { - /// Create a new client. - /// - /// # Arguments - /// - /// * `config` - An optional session if the user already has one from a - /// previous login call. - pub async fn new_with_config(config: BaseClientConfig) -> Result { - #[cfg_attr(not(feature = "sled_cryptostore"), allow(unused_variables))] - let config = config; - - #[cfg(feature = "sled_state_store")] - let stores = if let Some(path) = &config.store_path { - if config.passphrase.is_some() { - info!("Opening an encrypted store in path {}", path.display()); - } else { - info!("Opening store in path {}", path.display()); - } - Store::open_default(path, config.passphrase.as_deref().map(|p| p.as_str()))? - } else { - Store::open_temporary()? - }; - - #[cfg(feature = "indexeddb_state_store")] - let store = Store::open_default( - config.name.clone(), - config.passphrase.as_deref().map(|p| p.as_str()), - ) - .await?; - - #[cfg(not(feature = "sled_state_store"))] - let stores = Store::open_memory_store(); - - #[cfg(all(feature = "encryption", feature = "sled_state_store"))] - let crypto_store = if config.crypto_store.is_none() { - #[cfg(feature = "sled_cryptostore")] - let store: Option> = Some(Box::new( - matrix_sdk_crypto::store::SledStore::open_with_database( - stores.1, - config.passphrase.as_deref().map(|p| p.as_str()), +#[cfg(feature = "encryption")] +impl CryptoHolder { + fn new(store: Box) -> Self { + CryptoHolder::PreSetupStore(Some(store)) + } + async fn convert_to_olm(&mut self, session: &Session) -> Result<()> { + if let CryptoHolder::PreSetupStore(store) = self { + *self = CryptoHolder::Olm(Box::new( + OlmMachine::new_with_store( + session.user_id.to_owned(), + session.device_id.as_str().into(), + store.take().expect("We always exist"), ) - .map_err(OlmError::Store)?, + .await + .map_err(OlmError::from)?, )); - #[cfg(not(feature = "sled_cryptostore"))] - let store = config.crypto_store; - - store + Ok(()) } else { - config.crypto_store - }; - #[cfg(all(not(feature = "sled_state_store"), feature = "encryption"))] - let crypto_store = config.crypto_store; - - #[cfg(feature = "sled_state_store")] - let store = stores.0; - #[cfg(not(feature = "sled_state_store"))] - let store = stores; - - Ok(BaseClient { - session: store.session.clone(), - sync_token: store.sync_token.clone(), - store_path: config.store_path, - store, - #[cfg(feature = "encryption")] - olm: Mutex::new(None).into(), - #[cfg(feature = "encryption")] - cryptostore: Mutex::new(crypto_store).into(), - store_passphrase: config.passphrase.into(), - }) + Err(Error::BadCryptoStoreState) + } } -} - -#[cfg(feature = "indexeddb_state_store")] -impl BaseClient { - /// Create a new client. - /// - /// # Arguments - /// - /// * `config` - An optional session if the user already has one from a - /// previous login call. - pub async fn new_with_config(config: BaseClientConfig) -> Result { - let store = Store::open_default( - config.name.clone(), - config.passphrase.as_deref().map(|p| p.as_str()), - ) - .await?; - Ok(BaseClient { - session: store.session.clone(), - sync_token: store.sync_token.clone(), - store_path: config.store_path, - store, - #[cfg(feature = "encryption")] - olm: Mutex::new(None).into(), - #[cfg(feature = "encryption")] - cryptostore: Mutex::new(config.crypto_store).into(), - store_passphrase: config.passphrase.into(), - }) + fn machine(&self) -> Option { + if let CryptoHolder::Olm(m) = self { + Some(*m.clone()) + } else { + None + } } } -#[cfg(not(any(feature = "sled_state_store", feature = "indexeddb_state_store")))] impl BaseClient { /// Create a new client. /// @@ -307,18 +201,16 @@ impl BaseClient { /// * `config` - An optional session if the user already has one from a /// previous login call. pub async fn new_with_config(config: BaseClientConfig) -> Result { - let store = Store::open_memory_store(); + let store = config.state_store.map(Store::new).unwrap_or_else(Store::open_memory_store); + #[cfg(feature = "encryption")] + let holder = config.crypto_store.map(CryptoHolder::new).unwrap_or_default(); Ok(BaseClient { session: store.session.clone(), sync_token: store.sync_token.clone(), - store_path: config.store_path, store, #[cfg(feature = "encryption")] - olm: Mutex::new(None).into(), - #[cfg(feature = "encryption")] - cryptostore: Mutex::new(config.crypto_store).into(), - store_passphrase: config.passphrase.into(), + olm: Mutex::new(holder).into(), }) } } @@ -377,42 +269,7 @@ impl BaseClient { #[cfg(feature = "encryption")] { let mut olm = self.olm.lock().await; - 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.as_str().into(), - store, - ) - .await - .map_err(OlmError::from)?, - ); - } else { - #[cfg(feature = "sled_cryptostore")] - { - if let Some(path) = self.store_path.as_ref() { - *olm = Some( - OlmMachine::new_with_default_store( - &session.user_id, - &session.device_id, - path, - self.store_passphrase.as_deref().map(|p| p.as_str()), - ) - .await - .map_err(OlmError::from)?, - ); - } else { - *olm = Some(OlmMachine::new(&session.user_id, &session.device_id)); - } - } - - #[cfg(not(feature = "sled_cryptostore"))] - { - *olm = Some(OlmMachine::new(&session.user_id, &session.device_id)); - } - } + olm.convert_to_olm(&session).await?; } *self.session.write().await = Some(session); @@ -753,9 +610,7 @@ impl BaseClient { #[cfg(feature = "encryption")] let to_device = { - let olm = self.olm.lock().await; - - if let Some(o) = &*olm { + if let Some(o) = self.olm_machine().await { // Let the crypto machine handle the sync response, this // decrypts to-device events, but leaves room events alone. // This makes sure that we have the decryption keys for the room @@ -1123,9 +978,7 @@ impl BaseClient { /// [`mark_request_as_sent`]: #method.mark_request_as_sent #[cfg(feature = "encryption")] pub async fn outgoing_requests(&self) -> Result, CryptoStoreError> { - let olm = self.olm.lock().await; - - match &*olm { + match self.olm_machine().await { Some(o) => o.outgoing_requests().await, None => Ok(vec![]), } @@ -1146,9 +999,7 @@ impl BaseClient { request_id: &TransactionId, response: impl Into>, ) -> Result<()> { - let olm = self.olm.lock().await; - - match &*olm { + match self.olm_machine().await { Some(o) => Ok(o.mark_request_as_sent(request_id, response).await?), None => Ok(()), } @@ -1162,9 +1013,7 @@ impl BaseClient { &self, users: impl Iterator, ) -> Result, KeysClaimRequest)>> { - let olm = self.olm.lock().await; - - match &*olm { + match self.olm_machine().await { Some(o) => Ok(o.get_missing_sessions(users).await?), None => Ok(None), } @@ -1173,9 +1022,7 @@ impl BaseClient { /// Get a to-device request that will share a group session for a room. #[cfg(feature = "encryption")] pub async fn share_group_session(&self, room_id: &RoomId) -> Result>> { - let olm = self.olm.lock().await; - - match &*olm { + match self.olm_machine().await { Some(o) => { let (history_visibility, settings) = self .get_room(room_id) @@ -1218,9 +1065,7 @@ impl BaseClient { room_id: &RoomId, content: impl MessageEventContent, ) -> Result { - let olm = self.olm.lock().await; - - match &*olm { + match self.olm_machine().await { Some(o) => Ok(o.encrypt(room_id, content).await?), None => panic!("Olm machine wasn't started"), } @@ -1236,9 +1081,7 @@ impl BaseClient { &self, room_id: &RoomId, ) -> Result { - let olm = self.olm.lock().await; - - match &*olm { + match self.olm_machine().await { Some(o) => o.invalidate_group_session(room_id).await, None => Ok(false), } @@ -1278,9 +1121,7 @@ impl BaseClient { user_id: &UserId, device_id: &DeviceId, ) -> StdResult, CryptoStoreError> { - let olm = self.olm.lock().await; - - if let Some(olm) = olm.as_ref() { + if let Some(olm) = self.olm_machine().await { olm.get_device(user_id, device_id).await } else { Ok(None) @@ -1335,9 +1176,7 @@ impl BaseClient { &self, user_id: &UserId, ) -> StdResult { - let olm = self.olm.lock().await; - - if let Some(olm) = olm.as_ref() { + if let Some(olm) = self.olm_machine().await { Ok(olm.get_user_devices(user_id).await?) } else { // TODO remove this panic. @@ -1349,7 +1188,7 @@ impl BaseClient { #[cfg(feature = "encryption")] pub async fn olm_machine(&self) -> Option { let olm = self.olm.lock().await; - olm.as_ref().cloned() + olm.machine() } /// Get the push rules. diff --git a/crates/matrix-sdk-base/src/error.rs b/crates/matrix-sdk-base/src/error.rs index 081e89901ac..882441096b8 100644 --- a/crates/matrix-sdk-base/src/error.rs +++ b/crates/matrix-sdk-base/src/error.rs @@ -33,6 +33,12 @@ pub enum Error { #[error("the queried endpoint requires authentication but was called before logging in")] AuthenticationRequired, + /// Attempting to restore a session after the olm-machine has already been + /// set up fails + #[cfg(feature = "encryption")] + #[error("The olm machine has already been initialized")] + BadCryptoStoreState, + /// A generic error returned when the state store fails not due to /// IO or (de)serialization. #[error(transparent)] diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 5eacb5e7f49..b8d6147e75a 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -15,9 +15,9 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![warn(missing_docs)] #![deny( missing_debug_implementations, - missing_docs, trivial_casts, trivial_numeric_casts, unused_extern_crates, @@ -25,15 +25,6 @@ unused_qualifications )] -#[cfg(all(feature = "sled_state_store", feature = "indexeddb_state_store"))] -compile_error!("sled_state_store and indexeddb_state_store are mutually exclusive and cannot be enabled together"); - -#[cfg(all(feature = "indexeddb_state_store", not(target_arch = "wasm32")))] -compile_error!("indexeddb_state_store only works for wasm32 target"); - -#[cfg(all(feature = "sled_cryptostore", feature = "indexeddb_state_store"))] -compile_error!("sled_cryptostore and indexeddb_state_store are mutually exclusive and cannot be enabled together"); - pub use matrix_sdk_common::*; pub use crate::{ @@ -47,10 +38,12 @@ mod error; pub mod media; mod rooms; mod session; -mod store; +pub mod store; mod timeline_stream; pub use client::{BaseClient, BaseClientConfig}; +#[cfg(any(test, feature = "testing"))] +pub use http; #[cfg(feature = "encryption")] pub use matrix_sdk_crypto as crypto; pub use rooms::{Room, RoomInfo, RoomMember, RoomType}; diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 25b66d5fa5c..fbd7caabb1f 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -375,11 +375,13 @@ impl Room { Ok(inner.base_info.calculate_room_name(joined, invited, members)) } - pub(crate) fn clone_info(&self) -> RoomInfo { + /// Clone the inner RoomInfo + pub fn clone_info(&self) -> RoomInfo { (*self.inner.read().unwrap()).clone() } - pub(crate) fn update_summary(&self, summary: RoomInfo) { + /// Update the summary with given RoomInfo + pub fn update_summary(&self, summary: RoomInfo) { let mut inner = self.inner.write().unwrap(); *inner = summary; } @@ -611,23 +613,30 @@ pub struct RoomInfo { } impl RoomInfo { - pub(crate) fn mark_as_joined(&mut self) { + /// Mark this Room as joined + pub fn mark_as_joined(&mut self) { self.room_type = RoomType::Joined; } - pub(crate) fn mark_as_left(&mut self) { + /// Mark this Room as left + pub fn mark_as_left(&mut self) { self.room_type = RoomType::Left; } - pub(crate) fn mark_members_synced(&mut self) { + /// Mark this Room as having all the members synced + pub fn mark_members_synced(&mut self) { self.members_synced = true; } - pub(crate) fn mark_members_missing(&mut self) { + /// Mark this Room still missing member information + pub fn mark_members_missing(&mut self) { self.members_synced = false; } - pub(crate) fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool { + /// Set the `prev_batch`-token. + /// Returns whether the token has differed and thus has been upgraded: + /// `false` means no update was applied as the were the same + pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool { if self.last_prev_batch.as_deref() != prev_batch { self.last_prev_batch = prev_batch.map(|p| p.to_string()); true @@ -636,22 +645,27 @@ impl RoomInfo { } } - pub(crate) fn is_encrypted(&self) -> bool { + /// Whether this is an encrypted Room + pub fn is_encrypted(&self) -> bool { self.base_info.encryption.is_some() } - pub(crate) fn handle_state_event(&mut self, event: &AnyStateEventContent) -> bool { + /// handle the given State event. + /// + /// Returns true if the event modified the info, false otherwise. + pub fn handle_state_event(&mut self, event: &AnyStateEventContent) -> bool { self.base_info.handle_state_event(event) } - pub(crate) fn update_notification_count( - &mut self, - notification_counts: UnreadNotificationsCount, - ) { + /// Update the notifications count + pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) { self.notification_counts = notification_counts; } - pub(crate) fn update_summary(&mut self, summary: &RumaSummary) -> bool { + /// Update the RoomSummary + /// + /// Returns true if the Summary modified the info, false otherwise. + pub fn update_summary(&mut self, summary: &RumaSummary) -> bool { let mut changed = false; if !summary.is_empty() { diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 72d1a180ec9..4ae8a1b970f 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -1,14 +1,41 @@ -#[allow(unused_macros)] - +//! Macro of integration tests for StateStore implementions. + +/// Macro building to allow your StateStore implementation to run the entire +/// tests suite locally. +/// +/// You need to provide a `async fn get_store() -> StoreResult` +/// providing a fresh store on the same level you invoke the macro. +/// +/// ## Usage Example: +/// ```no_run +/// # use matrix_sdk_base::store::{ +/// # StateStore, +/// # MemoryStore as MyStore, +/// # Result as StoreResult, +/// # }; +/// +/// #[cfg(test)] +/// mod test { +/// +/// use super::{MyStore, StoreResult, StateStore}; +/// +/// async fn get_store() -> StoreResult { +/// Ok(MyStore::new()) +/// } +/// +/// statestore_integration_tests! { integration } +/// } +/// ``` +#[allow(unused_macros, unused_extern_crates)] +#[macro_export] macro_rules! statestore_integration_tests { ($($name:ident)*) => { $( mod $name { use futures_util::StreamExt; - use http::Response; use matrix_sdk_test::{async_test, test_json}; - use ruma::{ + use matrix_sdk_common::ruma::{ api::{ client::{ media::get_content_thumbnail::v3::Method, @@ -38,14 +65,15 @@ macro_rules! statestore_integration_tests { use std::collections::{BTreeMap, BTreeSet}; - use crate::{ + use $crate::{ + http::Response, RoomType, Session, deserialized_responses::{MemberEvent, StrippedMemberEvent, RoomEvent, SyncRoomEvent, TimelineSlice}, media::{MediaFormat, MediaRequest, MediaThumbnailSize, MediaType}, store::{ Store, StateStore, - Result, + Result as StoreResult, StateChanges } }; @@ -73,7 +101,7 @@ macro_rules! statestore_integration_tests { } /// Populate the given `StateStore`. - pub(crate) async fn populated_store(inner: Box) -> Result { + pub(crate) async fn populated_store(inner: Box) -> StoreResult { let mut changes = StateChanges::default(); let store = Store::new(inner); @@ -240,7 +268,7 @@ macro_rules! statestore_integration_tests { } #[async_test] - async fn test_populate_store() -> Result<()> { + async fn test_populate_store() -> StoreResult<()> { let room_id = room_id(); let user_id = user_id(); let inner_store = get_store().await?; @@ -458,7 +486,7 @@ macro_rules! statestore_integration_tests { } #[async_test] - async fn test_custom_storage() -> Result<()> { + async fn test_custom_storage() -> StoreResult<()> { let key = "my_key"; let value = &[0, 1, 2, 3]; let store = get_store().await?; @@ -473,7 +501,7 @@ macro_rules! statestore_integration_tests { } #[async_test] - async fn test_persist_invited_room() -> Result<()> { + async fn test_persist_invited_room() -> StoreResult<()> { let stripped_room_id = stripped_room_id(); let inner_store = get_store().await?; let store = populated_store(Box::new(inner_store)).await?; @@ -486,7 +514,7 @@ macro_rules! statestore_integration_tests { } #[async_test] - async fn test_room_removal() -> Result<()> { + async fn test_room_removal() -> StoreResult<()> { let room_id = room_id(); let user_id = user_id(); let inner_store = get_store().await?; @@ -668,7 +696,7 @@ macro_rules! statestore_integration_tests { assert_eq!(end_token.as_deref(), expected_end_token); - let timeline = timeline_iter.collect::>>().await; + let timeline = timeline_iter.collect::>>().await; assert!(timeline .into_iter() diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 1121ec60088..907f2d487d5 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -45,6 +45,9 @@ use crate::{ StoreError, }; +/// In-Memory, non-persistent implementation of the `StateStore` +/// +/// Default if no other is configured at startup. #[allow(clippy::type_complexity)] #[derive(Debug, Clone)] pub struct MemoryStore { @@ -74,8 +77,15 @@ pub struct MemoryStore { room_timeline: Arc, TimelineData>>, } +impl Default for MemoryStore { + fn default() -> Self { + Self::new() + } +} + impl MemoryStore { #[allow(dead_code)] + /// Create a new empty MemoryStore pub fn new() -> Self { Self { sync_token: Default::default(), diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 7ff465767b7..04fea6ad59f 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -12,8 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(feature = "sled_state_store")] -use std::path::Path; +//! The state store holds the overall state for rooms, users and their +//! profiles and their timelines. It is an overall cache for faster access +//! and convenience- accessible through `Store`. +//! +//! Implementing the `StateStore` trait, you can plug any storage backend +//! into the store for the actual storage. By default this brings an in-memory +//! store. + use std::{ collections::{BTreeMap, BTreeSet}, ops::Deref, @@ -21,7 +27,7 @@ use std::{ sync::Arc, }; -#[cfg(test)] +#[cfg(any(test, feature = "testing"))] #[macro_use] pub mod integration_tests; @@ -41,16 +47,11 @@ use ruma::{ EventId, MxcUri, RoomId, UserId, }; -pub type BoxStream = Pin + Send>>; - -#[cfg(any(feature = "sled_state_store", feature = "indexeddb_state_store"))] -mod store_key; - -#[cfg(feature = "sled_state_store")] -use sled::Db; +#[cfg(feature = "store_key")] +pub mod store_key; -#[cfg(feature = "indexeddb_state_store")] -mod indexeddb_store; +/// BoxStream of owned Types +pub type BoxStream = Pin + Send>>; use crate::{ deserialized_responses::{MemberEvent, StrippedMemberEvent, SyncRoomEvent, TimelineSlice}, @@ -61,34 +62,15 @@ use crate::{ pub(crate) mod ambiguity_map; mod memory_store; -#[cfg(feature = "sled_state_store")] -mod sled_store; -#[cfg(feature = "indexeddb_state_store")] -use self::indexeddb_store::IndexeddbStore; -#[cfg(not(any(feature = "sled_state_store", feature = "indexeddb_state_store")))] -use self::memory_store::MemoryStore; -#[cfg(feature = "sled_state_store")] -use self::sled_store::SledStore; +pub use self::memory_store::MemoryStore; /// State store specific error type. #[derive(Debug, thiserror::Error)] pub enum StoreError { - /// An error happened in the underlying sled database. - #[cfg(feature = "sled_state_store")] #[error(transparent)] - Sled(#[from] sled::Error), - /// An error happened in the underlying Indexed Database. - #[cfg(feature = "indexeddb_state_store")] - #[error("IndexDB error: {name} ({code}): {message}")] - Indexeddb { - /// DomException code - code: u16, - /// Specific name of the DomException - name: String, - /// Message given to the DomException - message: String, - }, + /// An error happened in the underlying database backend. + Backend(#[from] anyhow::Error), /// An error happened while serializing or deserializing some data. #[error(transparent)] Json(#[from] serde_json::Error), @@ -109,24 +91,12 @@ pub enum StoreError { /// The store failed to encode or decode some data. #[error("Error encoding or decoding data from the store: {0}")] Codec(String), - /// An error happened while running a tokio task. - #[cfg(feature = "sled_state_store")] - #[error(transparent)] - Task(#[from] tokio::task::JoinError), /// Redacting an event in the store has failed. /// /// This should never happen. #[error("Redaction failed: {0}")] Redaction(#[source] ruma::signatures::Error), } - -#[cfg(feature = "indexeddb_state_store")] -impl From for StoreError { - fn from(frm: indexed_db_futures::web_sys::DomException) -> StoreError { - StoreError::Indexeddb { name: frm.name(), message: frm.message(), code: frm.code() } - } -} - /// A `StateStore` specific result type. pub type Result = std::result::Result; @@ -390,57 +360,9 @@ pub struct Store { stripped_rooms: Arc, Room>>, } -#[cfg(feature = "sled_state_store")] -impl Store { - /// Open the default Sled store. - /// - /// # Arguments - /// - /// * `path` - The path where the store should reside in. - /// - /// * `passphrase` - A passphrase that should be used to encrypt the state - /// store. - pub fn open_default(path: impl AsRef, passphrase: Option<&str>) -> Result<(Self, Db)> { - let inner = if let Some(passphrase) = passphrase { - SledStore::open_with_passphrase(path, passphrase)? - } else { - SledStore::open_with_path(path)? - }; - - Ok((Self::new(Box::new(inner.clone())), inner.inner)) - } - - pub(crate) fn open_temporary() -> Result<(Self, Db)> { - let inner = SledStore::open()?; - - Ok((Self::new(Box::new(inner.clone())), inner.inner)) - } -} - -#[cfg(feature = "indexeddb_state_store")] -impl Store { - /// Open the default IndexedDB store. - /// - /// # Arguments - /// - /// * `name` - The name of the store should reside in. - /// - /// * `passphrase` - A passphrase that should be used to encrypt the state - /// store. - pub async fn open_default(name: String, passphrase: Option<&str>) -> Result { - let inner = if let Some(passphrase) = passphrase { - IndexeddbStore::open_with_passphrase(name, passphrase).await? - } else { - IndexeddbStore::open_with_name(name).await? - }; - - Ok(Self::new(Box::new(inner))) - } -} - -#[cfg(not(any(feature = "sled_state_store", feature = "indexeddb_state_store")))] impl Store { - pub(crate) fn open_memory_store() -> Self { + /// Create a new Store with the default `MemoryStore` + pub fn open_memory_store() -> Self { let inner = Box::new(MemoryStore::new()); Self::new(inner) @@ -448,7 +370,8 @@ impl Store { } impl Store { - fn new(inner: Box) -> Self { + /// Create a new store, wrappning the given `StateStore` + pub fn new(inner: Box) -> Self { Self { inner: inner.into(), session: Default::default(), @@ -458,7 +381,9 @@ impl Store { } } - pub(crate) async fn restore_session(&self, session: Session) -> Result<()> { + /// Restore the access to the Store from the given `Session`, overwrites any + /// previously existing access to the Store. + pub async fn restore_session(&self, session: Session) -> Result<()> { for info in self.inner.get_room_infos().await? { let room = Room::restore(&session.user_id, self.inner.clone(), info); self.rooms.insert(room.room_id().to_owned(), room); @@ -504,7 +429,9 @@ impl Store { self.stripped_rooms.get(room_id).map(|r| r.clone()) } - pub(crate) async fn get_or_create_stripped_room(&self, room_id: &RoomId) -> Room { + /// Lookup the stripped Room for the given RoomId, or create one, if it + /// didn't exist yet in the store + pub async fn get_or_create_stripped_room(&self, room_id: &RoomId) -> Room { let session = self.session.read().await; let user_id = &session.as_ref().expect("Creating room while not being logged in").user_id; @@ -514,7 +441,9 @@ impl Store { .clone() } - pub(crate) async fn get_or_create_room(&self, room_id: &RoomId, room_type: RoomType) -> Room { + /// Lookup the Room for the given RoomId, or create one, if it didn't exist + /// yet in the store + pub async fn get_or_create_room(&self, room_id: &RoomId, room_type: RoomType) -> Room { if room_type == RoomType::Invited { return self.get_or_create_stripped_room(room_id).await; } diff --git a/crates/matrix-sdk-base/src/store/store_key.rs b/crates/matrix-sdk-base/src/store/store_key.rs index cadb7f26298..54869854e30 100644 --- a/crates/matrix-sdk-base/src/store/store_key.rs +++ b/crates/matrix-sdk-base/src/store/store_key.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Facilities for StateStore implementations to reuse to manage encrypted +//! state keys + use std::convert::TryFrom; use chacha20poly1305::{ @@ -37,12 +40,19 @@ const KDF_ROUNDS: u32 = 200_000; #[cfg(test)] const KDF_ROUNDS: u32 = 1000; +/// Local State Error for Store Keys +/// +/// provides facilities to directly convert into a `StoreError` thus the most +/// common usage is to `.map_err(StoreError::into)` it. #[derive(Debug, thiserror::Error)] pub enum Error { + /// A problem de/serializing the JSON #[error(transparent)] Serialization(#[from] serde_json::Error), + /// A problem with en/decrypting #[error("Error encrypting or decrypting an event {0}")] Encryption(String), + /// Error generating enough random data for a cryptographic operation #[error("Error generating enough random data for a cryptographic operation")] Random(#[from] RngError), } @@ -64,6 +74,7 @@ impl From for Error { } } +/// Holding the data of the encrypted event #[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct EncryptedEvent { version: u8, @@ -73,7 +84,7 @@ pub struct EncryptedEvent { /// Version specific info for the key derivation method that is used. #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub enum KdfInfo { +enum KdfInfo { Pbkdf2ToChaCha20Poly1305 { /// The number of PBKDF rounds that were used when deriving the store /// key. @@ -87,7 +98,7 @@ pub enum KdfInfo { /// Version specific info for encryption method that is used to encrypt our /// store key. #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub enum CipherTextInfo { +enum CipherTextInfo { ChaCha20Poly1305 { /// The nonce that was used to encrypt the ciphertext. nonce: Vec, @@ -102,10 +113,10 @@ pub enum CipherTextInfo { pub struct EncryptedStoreKey { /// Info about the key derivation method that was used to expand the /// passphrase into an encryption key. - pub kdf_info: KdfInfo, + kdf_info: KdfInfo, /// The ciphertext with it's accompanying additional data that is needed to /// decrypt the store key. - pub ciphertext_info: CipherTextInfo, + ciphertext_info: CipherTextInfo, } /// A store key that can be used to encrypt entries in the store. @@ -185,6 +196,7 @@ impl StoreKey { Ok(nonce) } + /// Encrypt the given Event after serializing it with serde_json pub fn encrypt(&self, event: &impl Serialize) -> Result { let event = serde_json::to_vec(event)?; @@ -197,6 +209,7 @@ impl StoreKey { Ok(EncryptedEvent { version: VERSION, ciphertext, nonce }) } + /// Decrypt the given encrypted event back into the inner pub fn decrypt Deserialize<'b>>(&self, event: EncryptedEvent) -> Result { if event.version != VERSION { return Err(Error::Encryption( diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index 867ef23f073..85bd553f5f4 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -34,8 +34,6 @@ async-lock = "2.4.0" instant = { version = "0.1.12", features = ["wasm-bindgen", "inaccurate"] } futures-util = { version = "0.3.15", default-features = false, features = ["channel"] } wasm-bindgen-futures = "0.4.24" -web-sys = { version = "0.3.35", features = ["IdbKeyRange"] } -wasm-bindgen = "0.2" uuid = { version = "0.8.2", default-features = false, features = [ "v4", "wasm-bindgen", diff --git a/crates/matrix-sdk-common/src/lib.rs b/crates/matrix-sdk-common/src/lib.rs index 43fbf53faa2..1ecf2c6c22a 100644 --- a/crates/matrix-sdk-common/src/lib.rs +++ b/crates/matrix-sdk-common/src/lib.rs @@ -10,17 +10,13 @@ pub use async_trait::async_trait; pub use instant; +pub use ruma; pub mod deserialized_responses; pub mod executor; pub mod locks; pub mod util; -#[cfg(target_arch = "wasm32")] -mod wasm_helpers; -#[cfg(target_arch = "wasm32")] -pub use wasm_helpers::SafeEncode; - /// Super trait that is used for our store traits, this trait will differ if /// it's used on WASM. WASM targets will not require `Send` and `Sync` to have /// implemented, while other targets will. diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 9879f6ede9e..bfe6435b88d 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -19,9 +19,10 @@ rustdoc-args = ["--cfg", "docsrs"] default = [] qrcode = ["matrix-qrcode"] backups_v1 = [] -sled_cryptostore = ["sled"] -docsrs = ["sled_cryptostore"] -indexeddb_cryptostore = ["indexed_db_futures", "wasm-bindgen"] +docsrs = [] + +# Testing helpers for implementations based upon this +testing = ["http"] [dependencies] aes = { version = "0.7.4", features = ["ctr"] } @@ -42,14 +43,13 @@ rand = "0.8.4" serde = { version = "1.0.126", features = ["derive", "rc"] } serde_json = "1.0.64" sha2 = "0.10.1" -sled = { version = "0.34.6", optional = true } thiserror = "1.0.25" tracing = "0.1.26" zeroize = { version = "1.3.0", features = ["zeroize_derive"] } +anyhow = "1" -## Feature indexeddb-state-store -indexed_db_futures = { version = "0.2.0", optional = true } -wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true } +# feature = testing only +http = { version = "0.2.4", optional = true} [dependencies.ruma] version = "0.5.0" @@ -61,6 +61,7 @@ http = "0.2.4" indoc = "1.0.3" matches = "0.1.8" matrix-sdk-test = { version = "0.4.0", path = "../matrix-sdk-test" } +matrix-sdk-sled = { version = "0.1.0", path = "../matrix-sdk-sled", features = ["encryption"] } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] criterion = { version = "0.3.4", features = [ @@ -74,15 +75,11 @@ tokio = { version = "1.7.1", default-features = false, features = [ "rt-multi-thread", "macros", ] } -lazy_static = "1.4" [target.'cfg(target_os = "linux")'.dev-dependencies] pprof = { version = "0.6.2", features = ["flamegraph", "criterion"] } -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.24" [[bench]] name = "crypto_bench" harness = false -required-features = ["sled_cryptostore"] diff --git a/crates/matrix-sdk-crypto/README.md b/crates/matrix-sdk-crypto/README.md index a37c3cccc66..e5b2e26907e 100644 --- a/crates/matrix-sdk-crypto/README.md +++ b/crates/matrix-sdk-crypto/README.md @@ -61,3 +61,12 @@ The decision tree below visualizes the way this crate decides whether a room key will be shared with a requester upon a key request. ![](https://raw.githubusercontent.com/matrix-org/matrix-rust-sdk/main/contrib/key-sharing-algorithm/model.png) + + +# Crate Feature Flags + +The following crate feature flags are available: + +* `qrcode`: Enbles QRcode generation and reading code + +* `testing`: provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider. \ No newline at end of file diff --git a/crates/matrix-sdk-crypto/benches/crypto_bench.rs b/crates/matrix-sdk-crypto/benches/crypto_bench.rs index 153f9d2a7e5..3e168d5e129 100644 --- a/crates/matrix-sdk-crypto/benches/crypto_bench.rs +++ b/crates/matrix-sdk-crypto/benches/crypto_bench.rs @@ -2,6 +2,7 @@ use std::ops::Deref; use criterion::*; use matrix_sdk_crypto::{EncryptionSettings, OlmMachine}; +use matrix_sdk_sled::CryptoStore as SledCryptoStore; use matrix_sdk_test::response_from_file; use ruma::{ api::{ @@ -70,13 +71,9 @@ pub fn keys_query(c: &mut Criterion) { }); let dir = tempfile::tempdir().unwrap(); + let store = Box::new(SledCryptoStore::open_with_passphrase(dir, None).unwrap()); let machine = runtime - .block_on(OlmMachine::new_with_default_store( - alice_id(), - alice_device_id(), - dir.path(), - None, - )) + .block_on(OlmMachine::new_with_store(alice_id().into(), alice_device_id().into(), store)) .unwrap(); group.bench_with_input(BenchmarkId::new("sled store", &name), &response, |b, response| { @@ -122,12 +119,13 @@ pub fn keys_claiming(c: &mut Criterion) { b.iter_batched( || { let dir = tempfile::tempdir().unwrap(); + let store = Box::new(SledCryptoStore::open_with_passphrase(dir, None).unwrap()); + let machine = runtime - .block_on(OlmMachine::new_with_default_store( - alice_id(), - alice_device_id(), - dir.path(), - None, + .block_on(OlmMachine::new_with_store( + alice_id().into(), + alice_device_id().into(), + store, )) .unwrap(); runtime @@ -186,15 +184,11 @@ pub fn room_key_sharing(c: &mut Criterion) { machine.invalidate_group_session(room_id).await.unwrap(); }) }); - let dir = tempfile::tempdir().unwrap(); + let store = Box::new(SledCryptoStore::open_with_passphrase(dir, None).unwrap()); + let machine = runtime - .block_on(OlmMachine::new_with_default_store( - alice_id(), - alice_device_id(), - dir.path(), - None, - )) + .block_on(OlmMachine::new_with_store(alice_id().into(), alice_device_id().into(), store)) .unwrap(); runtime.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response)).unwrap(); runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap(); @@ -247,13 +241,10 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) { }); let dir = tempfile::tempdir().unwrap(); + let store = Box::new(SledCryptoStore::open_with_passphrase(dir, None).unwrap()); + let machine = runtime - .block_on(OlmMachine::new_with_default_store( - alice_id(), - alice_device_id(), - dir.path(), - None, - )) + .block_on(OlmMachine::new_with_store(alice_id().into(), alice_device_id().into(), store)) .unwrap(); runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap(); diff --git a/crates/matrix-sdk-crypto/src/error.rs b/crates/matrix-sdk-crypto/src/error.rs index 5b702942703..a2cdb5f2814 100644 --- a/crates/matrix-sdk-crypto/src/error.rs +++ b/crates/matrix-sdk-crypto/src/error.rs @@ -188,7 +188,7 @@ pub enum SignatureError { } #[derive(Error, Debug)] -pub(crate) enum SessionCreationError { +pub enum SessionCreationError { #[error( "Failed to create a new Olm session for {0} {1}, the requested \ one-time key isn't a signed curve key" diff --git a/crates/matrix-sdk-crypto/src/gossiping/mod.rs b/crates/matrix-sdk-crypto/src/gossiping/mod.rs index 8754b953cef..a5f12b17633 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/mod.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/mod.rs @@ -85,10 +85,9 @@ pub enum SecretInfo { } impl SecretInfo { - #[allow(dead_code)] /// Serialize `SecretInfo` into `String` for usage as database keys and /// comparison - pub(crate) fn as_key(&self) -> String { + pub fn as_key(&self) -> String { match &self { SecretInfo::KeyRequest(ref info) => format!( "keyRequest:{:}:{:}:{:}:{:}", diff --git a/crates/matrix-sdk-crypto/src/identities/device.rs b/crates/matrix-sdk-crypto/src/identities/device.rs index d4823d2e229..bd2406b66ab 100644 --- a/crates/matrix-sdk-crypto/src/identities/device.rs +++ b/crates/matrix-sdk-crypto/src/identities/device.rs @@ -47,7 +47,7 @@ use crate::{ verification::VerificationMachine, OutgoingVerificationRequest, Sas, ToDeviceRequest, VerificationRequest, }; -#[cfg(test)] +#[cfg(any(test, feature = "testing"))] use crate::{OlmMachine, ReadOnlyAccount}; /// A read-only version of a `Device`. @@ -374,8 +374,7 @@ impl ReadOnlyDevice { /// Create a new Device, this constructor skips signature verification of /// the keys, `TryFrom` should be used for completely new devices we /// receive. - #[cfg(feature = "sled_cryptostore")] - pub(crate) fn new(device_keys: DeviceKeys, trust_state: LocalTrust) -> Self { + pub fn new(device_keys: DeviceKeys, trust_state: LocalTrust) -> Self { Self { inner: device_keys.into(), trust_state: Arc::new(Atomic::new(trust_state)), @@ -572,12 +571,20 @@ impl ReadOnlyDevice { self.deleted.store(true, Ordering::Relaxed); } - #[cfg(test)] + #[cfg(any(test, feature = "testing"))] + #[allow(dead_code)] + /// Generate the Device from the reference of an OlmMachine. + /// + /// TESTING FACILITY ONLY, DO NOT USE OUTSIDE OF TESTS pub async fn from_machine(machine: &OlmMachine) -> ReadOnlyDevice { ReadOnlyDevice::from_account(machine.account()).await } - #[cfg(test)] + #[cfg(any(test, feature = "testing"))] + #[allow(dead_code)] + /// Generate the Device from the reference of an Account. + /// + /// TESTING FACILITY ONLY, DO NOT USE OUTSIDE OF TESTS pub async fn from_account(account: &ReadOnlyAccount) -> ReadOnlyDevice { let device_keys = account.device_keys().await; ReadOnlyDevice::try_from(&device_keys).unwrap() @@ -605,16 +612,17 @@ impl PartialEq for ReadOnlyDevice { } } -#[cfg(test)] -pub(crate) mod test { - use std::convert::TryFrom; - - use ruma::{encryption::DeviceKeys, user_id, DeviceKeyAlgorithm}; +#[cfg(any(test, feature = "testing"))] +pub(crate) mod testing { + //! Testing Facilities for Device Management + #![allow(dead_code)] + use ruma::encryption::DeviceKeys; use serde_json::json; - use crate::identities::{LocalTrust, ReadOnlyDevice}; + use crate::identities::ReadOnlyDevice; - fn device_keys() -> DeviceKeys { + /// Generate default DeviceKeys for tests + pub fn device_keys() -> DeviceKeys { let device_keys = json!({ "algorithms": vec![ "m.olm.v1.curve25519-aes-sha2", @@ -639,10 +647,19 @@ pub(crate) mod test { serde_json::from_value(device_keys).unwrap() } - pub(crate) fn get_device() -> ReadOnlyDevice { + /// Generate default ReadOnlyDevice for tests + pub fn get_device() -> ReadOnlyDevice { let device_keys = device_keys(); ReadOnlyDevice::try_from(&device_keys).unwrap() } +} + +#[cfg(test)] +pub(crate) mod test { + use ruma::{user_id, DeviceKeyAlgorithm}; + + use super::testing::{device_keys, get_device}; + use crate::identities::LocalTrust; #[test] fn create_a_device() { diff --git a/crates/matrix-sdk-crypto/src/identities/manager.rs b/crates/matrix-sdk-crypto/src/identities/manager.rs index c54befbc412..a44610afae6 100644 --- a/crates/matrix-sdk-crypto/src/identities/manager.rs +++ b/crates/matrix-sdk-crypto/src/identities/manager.rs @@ -537,12 +537,12 @@ impl IdentityManager { } } -#[cfg(test)] -pub(crate) mod test { +#[cfg(any(test, feature = "testing"))] +pub(crate) mod testing { + #![allow(dead_code)] use std::sync::Arc; use matrix_sdk_common::locks::Mutex; - use matrix_sdk_test::async_test; use ruma::{ api::{client::keys::get_keys::v3::Response as KeyQueryResponse, IncomingResponse}, device_id, user_id, DeviceId, UserId, @@ -551,25 +551,25 @@ pub(crate) mod test { use crate::{ identities::IdentityManager, - machine::test::response_from_file, + machine::testing::response_from_file, olm::{PrivateCrossSigningIdentity, ReadOnlyAccount}, store::{CryptoStore, MemoryStore, Store}, verification::VerificationMachine, }; - fn user_id() -> &'static UserId { + pub fn user_id() -> &'static UserId { user_id!("@example:localhost") } - fn other_user_id() -> &'static UserId { + pub fn other_user_id() -> &'static UserId { user_id!("@example2:localhost") } - fn device_id() -> &'static DeviceId { + pub fn device_id() -> &'static DeviceId { device_id!("WSKKLTJZCL") } - fn manager() -> IdentityManager { + pub(crate) fn manager() -> IdentityManager { let identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(user_id().to_owned()))); let user_id = Arc::from(user_id()); @@ -581,7 +581,7 @@ pub(crate) mod test { IdentityManager::new(user_id, device_id().into(), store) } - pub(crate) fn other_key_query() -> KeyQueryResponse { + pub fn other_key_query() -> KeyQueryResponse { let data = response_from_file(&json!({ "device_keys": { "@example2:localhost": { @@ -640,7 +640,7 @@ pub(crate) mod test { .expect("Can't parse the keys upload response") } - pub(crate) fn own_key_query() -> KeyQueryResponse { + pub fn own_key_query() -> KeyQueryResponse { let data = response_from_file(&json!({ "device_keys": { "@example:localhost": { @@ -740,6 +740,14 @@ pub(crate) mod test { KeyQueryResponse::try_from_http_response(data) .expect("Can't parse the keys upload response") } +} + +#[cfg(test)] +pub(crate) mod test { + use matrix_sdk_test::async_test; + use ruma::device_id; + + use super::testing::{manager, other_key_query, other_user_id}; #[async_test] async fn test_manager_creation() { diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index c1d40739c4d..49a087c861e 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -935,26 +935,20 @@ impl ReadOnlyOwnUserIdentity { } } -#[cfg(test)] -pub(crate) mod test { - use std::{convert::TryFrom, sync::Arc}; - - use matrix_sdk_common::locks::Mutex; - use matrix_sdk_test::async_test; +#[cfg(any(test, feature = "testing"))] +pub(crate) mod testing { + //! Testing Facilities + #![allow(dead_code)] use ruma::{api::client::keys::get_keys::v3::Response as KeyQueryResponse, user_id}; - use super::{ReadOnlyOwnUserIdentity, ReadOnlyUserIdentities, ReadOnlyUserIdentity}; - use crate::{ - identities::{ - manager::test::{other_key_query, own_key_query}, - Device, ReadOnlyDevice, - }, - olm::{PrivateCrossSigningIdentity, ReadOnlyAccount}, - store::MemoryStore, - verification::VerificationMachine, + use super::{ReadOnlyOwnUserIdentity, ReadOnlyUserIdentity}; + use crate::identities::{ + manager::testing::{other_key_query, own_key_query}, + ReadOnlyDevice, }; - fn device(response: &KeyQueryResponse) -> (ReadOnlyDevice, ReadOnlyDevice) { + /// Generate test devices from KeyQueryResponse + pub fn device(response: &KeyQueryResponse) -> (ReadOnlyDevice, ReadOnlyDevice) { let mut devices = response.device_keys.values().next().unwrap().values(); let first = ReadOnlyDevice::try_from(&devices.next().unwrap().deserialize().unwrap()).unwrap(); @@ -963,7 +957,8 @@ pub(crate) mod test { (first, second) } - fn own_identity(response: &KeyQueryResponse) -> ReadOnlyOwnUserIdentity { + /// Generate ReadOnlyOwnUserIdentity from KeyQueryResponse for testing + pub fn own_identity(response: &KeyQueryResponse) -> ReadOnlyOwnUserIdentity { let user_id = user_id!("@example:localhost"); let master_key = response.master_keys.get(user_id).unwrap().deserialize().unwrap(); @@ -974,11 +969,13 @@ pub(crate) mod test { .unwrap() } - pub(crate) fn get_own_identity() -> ReadOnlyOwnUserIdentity { + /// Generate default own identity for tests + pub fn get_own_identity() -> ReadOnlyOwnUserIdentity { own_identity(&own_key_query()) } - pub(crate) fn get_other_identity() -> ReadOnlyUserIdentity { + /// Generate default other identify for tests + pub fn get_other_identity() -> ReadOnlyUserIdentity { let user_id = user_id!("@example2:localhost"); let response = other_key_query(); @@ -987,6 +984,26 @@ pub(crate) mod test { ReadOnlyUserIdentity::new(master_key.into(), self_signing.into()).unwrap() } +} + +#[cfg(test)] +pub(crate) mod test { + use std::sync::Arc; + + use matrix_sdk_common::locks::Mutex; + use matrix_sdk_test::async_test; + use ruma::user_id; + + use super::{ + testing::{device, get_other_identity, get_own_identity}, + ReadOnlyOwnUserIdentity, ReadOnlyUserIdentities, + }; + use crate::{ + identities::{manager::testing::own_key_query, Device}, + olm::{PrivateCrossSigningIdentity, ReadOnlyAccount}, + store::MemoryStore, + verification::VerificationMachine, + }; #[test] fn own_identity_create() { diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 2871f5ce7a3..8c314b27f70 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -14,10 +14,10 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![warn(missing_docs)] #![deny( missing_debug_implementations, dead_code, - missing_docs, trivial_casts, trivial_numeric_casts, unused_extern_crates, @@ -42,6 +42,15 @@ pub mod store; mod utilities; mod verification; +#[cfg(feature = "testing")] +/// Testing facilities and helpers for crypto tests +pub mod testing { + pub use crate::identities::{ + device::testing::get_device, + user::testing::{get_other_identity, get_own_identity}, + }; +} + use std::collections::{BTreeMap, BTreeSet}; use ruma::RoomId; @@ -75,6 +84,7 @@ pub use file_encryption::{ decrypt_key_export, encrypt_key_export, AttachmentDecryptor, AttachmentEncryptor, DecryptorError, KeyExportError, MediaEncryptionInfo, }; +pub use gossiping::GossipRequest; pub use identities::{ Device, LocalTrust, MasterPubkey, OwnUserIdentity, ReadOnlyDevice, ReadOnlyOwnUserIdentity, ReadOnlyUserIdentities, ReadOnlyUserIdentity, UserDevices, UserIdentities, UserIdentity, @@ -82,13 +92,12 @@ pub use identities::{ pub use machine::OlmMachine; #[cfg(feature = "qrcode")] pub use matrix_qrcode; -pub(crate) use olm::ReadOnlyAccount; -pub use olm::{CrossSigningStatus, EncryptionSettings}; +pub use olm::{CrossSigningStatus, EncryptionSettings, ReadOnlyAccount}; pub use requests::{ IncomingResponse, KeysBackupRequest, KeysQueryRequest, OutgoingRequest, OutgoingRequests, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest, }; -pub use store::{CrossSigningKeyExport, CryptoStoreError, SecretImportError}; +pub use store::{CrossSigningKeyExport, CryptoStoreError, SecretImportError, SecretInfo}; pub use verification::{AcceptSettings, CancelInfo, Emoji, Sas, Verification, VerificationRequest}; #[cfg(feature = "qrcode")] pub use verification::{QrVerification, ScanError}; diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index 1fc314861fb..91d7e6fd63d 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(feature = "sled_cryptostore")] -use std::path::Path; use std::{ collections::{BTreeMap, BTreeSet, HashSet}, mem, @@ -54,8 +52,6 @@ use tracing::{debug, error, info, trace, warn}; #[cfg(feature = "backups_v1")] use crate::backups::BackupMachine; -#[cfg(feature = "sled_cryptostore")] -use crate::store::sled::SledStore; use crate::{ error::{EventError, MegolmError, MegolmResult, OlmError, OlmResult}, gossiping::GossipMachine, @@ -440,37 +436,9 @@ impl OlmMachine { } } - /// Should device or one-time keys be uploaded to the server. - /// - /// This needs to be checked periodically, ideally after every sync request. - /// - /// # Example - /// - /// ``` - /// # use std::convert::TryFrom; - /// # use matrix_sdk_crypto::OlmMachine; - /// # use ruma::user_id; - /// # use futures::executor::block_on; - /// # let alice = user_id!("@alice:example.org"); - /// # let machine = OlmMachine::new(alice, device_id!("DEVICEID")); - /// # block_on(async { - /// if machine.should_upload_keys().await { - /// let request = machine - /// .keys_for_upload() - /// .await - /// .unwrap(); - /// - /// // Upload the keys here. - /// } - /// # }); - /// ``` - #[cfg(test)] - async fn should_upload_keys(&self) -> bool { - self.account.should_upload_keys().await - } - /// Get the underlying Olm account of the machine. - #[cfg(test)] + #[cfg(any(test, feature = "testing"))] + #[allow(dead_code)] pub(crate) fn account(&self) -> &ReadOnlyAccount { &self.account } @@ -1548,13 +1516,21 @@ impl OlmMachine { &self.backup_machine } } +#[cfg(any(feature = "testing", test))] +pub(crate) mod testing { + #![allow(dead_code)] + use http::Response; + + pub fn response_from_file(json: &serde_json::Value) -> Response> { + Response::builder().status(200).body(json.to_string().as_bytes().to_vec()).unwrap() + } +} #[cfg(test)] pub(crate) mod test { use std::{collections::BTreeMap, convert::TryInto, iter, sync::Arc}; - use http::Response; use matrix_sdk_common::util::milli_seconds_since_unix_epoch; use matrix_sdk_test::{async_test, test_json}; use ruma::{ @@ -1581,6 +1557,7 @@ pub(crate) mod test { }; use serde_json::json; + use super::testing::response_from_file; use crate::{ machine::OlmMachine, olm::Utility, @@ -1603,10 +1580,6 @@ pub(crate) mod test { user_id!("@bob:example.com") } - pub fn response_from_file(json: &serde_json::Value) -> Response> { - Response::builder().status(200).body(json.to_string().as_bytes().to_vec()).unwrap() - } - fn keys_upload_response() -> upload_keys::v3::Response { let data = response_from_file(&test_json::KEYS_UPLOAD); upload_keys::v3::Response::try_from_http_response(data) @@ -1712,7 +1685,7 @@ pub(crate) mod test { #[async_test] async fn create_olm_machine() { let machine = OlmMachine::new(user_id(), alice_device_id()); - assert!(machine.should_upload_keys().await); + assert!(machine.account().should_upload_keys().await); } #[async_test] @@ -1722,21 +1695,21 @@ pub(crate) mod test { response.one_time_key_counts.remove(&DeviceKeyAlgorithm::SignedCurve25519).unwrap(); - assert!(machine.should_upload_keys().await); + assert!(machine.account().should_upload_keys().await); machine.receive_keys_upload_response(&response).await.unwrap(); - assert!(machine.should_upload_keys().await); + assert!(machine.account().should_upload_keys().await); response.one_time_key_counts.insert(DeviceKeyAlgorithm::SignedCurve25519, uint!(10)); machine.receive_keys_upload_response(&response).await.unwrap(); - assert!(machine.should_upload_keys().await); + assert!(machine.account().should_upload_keys().await); response.one_time_key_counts.insert(DeviceKeyAlgorithm::SignedCurve25519, uint!(50)); machine.receive_keys_upload_response(&response).await.unwrap(); - assert!(!machine.should_upload_keys().await); + assert!(!machine.account().should_upload_keys().await); response.one_time_key_counts.remove(&DeviceKeyAlgorithm::SignedCurve25519); machine.receive_keys_upload_response(&response).await.unwrap(); - assert!(!machine.should_upload_keys().await); + assert!(!machine.account().should_upload_keys().await); } #[async_test] @@ -1745,10 +1718,10 @@ pub(crate) mod test { let mut response = keys_upload_response(); - assert!(machine.should_upload_keys().await); + assert!(machine.account().should_upload_keys().await); machine.receive_keys_upload_response(&response).await.unwrap(); - assert!(machine.should_upload_keys().await); + assert!(machine.account().should_upload_keys().await); assert!(machine.account.generate_one_time_keys().await.is_ok()); response.one_time_key_counts.insert(DeviceKeyAlgorithm::SignedCurve25519, uint!(50)); diff --git a/crates/matrix-sdk-crypto/src/olm/account.rs b/crates/matrix-sdk-crypto/src/olm/account.rs index 15d96b6f7dd..4efe24cc402 100644 --- a/crates/matrix-sdk-crypto/src/olm/account.rs +++ b/crates/matrix-sdk-crypto/src/olm/account.rs @@ -69,8 +69,8 @@ use crate::{ #[derive(Debug, Clone)] pub struct Account { - pub(crate) inner: ReadOnlyAccount, - pub(crate) store: Store, + pub inner: ReadOnlyAccount, + pub store: Store, } #[derive(Debug, Clone)] @@ -149,7 +149,7 @@ impl Account { Ok((message, message_hash)) } - pub(crate) async fn save(&self) -> Result<(), CryptoStoreError> { + pub async fn save(&self) -> Result<(), CryptoStoreError> { self.store.save_account(self.inner.clone()).await } @@ -482,10 +482,13 @@ impl Account { /// devices. #[derive(Clone)] pub struct ReadOnlyAccount { - pub(crate) user_id: Arc, - pub(crate) device_id: Arc, + /// The user_id this account belongs to + pub user_id: Arc, + /// The device_id of this entry + pub device_id: Arc, inner: Arc>, - pub(crate) identity_keys: Arc, + /// The associated identity keys + pub identity_keys: Arc, shared: Arc, /// The number of signed one-time keys we have uploaded to the server. If /// this is None, no action will be taken. After a sync request the client @@ -582,7 +585,7 @@ impl ReadOnlyAccount { /// # Arguments /// /// * `new_count` - The new count that was reported by the server. - pub(crate) fn update_uploaded_key_count(&self, new_count: u64) { + pub fn update_uploaded_key_count(&self, new_count: u64) { self.uploaded_signed_key_count.store(new_count, Ordering::SeqCst); } @@ -600,31 +603,31 @@ impl ReadOnlyAccount { /// /// Messages shouldn't be encrypted with the session before it has been /// shared. - pub(crate) fn mark_as_shared(&self) { + pub fn mark_as_shared(&self) { self.shared.store(true, Ordering::SeqCst); } /// Get the one-time keys of the account. /// /// This can be empty, keys need to be generated first. - pub(crate) async fn one_time_keys(&self) -> OneTimeKeys { + pub async fn one_time_keys(&self) -> OneTimeKeys { self.inner.lock().await.parsed_one_time_keys() } /// Generate count number of one-time keys. - pub(crate) async fn generate_one_time_keys_helper(&self, count: usize) { + pub async fn generate_one_time_keys_helper(&self, count: usize) { self.inner.lock().await.generate_one_time_keys(count); } /// Get the maximum number of one-time keys the account can hold. - pub(crate) async fn max_one_time_keys(&self) -> usize { + pub async fn max_one_time_keys(&self) -> usize { self.inner.lock().await.max_number_of_one_time_keys() } /// Get a tuple of device and one-time keys that need to be uploaded. /// /// Returns an empty error if no keys need to be uploaded. - pub(crate) async fn generate_one_time_keys(&self) -> Result { + pub async fn generate_one_time_keys(&self) -> Result { // Only generate one-time keys if there aren't any, otherwise the caller // might have failed to upload them the last time this method was // called. @@ -648,7 +651,7 @@ impl ReadOnlyAccount { } /// Should account or one-time keys be uploaded to the server. - pub(crate) async fn should_upload_keys(&self) -> bool { + pub async fn should_upload_keys(&self) -> bool { if !self.shared() { return true; } @@ -671,7 +674,7 @@ impl ReadOnlyAccount { /// Get a tuple of device and one-time keys that need to be uploaded. /// /// Returns None if no keys need to be uploaded. - pub(crate) async fn keys_for_upload( + pub async fn keys_for_upload( &self, ) -> Option<(Option, BTreeMap, Raw>)> { if !self.should_upload_keys().await { @@ -686,7 +689,7 @@ impl ReadOnlyAccount { } /// Mark the current set of one-time keys as being published. - pub(crate) async fn mark_keys_as_published(&self) { + pub async fn mark_keys_as_published(&self) { self.inner.lock().await.mark_keys_as_published(); } @@ -698,7 +701,7 @@ impl ReadOnlyAccount { } #[cfg(feature = "backups_v1")] - pub(crate) fn is_signed(&self, json: &mut Value) -> Result<(), SignatureError> { + pub fn is_signed(&self, json: &mut Value) -> Result<(), SignatureError> { let signing_key = self.identity_keys.ed25519(); let utility = crate::olm::Utility::new(); @@ -755,7 +758,8 @@ impl ReadOnlyAccount { }) } - pub(crate) fn unsigned_device_keys(&self) -> DeviceKeys { + /// Generate the unsigned `DeviceKeys` from this ReadOnlyAccount + pub fn unsigned_device_keys(&self) -> DeviceKeys { let identity_keys = self.identity_keys(); let keys = BTreeMap::from([ ( @@ -779,7 +783,7 @@ impl ReadOnlyAccount { /// Sign the device keys of the account and return them so they can be /// uploaded. - pub(crate) async fn device_keys(&self) -> DeviceKeys { + pub async fn device_keys(&self) -> DeviceKeys { let mut device_keys = self.unsigned_device_keys(); // Create a copy of the device keys containing only fields that will @@ -803,13 +807,15 @@ impl ReadOnlyAccount { device_keys } - pub(crate) async fn bootstrap_cross_signing( + /// Bootstrap Cross-Signing + pub async fn bootstrap_cross_signing( &self, ) -> (PrivateCrossSigningIdentity, UploadSigningKeysRequest, SignatureUploadRequest) { PrivateCrossSigningIdentity::new_with_account(self).await } - pub(crate) async fn sign_cross_signing_key( + /// Sign the given CrossSigning Key in place + pub async fn sign_cross_signing_key( &self, cross_signing_key: &mut CrossSigningKey, ) -> Result<(), SignatureError> { @@ -827,7 +833,8 @@ impl ReadOnlyAccount { Ok(()) } - pub(crate) async fn sign_master_key( + /// Sign the given Master Key + pub async fn sign_master_key( &self, master_key: MasterPubkey, ) -> Result { @@ -866,9 +873,8 @@ impl ReadOnlyAccount { self.sign(&canonical_json.to_string()).await } - pub(crate) async fn signed_one_time_keys_helper( - &self, - ) -> BTreeMap, Raw> { + /// Generate a Map of One-Time-Keys for each DeviceKeyId + pub async fn signed_one_time_keys_helper(&self) -> BTreeMap, Raw> { let one_time_keys = self.one_time_keys().await; let mut one_time_key_map = BTreeMap::new(); @@ -912,7 +918,7 @@ impl ReadOnlyAccount { /// Generate, sign and prepare one-time keys to be uploaded. /// /// If no one-time keys need to be uploaded returns an empty error. - pub(crate) async fn signed_one_time_keys( + pub async fn signed_one_time_keys( &self, ) -> Result, Raw>, ()> { let _ = self.generate_one_time_keys().await?; @@ -929,7 +935,7 @@ impl ReadOnlyAccount { /// /// * `their_one_time_key` - A signed one-time key that the other account /// created and shared with us. - pub(crate) async fn create_outbound_session_helper( + pub async fn create_outbound_session_helper( &self, their_identity_key: &str, their_one_time_key: &SignedKey, @@ -967,7 +973,7 @@ impl ReadOnlyAccount { /// /// * `key_map` - A map from the algorithm and device id to the one-time key /// that the other account created and shared with us. - pub(crate) async fn create_outbound_session( + pub async fn create_outbound_session( &self, device: ReadOnlyDevice, key_map: &BTreeMap, Raw>, @@ -1030,7 +1036,7 @@ impl ReadOnlyAccount { /// /// * `message` - A pre-key Olm message that was sent to us by the other /// account. - pub(crate) async fn create_inbound_session( + pub async fn create_inbound_session( &self, their_identity_key: &str, message: PreKeyMessage, @@ -1072,7 +1078,7 @@ impl ReadOnlyAccount { /// /// * `settings` - Settings determining the algorithm and rotation period of /// the outbound group session. - pub(crate) async fn create_group_session_pair( + pub async fn create_group_session_pair( &self, room_id: &RoomId, settings: EncryptionSettings, @@ -1106,16 +1112,21 @@ impl ReadOnlyAccount { Ok((outbound, inbound)) } - #[cfg(test)] - pub(crate) async fn create_group_session_pair_with_defaults( + #[cfg(any(test, feature = "testing"))] + #[allow(dead_code)] + /// Testing only facility to create a group session pair with default + /// settings + pub async fn create_group_session_pair_with_defaults( &self, room_id: &RoomId, ) -> Result<(OutboundGroupSession, InboundGroupSession), ()> { self.create_group_session_pair(room_id, EncryptionSettings::default()).await } - #[cfg(test)] - pub(crate) async fn create_session_for(&self, other: &ReadOnlyAccount) -> (Session, Session) { + #[cfg(any(test, feature = "testing"))] + #[allow(dead_code)] + /// Testing only helper to create a session for the given Account + pub async fn create_session_for(&self, other: &ReadOnlyAccount) -> (Session, Session) { use ruma::events::{dummy::ToDeviceDummyEventContent, AnyToDeviceEventContent}; other.generate_one_time_keys_helper(1).await; diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs index 80bb6287413..715b3ad07c9 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![warn(missing_docs)] + use std::{ collections::BTreeMap, convert::TryFrom, @@ -62,11 +64,15 @@ use crate::error::{EventError, MegolmResult}; pub struct InboundGroupSession { inner: Arc>, history_visibility: Arc>, - pub(crate) session_id: Arc, + /// The SessionId associated to this GroupSession + pub session_id: Arc, first_known_index: u32, - pub(crate) sender_key: Arc, - pub(crate) signing_keys: Arc>, - pub(crate) room_id: Arc, + /// The sender_key associated to this GroupSession + pub sender_key: Arc, + /// Map of DeviceKeyAlgorithm to the public ed25519 key of the account + pub signing_keys: Arc>, + /// The Room this GroupSession belongs to + pub room_id: Arc, forwarding_chains: Arc>, imported: bool, backed_up: Arc, @@ -89,7 +95,7 @@ impl InboundGroupSession { /// /// * `session_key` - The private session key that is used to decrypt /// messages. - pub(crate) fn new( + pub fn new( sender_key: &str, signing_key: &str, room_id: &RoomId, @@ -159,7 +165,7 @@ impl InboundGroupSession { /// /// * `content` - A forwarded room key content that contains the session key /// to create the `InboundGroupSession`. - pub(crate) fn from_forwarded_key( + pub fn from_forwarded_key( sender_key: &str, content: &mut ToDeviceForwardedRoomKeyEventContent, ) -> Result { @@ -228,13 +234,15 @@ impl InboundGroupSession { } /// Reset the backup state of the inbound group session. - pub(crate) fn reset_backup_state(&self) { + pub fn reset_backup_state(&self) { self.backed_up.store(false, SeqCst) } - #[cfg(test)] + #[cfg(any(test, feature = "testing"))] #[allow(dead_code)] - pub(crate) fn mark_as_backed_up(&self) { + /// For testing, allow to manually mark this GroupSession to have been + /// backed up + pub fn mark_as_backed_up(&self) { self.backed_up.store(true, SeqCst) } @@ -327,7 +335,7 @@ impl InboundGroupSession { /// # Arguments /// /// * `message` - The message that should be decrypted. - pub(crate) async fn decrypt_helper( + pub async fn decrypt_helper( &self, message: String, ) -> Result<(String, u32), OlmGroupSessionError> { @@ -335,7 +343,7 @@ impl InboundGroupSession { } #[cfg(feature = "backups_v1")] - pub(crate) async fn to_backup(&self) -> BackedUpRoomKey { + pub async fn to_backup(&self) -> BackedUpRoomKey { self.export().await.into() } @@ -344,7 +352,7 @@ impl InboundGroupSession { /// # Arguments /// /// * `event` - The event that should be decrypted. - pub(crate) async fn decrypt( + pub async fn decrypt( &self, event: &SyncRoomEncryptedEvent, ) -> MegolmResult<(Raw, u32)> { diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs index a47c6f3cc51..06af067b87b 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs @@ -27,8 +27,10 @@ mod inbound; mod outbound; pub use inbound::{InboundGroupSession, InboundGroupSessionPickle, PickledInboundGroupSession}; +pub(crate) use outbound::ShareState; pub use outbound::{ - EncryptionSettings, OutboundGroupSession, PickledOutboundGroupSession, ShareInfo, ShareState, + EncryptionSettings, OlmOutboundGroupSession, OutboundGroupSession, PickledOutboundGroupSession, + ShareInfo, }; /// The private session key of a group session. diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs index d55b7bdb88e..10f18fd79a5 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs @@ -27,12 +27,11 @@ use dashmap::DashMap; use matrix_sdk_common::{instant::Instant, locks::Mutex}; pub use olm_rs::{ account::IdentityKeys, + outbound_group_session::OlmOutboundGroupSession, session::{OlmMessage, PreKeyMessage}, utility::OlmUtility, }; -use olm_rs::{ - errors::OlmGroupSessionError, outbound_group_session::OlmOutboundGroupSession, PicklingMode, -}; +use olm_rs::{errors::OlmGroupSessionError, PicklingMode}; use ruma::{ events::{ room::{ @@ -61,7 +60,7 @@ const ROTATION_PERIOD: Duration = Duration::from_millis(604800000); const ROTATION_MESSAGES: u64 = 100; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ShareState { +pub(crate) enum ShareState { NotShared, SharedButChangedSenderKey, Shared(u32), diff --git a/crates/matrix-sdk-crypto/src/olm/mod.rs b/crates/matrix-sdk-crypto/src/olm/mod.rs index 5f94549f5cb..d5d3c4d8649 100644 --- a/crates/matrix-sdk-crypto/src/olm/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/mod.rs @@ -25,11 +25,12 @@ mod utility; pub(crate) use account::{Account, OlmDecryptionInfo, SessionType}; pub use account::{AccountPickle, OlmMessageHash, PickledAccount, ReadOnlyAccount}; +pub(crate) use group_sessions::ShareState; pub use group_sessions::{ - EncryptionSettings, ExportedRoomKey, InboundGroupSession, InboundGroupSessionPickle, - OutboundGroupSession, PickledInboundGroupSession, PickledOutboundGroupSession, ShareInfo, + EncryptionSettings, ExportedRoomKey, GroupSessionKey, InboundGroupSession, + InboundGroupSessionPickle, OlmOutboundGroupSession, OutboundGroupSession, + PickledInboundGroupSession, PickledOutboundGroupSession, ShareInfo, }; -pub(crate) use group_sessions::{GroupSessionKey, ShareState}; use matrix_sdk_common::instant::{Duration, Instant}; pub use olm_rs::{account::IdentityKeys, PicklingMode}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; diff --git a/crates/matrix-sdk-crypto/src/olm/session.rs b/crates/matrix-sdk-crypto/src/olm/session.rs index 7c52747b641..b57ad6b401d 100644 --- a/crates/matrix-sdk-crypto/src/olm/session.rs +++ b/crates/matrix-sdk-crypto/src/olm/session.rs @@ -43,15 +43,24 @@ use crate::{ /// `Account`s #[derive(Clone)] pub struct Session { - pub(crate) user_id: Arc, - pub(crate) device_id: Arc, - pub(crate) our_identity_keys: Arc, - pub(crate) inner: Arc>, - pub(crate) session_id: Arc, - pub(crate) sender_key: Arc, - pub(crate) created_using_fallback_key: bool, - pub(crate) creation_time: Arc, - pub(crate) last_use_time: Arc, + /// The `UserId` associated with this session + pub user_id: Arc, + /// The specific `DeviceId` associated with this session + pub device_id: Arc, + /// The `IdentityKeys` associated with this session + pub our_identity_keys: Arc, + /// The OlmSession + pub inner: Arc>, + /// Our sessionId + pub session_id: Arc, + /// The Key of the sender + pub sender_key: Arc, + /// Has this been created using the fallback key + pub created_using_fallback_key: bool, + /// When the session was created + pub creation_time: Arc, + /// When the session was last used + pub last_use_time: Arc, } #[cfg(not(tarpaulin_include))] diff --git a/crates/matrix-sdk-crypto/src/olm/signing/mod.rs b/crates/matrix-sdk-crypto/src/olm/signing/mod.rs index 84be14269b2..b33cfd02aa4 100644 --- a/crates/matrix-sdk-crypto/src/olm/signing/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/signing/mod.rs @@ -527,8 +527,9 @@ impl PrivateCrossSigningIdentity { /// Create a new cross signing identity without signing the device that /// created it. - #[cfg(test)] - pub(crate) async fn new(user_id: Box) -> Self { + #[cfg(any(test, feature = "testing"))] + #[allow(dead_code)] + pub async fn new(user_id: Box) -> Self { let master = Signing::new(); let public_key = master.cross_signing_key(user_id.clone(), KeyUsage::Master); @@ -537,8 +538,11 @@ impl PrivateCrossSigningIdentity { Self::new_helper(&user_id, master).await } - #[cfg(test)] - pub(crate) async fn reset(&mut self) { + #[cfg(any(test, feature = "testing"))] + #[allow(dead_code)] + /// Testing helper to reset this CrossSigning with a fresh one using the + /// local ideniy + pub async fn reset(&mut self) { let new = Self::new(self.user_id().to_owned()).await; *self = new } diff --git a/crates/matrix-sdk-crypto/src/store/caches.rs b/crates/matrix-sdk-crypto/src/store/caches.rs index ea3d204d6ec..160fc66e596 100644 --- a/crates/matrix-sdk-crypto/src/store/caches.rs +++ b/crates/matrix-sdk-crypto/src/store/caches.rs @@ -191,7 +191,7 @@ mod test { use ruma::room_id; use crate::{ - identities::device::test::get_device, + identities::device::testing::get_device, olm::{test::get_account_and_session, InboundGroupSession}, store::caches::{DeviceStore, GroupSessionStore, SessionStore}, }; diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index 8a758b44fd1..5782fc1f14c 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -1,5 +1,5 @@ #[allow(unused_macros)] - +#[macro_export] macro_rules! cryptostore_integration_tests { ($($name:ident)*) => { $( @@ -8,20 +8,17 @@ macro_rules! cryptostore_integration_tests { use std::collections::BTreeMap; use matrix_sdk_test::async_test; - use olm_rs::outbound_group_session::OlmOutboundGroupSession; - use ruma::{ + use matrix_sdk_common::ruma::{ encryption::SignedKey, events::room_key_request::RequestedKeyInfo, serde::Base64, user_id, TransactionId, DeviceId, EventEncryptionAlgorithm, UserId, room_id, device_id, }; - use crate::{ - gossiping::SecretInfo, - identities::{ - device::test::get_device, - user::test::{get_other_identity, get_own_identity}, - }, + use $crate::{ + SecretInfo, + testing::{get_device, get_other_identity, get_own_identity}, olm::{ + OlmOutboundGroupSession, GroupSessionKey, InboundGroupSession, OlmMessageHash, PrivateCrossSigningIdentity, ReadOnlyAccount, Session, }, @@ -346,31 +343,31 @@ macro_rules! cryptostore_integration_tests { let (_account, store) = get_loaded_store(dir.clone()).await; let device = get_device(); - assert!(store.update_tracked_user(device.user_id(), false).await.unwrap()); - assert!(!store.update_tracked_user(device.user_id(), false).await.unwrap()); + assert!(store.update_tracked_user(device.user_id(), false).await.unwrap(), "We were not tracked"); + assert!(!store.update_tracked_user(device.user_id(), false).await.unwrap(), "We were still tracked "); assert!(store.is_user_tracked(device.user_id())); - assert!(!store.users_for_key_query().contains(device.user_id())); - assert!(!store.update_tracked_user(device.user_id(), true).await.unwrap()); - assert!(store.users_for_key_query().contains(device.user_id())); + assert!(!store.users_for_key_query().contains(device.user_id()), "Unexpectedly key found"); + assert!(!store.update_tracked_user(device.user_id(), true).await.unwrap(), "User was there?"); + assert!(store.users_for_key_query().contains(device.user_id()), "Didn't find the key despite tracking"); drop(store); let store = get_store(dir.clone(), None).await; store.load_account().await.unwrap(); - assert!(store.is_user_tracked(device.user_id())); - assert!(store.users_for_key_query().contains(device.user_id())); + assert!(store.is_user_tracked(device.user_id()), "Reopened didn't track"); + assert!(store.users_for_key_query().contains(device.user_id()), "Reopened doesn't have the key"); store.update_tracked_user(device.user_id(), false).await.unwrap(); - assert!(!store.users_for_key_query().contains(device.user_id())); + assert!(!store.users_for_key_query().contains(device.user_id()), "Reopened has the key despite us not tracking"); drop(store); let store = get_store(dir, None).await; store.load_account().await.unwrap(); - assert!(!store.users_for_key_query().contains(device.user_id())); + assert!(!store.users_for_key_query().contains(device.user_id()), "Reloaded store has the account"); } #[async_test] diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index 53d527080d8..57b6ac2426f 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -312,7 +312,7 @@ mod test { use ruma::room_id; use crate::{ - identities::device::test::get_device, + identities::device::testing::get_device, olm::{test::get_account_and_session, InboundGroupSession, OlmMessageHash}, store::{memorystore::MemoryStore, Changes, CryptoStore}, }; diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 9e54eae8f2b..1c6f9912b8c 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -40,13 +40,11 @@ pub mod caches; mod memorystore; mod pickle_key; -#[cfg(test)] + +#[cfg(any(test, feature = "testing"))] #[macro_use] +#[allow(missing_docs)] pub mod integration_tests; -#[cfg(feature = "indexeddb_cryptostore")] -pub(crate) mod indexeddb; -#[cfg(feature = "sled_cryptostore")] -pub(crate) mod sled; use std::{ collections::{HashMap, HashSet}, @@ -57,11 +55,11 @@ use std::{ }; use base64::DecodeError; -#[cfg(feature = "indexeddb_cryptostore")] -use indexed_db_futures::web_sys::DomException; use matrix_sdk_common::{async_trait, locks::Mutex, AsyncTraitDeps}; pub use memorystore::MemoryStore; use olm_rs::errors::{OlmAccountError, OlmGroupSessionError, OlmSessionError}; +#[allow(unused_imports)] +pub use olm_rs::{account::IdentityKeys, PicklingMode}; pub use pickle_key::{EncryptedPickleKey, PickleKey}; use ruma::{ events::secret::request::SecretName, identifiers::Error as IdentifierValidationError, DeviceId, @@ -72,10 +70,6 @@ use thiserror::Error; use tracing::{info, warn}; use zeroize::Zeroize; -#[cfg(feature = "indexeddb_cryptostore")] -pub use self::indexeddb::IndexeddbStore; -#[cfg(feature = "sled_cryptostore")] -pub use self::sled::SledStore; use crate::{ error::SessionUnpicklingError, identities::{ @@ -102,7 +96,7 @@ pub use crate::gossiping::{GossipRequest, SecretInfo}; /// generics don't mix let the CryptoStore store strings and this wrapper /// adds the generic interface on top. #[derive(Debug, Clone)] -pub(crate) struct Store { +pub struct Store { user_id: Arc, identity: Arc>, inner: Arc, @@ -239,6 +233,7 @@ pub enum SecretImportError { } impl Store { + /// Create a new Store pub fn new( user_id: Arc, identity: Arc>, @@ -248,27 +243,33 @@ impl Store { Self { user_id, identity, inner: store, verification_machine } } + /// UserId associated with this store pub fn user_id(&self) -> &UserId { &self.user_id } + /// DeviceId associated with this store pub fn device_id(&self) -> &DeviceId { self.verification_machine.own_device_id() } + /// The Account associated with this store pub fn account(&self) -> &ReadOnlyAccount { &self.verification_machine.store.account } #[cfg(test)] + /// test helper to reset the cross signing identity pub async fn reset_cross_signing_identity(&self) { self.identity.lock().await.reset().await; } + /// PrivateCrossSigningIdentity associated with this store pub fn private_identity(&self) -> Arc> { self.identity.clone() } + /// Save the given Sessions to the store pub async fn save_sessions(&self, sessions: &[Session]) -> Result<()> { let changes = Changes { sessions: sessions.to_vec(), ..Default::default() }; @@ -276,6 +277,7 @@ impl Store { } #[cfg(test)] + /// Testing helper to allo to save only a set of devices pub async fn save_devices(&self, devices: &[ReadOnlyDevice]) -> Result<()> { let changes = Changes { devices: DeviceChanges { changed: devices.to_vec(), ..Default::default() }, @@ -286,6 +288,7 @@ impl Store { } #[cfg(test)] + /// Testing helper to allo to save only a set of InboundGroupSession pub async fn save_inbound_group_sessions( &self, sessions: &[InboundGroupSession], @@ -304,6 +307,7 @@ impl Store { .and_then(|d| d.display_name().map(|d| d.to_string()))) } + /// Get the read-only device associated with `device_id` for `user_id` pub async fn get_readonly_device( &self, user_id: &UserId, @@ -352,6 +356,7 @@ impl Store { }) } + /// Get all devices associated with the given `user_id` pub async fn get_user_devices(&self, user_id: &UserId) -> Result { let devices = self.inner.get_user_devices(user_id).await?; @@ -367,6 +372,7 @@ impl Store { }) } + /// Get a Device copy associated with `device_id` for `user_id` pub async fn get_device( &self, user_id: &UserId, @@ -384,6 +390,7 @@ impl Store { })) } + /// Get the Identity of `user_id` pub async fn get_identity(&self, user_id: &UserId) -> Result> { // let own_identity = // self.inner.get_user_identity(self.user_id()).await?.and_then(|i| i.own()); @@ -450,6 +457,7 @@ impl Store { } } + /// Import the Cross Signing Keys pub async fn import_cross_signing_keys( &self, export: CrossSigningKeyExport, @@ -479,6 +487,7 @@ impl Store { Ok(self.identity.lock().await.status().await) } + /// Import the given `secret` named `secret_name` into the keystore. pub async fn import_secret( &self, secret_name: &SecretName, @@ -538,23 +547,6 @@ pub enum CryptoStoreError { #[error("can't save/load sessions or group sessions in the store before an account is stored")] AccountUnset, - /// Error in the internal database - #[cfg(feature = "sled_cryptostore")] - #[error(transparent)] - Database(#[from] sled::Error), - - /// Error in the internal database - #[cfg(feature = "indexeddb_cryptostore")] - #[error("IndexedDB error: {name} ({code}): {message}")] - IndexedDatabase { - /// DomException code - code: u16, - /// Specific name of the DomException - name: String, - /// Message given to the DomException - message: String, - }, - /// An IO error occurred. #[error(transparent)] Io(#[from] IoError), @@ -586,17 +578,9 @@ pub enum CryptoStoreError { /// The store failed to (de)serialize a data type. #[error(transparent)] Serialization(#[from] SerdeError), -} - -#[cfg(feature = "indexeddb_cryptostore")] -impl From for CryptoStoreError { - fn from(frm: DomException) -> CryptoStoreError { - CryptoStoreError::IndexedDatabase { - name: frm.name(), - message: frm.message(), - code: frm.code(), - } - } + /// A problem with the underlying database backend + #[error(transparent)] + Backend(#[from] anyhow::Error), } /// Trait abstracting a store that the `OlmMachine` uses to store cryptographic diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml new file mode 100644 index 00000000000..8e0fed52450 --- /dev/null +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "matrix-sdk-indexeddb" +version = "0.1.0" +edition = "2021" + +[features] +default = ["encryption"] +encryption = ["matrix-sdk-crypto"] + +[package.metadata.docs.rs] +default-target = "wasm32-unknown-unknown" + +[dependencies] +matrix-sdk-base = { path = "../matrix-sdk-base", features = ["store_key"] } +matrix-sdk-crypto = { path = "../matrix-sdk-crypto", optional = true } +matrix-sdk-common = { path = "../matrix-sdk-common" } +thiserror = "1.0.25" +anyhow = "1" + +futures-util = { version = "0.3.15", default-features = false } +indexed_db_futures = { version = "0.2.0" } +wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"] } +web-sys = { version = "0.3.35", features = ["IdbKeyRange"] } +serde = { version = "1.0.126" } +serde_json = "1.0.64" +dashmap = "4.0.2" +tracing = "0.1.26" + + +[target.'cfg(target_arch = "wasm32")'.dependencies] +# for wasm32 we need to activate this +getrandom = { version = "0.2", features = ["js"]} + +[dev-dependencies] +matrix-sdk-base = { path = "../matrix-sdk-base", features = ["testing"] } +matrix-sdk-crypto = { path = "../matrix-sdk-crypto", features = ["testing"] } +matrix-sdk-test = { path = "../matrix-sdk-test" } +wasm-bindgen-test = "0.3.24" \ No newline at end of file diff --git a/crates/matrix-sdk-crypto/src/store/indexeddb.rs b/crates/matrix-sdk-indexeddb/src/cryptostore.rs similarity index 80% rename from crates/matrix-sdk-crypto/src/store/indexeddb.rs rename to crates/matrix-sdk-indexeddb/src/cryptostore.rs index e4460b20265..9320047fee3 100644 --- a/crates/matrix-sdk-crypto/src/store/indexeddb.rs +++ b/crates/matrix-sdk-indexeddb/src/cryptostore.rs @@ -18,33 +18,28 @@ use std::{ sync::{Arc, RwLock}, }; +use anyhow::anyhow; use dashmap::DashSet; use indexed_db_futures::prelude::*; -use matrix_sdk_common::{async_trait, locks::Mutex, SafeEncode}; -use olm_rs::{account::IdentityKeys, PicklingMode}; -use ruma::{DeviceId, RoomId, TransactionId, UserId}; -use wasm_bindgen::JsValue; - -use super::{ - caches::SessionStore, BackupKeys, Changes, CryptoStore, CryptoStoreError, EncryptedPickleKey, - InboundGroupSession, PickleKey, ReadOnlyAccount, Result, RoomKeyCounts, Session, +use matrix_sdk_common::{ + async_trait, + locks::Mutex, + ruma::{DeviceId, RoomId, TransactionId, UserId}, }; -use crate::{ - gossiping::{GossipRequest, SecretInfo}, - identities::{ReadOnlyDevice, ReadOnlyUserIdentities}, - olm::{OutboundGroupSession, PrivateCrossSigningIdentity}, +use matrix_sdk_crypto::{ + olm::{ + InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity, + Session, + }, + store::{ + caches::SessionStore, BackupKeys, Changes, CryptoStore, CryptoStoreError, + EncryptedPickleKey, IdentityKeys, PickleKey, PicklingMode, RoomKeyCounts, + }, + GossipRequest, ReadOnlyAccount, ReadOnlyDevice, ReadOnlyUserIdentities, SecretInfo, }; +use wasm_bindgen::JsValue; -/// This needs to be 32 bytes long since AES-GCM requires it, otherwise we will -/// panic once we try to pickle a Signing object. -const DEFAULT_PICKLE: &str = "DEFAULT_PICKLE_PASSPHRASE_123456"; - -#[derive(Clone, Debug)] -pub struct AccountInfo { - user_id: Arc, - device_id: Arc, - identity_keys: Arc, -} +use crate::safe_encode::SafeEncode; #[allow(non_snake_case)] mod KEYS { @@ -90,6 +85,56 @@ impl std::fmt::Debug for IndexeddbStore { } } +#[derive(Debug, thiserror::Error)] +pub enum IndexeddbStoreError { + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error("DomException {name} ({code}): {message}")] + DomException { + /// DomException code + code: u16, + /// Specific name of the DomException + name: String, + /// Message given to the DomException + message: String, + }, + #[error(transparent)] + CryptoStoreError(#[from] CryptoStoreError), +} + +impl From for IndexeddbStoreError { + fn from(frm: indexed_db_futures::web_sys::DomException) -> IndexeddbStoreError { + IndexeddbStoreError::DomException { + name: frm.name(), + message: frm.message(), + code: frm.code(), + } + } +} + +impl From for CryptoStoreError { + fn from(frm: IndexeddbStoreError) -> CryptoStoreError { + match frm { + IndexeddbStoreError::Json(e) => CryptoStoreError::Serialization(e), + IndexeddbStoreError::CryptoStoreError(e) => e, + _ => CryptoStoreError::Backend(anyhow!(frm)), + } + } +} + +type Result = std::result::Result; + +/// This needs to be 32 bytes long since AES-GCM requires it, otherwise we will +/// panic once we try to pickle a Signing object. +const DEFAULT_PICKLE: &str = "DEFAULT_PICKLE_PASSPHRASE_123456"; + +#[derive(Clone, Debug)] +pub struct AccountInfo { + user_id: Arc, + device_id: Arc, + identity_keys: Arc, +} + impl IndexeddbStore { async fn open_helper(prefix: String, pickle_key: Option) -> Result { let name = format!("{:0}::matrix-sdk-crypto", prefix); @@ -205,7 +250,7 @@ impl IndexeddbStore { } async fn save_changes(&self, changes: Changes) -> Result<()> { - let mut stores: Vec<&'static str> = [ + let mut stores: Vec<&str> = [ (changes.account.is_some() || changes.private_identity.is_some(), KEYS::CORE), (!changes.sessions.is_empty(), KEYS::SESSION), ( @@ -234,7 +279,7 @@ impl IndexeddbStore { ]) } - if stores.len() == 0 { + if stores.is_empty() { // nothing to do, quit early return Ok(()); } @@ -381,11 +426,9 @@ impl IndexeddbStore { let os = tx.object_store(KEYS::TRACKED_USERS)?; let user_ids = os.get_all_keys()?.await?; for user_id in user_ids.iter() { - let dirty: bool = match os.get(&user_id)?.await?.map(|v| v.into_serde()) { - Some(Ok(false)) => false, - _ => true, - }; - let user = match user_id.as_string().map(|u| UserId::parse(u)) { + let dirty: bool = + !matches!(os.get(&user_id)?.await?.map(|v| v.into_serde()), Some(Ok(false))); + let user = match user_id.as_string().map(UserId::parse) { Some(Ok(user)) => user, _ => continue, }; @@ -450,10 +493,7 @@ impl IndexeddbStore { Some(request) => Some(request), }) } -} -#[async_trait(?Send)] -impl CryptoStore for IndexeddbStore { async fn load_account(&self) -> Result> { if let Some(pickle) = self .inner @@ -465,7 +505,8 @@ impl CryptoStore for IndexeddbStore { self.load_tracked_users().await?; let account = - ReadOnlyAccount::from_pickle(pickle.into_serde()?, self.get_pickle_mode())?; + ReadOnlyAccount::from_pickle(pickle.into_serde()?, self.get_pickle_mode()) + .map_err(CryptoStoreError::OlmAccount)?; let account_info = AccountInfo { user_id: account.user_id.clone(), @@ -481,20 +522,6 @@ impl CryptoStore for IndexeddbStore { } } - async fn save_account(&self, account: ReadOnlyAccount) -> Result<()> { - let account_info = AccountInfo { - user_id: account.user_id.clone(), - device_id: account.device_id.clone(), - identity_keys: account.identity_keys.clone(), - }; - - *self.account_info.write().unwrap() = Some(account_info); - - let changes = Changes { account: Some(account), ..Default::default() }; - - self.save_changes(changes).await - } - async fn load_identity(&self) -> Result> { if let Some(pickle) = self .inner @@ -516,16 +543,12 @@ impl CryptoStore for IndexeddbStore { } } - async fn save_changes(&self, changes: Changes) -> Result<()> { - self.save_changes(changes).await - } - async fn get_sessions(&self, sender_key: &str) -> Result>>>> { let account_info = self.get_account_info().ok_or(CryptoStoreError::AccountUnset)?; if self.session_cache.get(sender_key).is_none() { let range = - sender_key.encode_to_range().map_err(|e| CryptoStoreError::IndexedDatabase { + sender_key.encode_to_range().map_err(|e| IndexeddbStoreError::DomException { code: 0, name: "IdbKeyRangeMakeError".to_owned(), message: e, @@ -573,10 +596,10 @@ impl CryptoStore for IndexeddbStore { .get(&key)? .await? { - Ok(Some(InboundGroupSession::from_pickle( - pickle.into_serde()?, - self.get_pickle_mode(), - )?)) + Ok(Some( + InboundGroupSession::from_pickle(pickle.into_serde()?, self.get_pickle_mode()) + .map_err(CryptoStoreError::OlmGroupSession)?, + )) } else { Ok(None) } @@ -598,13 +621,6 @@ impl CryptoStore for IndexeddbStore { .collect()) } - async fn get_outbound_group_sessions( - &self, - room_id: &RoomId, - ) -> Result> { - self.load_outbound_group_session(room_id).await - } - async fn inbound_group_session_counts(&self) -> Result { let all = self.get_inbound_group_sessions().await?; let backed_up = all.iter().filter(|s| s.backed_up()).count(); @@ -646,14 +662,6 @@ impl CryptoStore for IndexeddbStore { Ok(()) } - fn is_user_tracked(&self, user_id: &UserId) -> bool { - self.tracked_users_cache.contains(user_id) - } - - fn has_users_for_key_query(&self) -> bool { - !self.users_for_key_query_cache.is_empty() - } - fn tracked_users(&self) -> HashSet> { self.tracked_users_cache.to_owned().iter().map(|u| u.clone()).collect() } @@ -662,10 +670,6 @@ impl CryptoStore for IndexeddbStore { self.users_for_key_query_cache.iter().map(|u| u.clone()).collect() } - async fn load_backup_keys(&self) -> Result { - todo!() - } - async fn update_tracked_user(&self, user: &UserId, dirty: bool) -> Result { let already_added = self.tracked_users_cache.insert(user.to_owned()); @@ -712,7 +716,7 @@ impl CryptoStore for IndexeddbStore { &self, user_id: &UserId, ) -> Result, ReadOnlyDevice>> { - let range = user_id.encode_to_range().map_err(|e| CryptoStoreError::IndexedDatabase { + let range = user_id.encode_to_range().map_err(|e| IndexeddbStoreError::DomException { code: 0, name: "IdbKeyRangeMakeError".to_owned(), message: e, @@ -742,7 +746,7 @@ impl CryptoStore for IndexeddbStore { .transpose()?) } - async fn is_message_known(&self, hash: &crate::olm::OlmMessageHash) -> Result { + async fn is_message_known(&self, hash: &OlmMessageHash) -> Result { Ok(self .inner .transaction_on_one_with_mode(KEYS::OLM_HASHES, IdbTransactionMode::Readonly)? @@ -752,13 +756,6 @@ impl CryptoStore for IndexeddbStore { .is_some()) } - async fn get_outgoing_secret_requests( - &self, - request_id: &TransactionId, - ) -> Result> { - self.get_outgoing_key_request_helper(&request_id.as_str()).await - } - async fn get_secret_request_by_info( &self, key_info: &SecretInfo, @@ -772,8 +769,7 @@ impl CryptoStore for IndexeddbStore { .object_store(KEYS::SECRET_REQUESTS_BY_INFO)? .get(&key_info.as_key().encode())? .await? - .map(|i| i.as_string()) - .flatten(); + .and_then(|i| i.as_string()); if let Some(id) = id { self.get_outgoing_key_request_helper(&id).await } else { @@ -834,10 +830,163 @@ impl CryptoStore for IndexeddbStore { } } +#[async_trait(?Send)] +impl CryptoStore for IndexeddbStore { + async fn load_account(&self) -> Result, CryptoStoreError> { + self.load_account().await.map_err(|e| e.into()) + } + + async fn save_account(&self, account: ReadOnlyAccount) -> Result<(), CryptoStoreError> { + let account_info = AccountInfo { + user_id: account.user_id.clone(), + device_id: account.device_id.clone(), + identity_keys: account.identity_keys.clone(), + }; + + *self.account_info.write().unwrap() = Some(account_info); + + let changes = Changes { account: Some(account), ..Default::default() }; + + self.save_changes(changes).await.map_err(|e| e.into()) + } + + async fn load_identity(&self) -> Result, CryptoStoreError> { + self.load_identity().await.map_err(|e| e.into()) + } + + async fn save_changes(&self, changes: Changes) -> Result<(), CryptoStoreError> { + self.save_changes(changes).await.map_err(|e| e.into()) + } + + async fn get_sessions( + &self, + sender_key: &str, + ) -> Result>>>, CryptoStoreError> { + self.get_sessions(sender_key).await.map_err(|e| e.into()) + } + + async fn get_inbound_group_session( + &self, + room_id: &RoomId, + sender_key: &str, + session_id: &str, + ) -> Result, CryptoStoreError> { + self.get_inbound_group_session(room_id, sender_key, session_id).await.map_err(|e| e.into()) + } + + async fn get_inbound_group_sessions( + &self, + ) -> Result, CryptoStoreError> { + self.get_inbound_group_sessions().await.map_err(|e| e.into()) + } + + async fn get_outbound_group_sessions( + &self, + room_id: &RoomId, + ) -> Result, CryptoStoreError> { + self.load_outbound_group_session(room_id).await.map_err(|e| e.into()) + } + + async fn inbound_group_session_counts(&self) -> Result { + self.inbound_group_session_counts().await.map_err(|e| e.into()) + } + + async fn inbound_group_sessions_for_backup( + &self, + limit: usize, + ) -> Result, CryptoStoreError> { + self.inbound_group_sessions_for_backup(limit).await.map_err(|e| e.into()) + } + + async fn reset_backup_state(&self) -> Result<(), CryptoStoreError> { + self.reset_backup_state().await.map_err(|e| e.into()) + } + + fn is_user_tracked(&self, user_id: &UserId) -> bool { + self.tracked_users_cache.contains(user_id) + } + + fn has_users_for_key_query(&self) -> bool { + !self.users_for_key_query_cache.is_empty() + } + + fn tracked_users(&self) -> HashSet> { + self.tracked_users() + } + + fn users_for_key_query(&self) -> HashSet> { + self.users_for_key_query() + } + + async fn load_backup_keys(&self) -> Result { + todo!() + } + + async fn update_tracked_user( + &self, + user: &UserId, + dirty: bool, + ) -> Result { + self.update_tracked_user(user, dirty).await.map_err(|e| e.into()) + } + + async fn get_device( + &self, + user_id: &UserId, + device_id: &DeviceId, + ) -> Result, CryptoStoreError> { + self.get_device(user_id, device_id).await.map_err(|e| e.into()) + } + + async fn get_user_devices( + &self, + user_id: &UserId, + ) -> Result, ReadOnlyDevice>, CryptoStoreError> { + self.get_user_devices(user_id).await.map_err(|e| e.into()) + } + + async fn get_user_identity( + &self, + user_id: &UserId, + ) -> Result, CryptoStoreError> { + self.get_user_identity(user_id).await.map_err(|e| e.into()) + } + + async fn is_message_known(&self, hash: &OlmMessageHash) -> Result { + self.is_message_known(hash).await.map_err(|e| e.into()) + } + + async fn get_outgoing_secret_requests( + &self, + request_id: &TransactionId, + ) -> Result, CryptoStoreError> { + self.get_outgoing_key_request_helper(request_id.as_str()).await.map_err(|e| e.into()) + } + + async fn get_secret_request_by_info( + &self, + key_info: &SecretInfo, + ) -> Result, CryptoStoreError> { + self.get_secret_request_by_info(key_info).await.map_err(|e| e.into()) + } + + async fn get_unsent_secret_requests(&self) -> Result, CryptoStoreError> { + self.get_unsent_secret_requests().await.map_err(|e| e.into()) + } + + async fn delete_outgoing_secret_requests( + &self, + request_id: &TransactionId, + ) -> Result<(), CryptoStoreError> { + self.delete_outgoing_secret_requests(request_id).await.map_err(|e| e.into()) + } +} + #[cfg(test)] mod test { use super::IndexeddbStore; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + use matrix_sdk_crypto::cryptostore_integration_tests; async fn get_store(name: String, passphrase: Option<&str>) -> IndexeddbStore { match passphrase { diff --git a/crates/matrix-sdk-indexeddb/src/lib.rs b/crates/matrix-sdk-indexeddb/src/lib.rs new file mode 100644 index 00000000000..8ecfae46d08 --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/lib.rs @@ -0,0 +1,14 @@ +mod safe_encode; + +#[cfg(target_arch = "wasm32")] +mod state_store; + +#[cfg(target_arch = "wasm32")] +#[cfg(feature = "encryption")] +mod cryptostore; + +#[cfg(target_arch = "wasm32")] +#[cfg(feature = "encryption")] +pub use cryptostore::IndexeddbStore as CryptoStore; +#[cfg(target_arch = "wasm32")] +pub use state_store::IndexeddbStore as StateStore; diff --git a/crates/matrix-sdk-common/src/wasm_helpers.rs b/crates/matrix-sdk-indexeddb/src/safe_encode.rs similarity index 98% rename from crates/matrix-sdk-common/src/wasm_helpers.rs rename to crates/matrix-sdk-indexeddb/src/safe_encode.rs index d59cd5ffc05..d97781a7373 100644 --- a/crates/matrix-sdk-common/src/wasm_helpers.rs +++ b/crates/matrix-sdk-indexeddb/src/safe_encode.rs @@ -1,4 +1,5 @@ -use ruma::{ +#![allow(dead_code)] +use matrix_sdk_common::ruma::{ events::EventType, receipt::ReceiptType, DeviceId, EventId, MxcUri, RoomId, TransactionId, UserId, }; diff --git a/crates/matrix-sdk-base/src/store/indexeddb_store.rs b/crates/matrix-sdk-indexeddb/src/state_store.rs similarity index 85% rename from crates/matrix-sdk-base/src/store/indexeddb_store.rs rename to crates/matrix-sdk-indexeddb/src/state_store.rs index 293bd83ec94..0184054de6f 100644 --- a/crates/matrix-sdk-base/src/store/indexeddb_store.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store.rs @@ -14,32 +14,39 @@ use std::collections::BTreeSet; +use anyhow::anyhow; use futures_util::stream; use indexed_db_futures::prelude::*; -use matrix_sdk_common::{async_trait, SafeEncode}; -use ruma::{ - events::{ - presence::PresenceEvent, - receipt::Receipt, - room::member::{MembershipState, RoomMemberEventContent}, - AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncMessageEvent, AnySyncRoomEvent, - AnySyncStateEvent, EventType, +use matrix_sdk_base::{ + deserialized_responses::{MemberEvent, SyncRoomEvent}, + media::{MediaRequest, UniqueKey}, + store::{ + store_key::{self, EncryptedEvent, StoreKey}, + BoxStream, Result as StoreResult, StateChanges, StateStore, StoreError, + }, + RoomInfo, +}; +use matrix_sdk_common::{ + async_trait, + ruma::{ + events::{ + presence::PresenceEvent, + receipt::Receipt, + room::member::{MembershipState, RoomMemberEventContent}, + AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncMessageEvent, + AnySyncRoomEvent, AnySyncStateEvent, EventType, + }, + receipt::ReceiptType, + serde::Raw, + signatures::{redact_in_place, CanonicalJsonObject}, + EventId, MxcUri, RoomId, RoomVersionId, UserId, }, - receipt::ReceiptType, - serde::Raw, - signatures::{redact_in_place, CanonicalJsonObject}, - EventId, MxcUri, RoomId, RoomVersionId, UserId, }; use serde::{Deserialize, Serialize}; use tracing::{info, warn}; use wasm_bindgen::JsValue; -use self::store_key::{EncryptedEvent, StoreKey}; -use super::{store_key, BoxStream, Result, RoomInfo, StateChanges, StateStore, StoreError}; -use crate::{ - deserialized_responses::{MemberEvent, SyncRoomEvent}, - media::{MediaRequest, UniqueKey}, -}; +use crate::safe_encode::SafeEncode; #[derive(Debug, Serialize, Deserialize)] pub enum DatabaseType { @@ -53,11 +60,39 @@ pub enum SerializationError { Json(#[from] serde_json::Error), #[error(transparent)] Encryption(#[from] store_key::Error), + #[error("DomException {name} ({code}): {message}")] + DomException { name: String, message: String, code: u16 }, + #[error(transparent)] + StoreError(#[from] StoreError), +} + +impl From for SerializationError { + fn from(frm: indexed_db_futures::web_sys::DomException) -> SerializationError { + SerializationError::DomException { + name: frm.name(), + message: frm.message(), + code: frm.code(), + } + } +} + +impl From for StoreError { + fn from(e: SerializationError) -> Self { + match e { + SerializationError::Json(e) => StoreError::Json(e), + SerializationError::StoreError(e) => e, + SerializationError::Encryption(e) => match e { + store_key::Error::Random(e) => StoreError::Encryption(e.to_string()), + store_key::Error::Serialization(e) => StoreError::Json(e), + store_key::Error::Encryption(e) => StoreError::Encryption(e), + }, + _ => StoreError::Backend(anyhow!(e)), + } + } } #[allow(non_snake_case)] mod KEYS { - // STORES pub const SESSION: &str = "session"; @@ -96,19 +131,6 @@ mod KEYS { pub const SYNC_TOKEN: &str = "sync_token"; } -impl From for StoreError { - fn from(e: SerializationError) -> Self { - match e { - SerializationError::Json(e) => StoreError::Json(e), - SerializationError::Encryption(e) => match e { - store_key::Error::Random(e) => StoreError::Encryption(e.to_string()), - store_key::Error::Serialization(e) => StoreError::Json(e), - store_key::Error::Encryption(e) => StoreError::Encryption(e), - }, - } - } -} - pub struct IndexeddbStore { name: String, pub(crate) inner: IdbDatabase, @@ -121,6 +143,8 @@ impl std::fmt::Debug for IndexeddbStore { } } +type Result = std::result::Result; + impl IndexeddbStore { async fn open_helper(name: String, store_key: Option) -> Result { // Open my_db v1 @@ -167,12 +191,17 @@ impl IndexeddbStore { Ok(Self { name, inner: db, store_key }) } + #[allow(dead_code)] - pub async fn open() -> Result { - IndexeddbStore::open_helper("state".to_owned(), None).await + pub async fn open() -> StoreResult { + Ok(IndexeddbStore::open_helper("state".to_owned(), None).await?) } - pub async fn open_with_passphrase(name: String, passphrase: &str) -> Result { + pub async fn open_with_passphrase(name: String, passphrase: &str) -> StoreResult { + Ok(Self::inner_open_with_passphrase(name, passphrase).await?) + } + + async fn inner_open_with_passphrase(name: String, passphrase: &str) -> Result { let name = format!("{:0}::matrix-sdk-state", name); let mut db_req: OpenDbRequest = IdbDatabase::open_u32(&name, 1)?; @@ -202,12 +231,12 @@ impl IndexeddbStore { if let DatabaseType::Encrypted(k) = key { StoreKey::import(passphrase, k).map_err(|_| StoreError::StoreLocked)? } else { - return Err(StoreError::UnencryptedStore); + return Err(StoreError::UnencryptedStore.into()); } } else { - let key = StoreKey::new().map_err::(|e| e.into())?; + let key = StoreKey::new().map_err::(|e| e.into())?; let encrypted_key = DatabaseType::Encrypted( - key.export(passphrase).map_err::(|e| e.into())?, + key.export(passphrase).map_err::(|e| e.into())?, ); ob.put_key_val( &JsValue::from_str(KEYS::STORE_KEY), @@ -221,11 +250,14 @@ impl IndexeddbStore { IndexeddbStore::open_helper(name, Some(store_key)).await } - pub async fn open_with_name(name: String) -> Result { - IndexeddbStore::open_helper(name, None).await + pub async fn open_with_name(name: String) -> StoreResult { + Ok(IndexeddbStore::open_helper(name, None).await?) } - fn serialize_event(&self, event: &impl Serialize) -> Result { + fn serialize_event( + &self, + event: &impl Serialize, + ) -> std::result::Result { Ok(match self.store_key { Some(ref key) => JsValue::from_serde(&key.encrypt(event)?)?, None => JsValue::from_serde(event)?, @@ -235,7 +267,7 @@ impl IndexeddbStore { fn deserialize_event Deserialize<'b>>( &self, event: JsValue, - ) -> Result { + ) -> std::result::Result { match self.store_key { Some(ref key) => { let encrypted: EncryptedEvent = event.into_serde()?; @@ -266,19 +298,17 @@ impl IndexeddbStore { .object_store(KEYS::SESSION)? .get(&(KEYS::FILTER, filter_name).encode())? .await? - .map(|f| f.as_string()) - .flatten()) + .and_then(|f| f.as_string())) } pub async fn get_sync_token(&self) -> Result> { - Ok(self - .inner + self.inner .transaction_on_one_with_mode(KEYS::SYNC_TOKEN, IdbTransactionMode::Readonly)? .object_store(KEYS::SYNC_TOKEN)? .get(&JsValue::from_str(KEYS::SYNC_TOKEN))? .await? .map(|f| self.deserialize_event(f)) - .transpose()?) + .transpose() } pub async fn save_changes(&self, changes: &StateChanges) -> Result<()> { @@ -464,11 +494,10 @@ impl IndexeddbStore { for (user_id, receipt) in receipts { let key = (room, receipt_type, user_id).encode(); - if let Some((old_event, _)) = room_user_receipts - .get(&key)? - .await? - .map(|f| self.deserialize_event::<(Box, Receipt)>(f).ok()) - .flatten() + if let Some((old_event, _)) = + room_user_receipts.get(&key)?.await?.and_then(|f| { + self.deserialize_event::<(Box, Receipt)>(f).ok() + }) { room_event_receipts .delete(&(room, receipt_type, &old_event, user_id).encode())?; @@ -666,18 +695,17 @@ impl IndexeddbStore { } } - tx.await.into_result().map_err::(|e| e.into()) + tx.await.into_result().map_err::(|e| e.into()) } pub async fn get_presence_event(&self, user_id: &UserId) -> Result>> { - Ok(self - .inner + self.inner .transaction_on_one_with_mode(KEYS::PRESENCE, IdbTransactionMode::Readonly)? .object_store(KEYS::PRESENCE)? .get(&user_id.encode())? .await? .map(|f| self.deserialize_event(f)) - .transpose()?) + .transpose() } pub async fn get_state_event( @@ -686,14 +714,13 @@ impl IndexeddbStore { event_type: EventType, state_key: &str, ) -> Result>> { - Ok(self - .inner + self.inner .transaction_on_one_with_mode(KEYS::ROOM_STATE, IdbTransactionMode::Readonly)? .object_store(KEYS::ROOM_STATE)? .get(&(room_id, &event_type, state_key).encode())? .await? .map(|f| self.deserialize_event(f)) - .transpose()?) + .transpose() } pub async fn get_state_events( @@ -718,14 +745,13 @@ impl IndexeddbStore { room_id: &RoomId, user_id: &UserId, ) -> Result> { - Ok(self - .inner + self.inner .transaction_on_one_with_mode(KEYS::PROFILES, IdbTransactionMode::Readonly)? .object_store(KEYS::PROFILES)? .get(&(room_id, user_id).encode())? .await? .map(|f| self.deserialize_event(f)) - .transpose()?) + .transpose() } pub async fn get_member_event( @@ -733,14 +759,13 @@ impl IndexeddbStore { room_id: &RoomId, state_key: &UserId, ) -> Result> { - Ok(self - .inner + self.inner .transaction_on_one_with_mode(KEYS::MEMBERS, IdbTransactionMode::Readonly)? .object_store(KEYS::MEMBERS)? .get(&(room_id, state_key).encode())? .await? .map(|f| self.deserialize_event(f)) - .transpose()?) + .transpose() } pub async fn get_user_ids_stream(&self, room_id: &RoomId) -> Result>> { @@ -828,7 +853,7 @@ impl IndexeddbStore { .await? .map(|f| { self.deserialize_event::>>(f) - .map_err::(|e| e.into()) + .map_err::(|e| e) }) .unwrap_or_else(|| Ok(Default::default())) } @@ -837,14 +862,13 @@ impl IndexeddbStore { &self, event_type: EventType, ) -> Result>> { - Ok(self - .inner + self.inner .transaction_on_one_with_mode(KEYS::ACCOUNT_DATA, IdbTransactionMode::Readonly)? .object_store(KEYS::ACCOUNT_DATA)? .get(&JsValue::from_str(event_type.as_str()))? .await? - .map(|f| self.deserialize_event(f).map_err::(|e| e.into())) - .transpose()?) + .map(|f| self.deserialize_event(f).map_err::(|e| e)) + .transpose() } pub async fn get_room_account_data_event( @@ -852,14 +876,13 @@ impl IndexeddbStore { room_id: &RoomId, event_type: EventType, ) -> Result>> { - Ok(self - .inner + self.inner .transaction_on_one_with_mode(KEYS::ROOM_ACCOUNT_DATA, IdbTransactionMode::Readonly)? .object_store(KEYS::ROOM_ACCOUNT_DATA)? .get(&(room_id.as_str(), event_type.as_str()).encode())? .await? - .map(|f| self.deserialize_event(f).map_err::(|e| e.into())) - .transpose()?) + .map(|f| self.deserialize_event(f).map_err::(|e| e)) + .transpose() } async fn get_user_room_receipt_event( @@ -868,14 +891,13 @@ impl IndexeddbStore { receipt_type: ReceiptType, user_id: &UserId, ) -> Result, Receipt)>> { - Ok(self - .inner + self.inner .transaction_on_one_with_mode(KEYS::ROOM_USER_RECEIPTS, IdbTransactionMode::Readonly)? .object_store(KEYS::ROOM_USER_RECEIPTS)? .get(&(room_id.as_str(), receipt_type.as_ref(), user_id.as_str()).encode())? .await? .map(|f| self.deserialize_event(f)) - .transpose()?) + .transpose() } async fn get_event_room_receipt_events( @@ -904,7 +926,7 @@ impl IndexeddbStore { UserId::parse(&k_str[prefix_len..]) .map_err(|e| StoreError::Codec(format!("{:?}", e)))? } else { - return Err(StoreError::Codec(format!("{:?}", k))); + return Err(StoreError::Codec(format!("{:?}", k)).into()); }; let r = self .deserialize_event::(res) @@ -926,14 +948,13 @@ impl IndexeddbStore { async fn get_media_content(&self, request: &MediaRequest) -> Result>> { let key = (&request.media_type.unique_key(), &request.format.unique_key()).encode(); - Ok(self - .inner + self.inner .transaction_on_one_with_mode(KEYS::MEDIA, IdbTransactionMode::Readonly)? .object_store(KEYS::MEDIA)? .get(&key)? .await? .map(|f| self.deserialize_event(f)) - .transpose()?) + .transpose() } async fn get_custom_value(&self, key: &[u8]) -> Result>> { @@ -944,14 +965,13 @@ impl IndexeddbStore { } async fn get_custom_value_for_js(&self, jskey: &JsValue) -> Result>> { - Ok(self - .inner + self.inner .transaction_on_one_with_mode(KEYS::CUSTOM, IdbTransactionMode::Readonly)? .object_store(KEYS::CUSTOM)? .get(jskey)? .await? .map(|f| self.deserialize_event(f)) - .transpose()?) + .transpose() } async fn set_custom_value(&self, key: &[u8], value: Vec) -> Result>> { @@ -966,7 +986,7 @@ impl IndexeddbStore { tx.object_store(KEYS::CUSTOM)?.put_key_val(&jskey, &self.serialize_event(&value)?)?; - tx.await.into_result().map_err::(|e| e.into())?; + tx.await.into_result().map_err::(|e| e.into())?; Ok(prev) } @@ -1036,13 +1056,13 @@ impl IndexeddbStore { store.delete(&key)?; } } - tx.await.into_result().map_err::(|e| e.into()) + tx.await.into_result().map_err::(|e| e.into()) } async fn room_timeline( &self, room_id: &RoomId, - ) -> Result>, Option)>> { + ) -> Result>, Option)>> { let key = room_id.encode(); let tx = self.inner.transaction_on_multi_with_mode( &[KEYS::ROOM_TIMELINE, KEYS::ROOM_TIMELINE_METADATA], @@ -1059,7 +1079,7 @@ impl IndexeddbStore { } let end_token = metadata.and_then(|m| m.end); #[allow(clippy::needless_collect)] - let timeline: Vec> = timeline + let timeline: Vec> = timeline .get_all_with_key(&key)? .await? .iter() @@ -1076,24 +1096,27 @@ impl IndexeddbStore { #[async_trait(?Send)] impl StateStore for IndexeddbStore { - async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> { - self.save_filter(filter_name, filter_id).await + async fn save_filter(&self, filter_name: &str, filter_id: &str) -> StoreResult<()> { + self.save_filter(filter_name, filter_id).await.map_err(|e| e.into()) } - async fn save_changes(&self, changes: &StateChanges) -> Result<()> { - self.save_changes(changes).await + async fn save_changes(&self, changes: &StateChanges) -> StoreResult<()> { + self.save_changes(changes).await.map_err(|e| e.into()) } - async fn get_filter(&self, filter_id: &str) -> Result> { - self.get_filter(filter_id).await + async fn get_filter(&self, filter_id: &str) -> StoreResult> { + self.get_filter(filter_id).await.map_err(|e| e.into()) } - async fn get_sync_token(&self) -> Result> { - self.get_sync_token().await + async fn get_sync_token(&self) -> StoreResult> { + self.get_sync_token().await.map_err(|e| e.into()) } - async fn get_presence_event(&self, user_id: &UserId) -> Result>> { - self.get_presence_event(user_id).await + async fn get_presence_event( + &self, + user_id: &UserId, + ) -> StoreResult>> { + self.get_presence_event(user_id).await.map_err(|e| e.into()) } async fn get_state_event( @@ -1101,75 +1124,75 @@ impl StateStore for IndexeddbStore { room_id: &RoomId, event_type: EventType, state_key: &str, - ) -> Result>> { - self.get_state_event(room_id, event_type, state_key).await + ) -> StoreResult>> { + self.get_state_event(room_id, event_type, state_key).await.map_err(|e| e.into()) } async fn get_state_events( &self, room_id: &RoomId, event_type: EventType, - ) -> Result>> { - self.get_state_events(room_id, event_type).await + ) -> StoreResult>> { + self.get_state_events(room_id, event_type).await.map_err(|e| e.into()) } async fn get_profile( &self, room_id: &RoomId, user_id: &UserId, - ) -> Result> { - self.get_profile(room_id, user_id).await + ) -> StoreResult> { + self.get_profile(room_id, user_id).await.map_err(|e| e.into()) } async fn get_member_event( &self, room_id: &RoomId, state_key: &UserId, - ) -> Result> { - self.get_member_event(room_id, state_key).await + ) -> StoreResult> { + self.get_member_event(room_id, state_key).await.map_err(|e| e.into()) } - async fn get_user_ids(&self, room_id: &RoomId) -> Result>> { - self.get_user_ids_stream(room_id).await + async fn get_user_ids(&self, room_id: &RoomId) -> StoreResult>> { + self.get_user_ids_stream(room_id).await.map_err(|e| e.into()) } - async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result>> { - self.get_invited_user_ids(room_id).await + async fn get_invited_user_ids(&self, room_id: &RoomId) -> StoreResult>> { + self.get_invited_user_ids(room_id).await.map_err(|e| e.into()) } - async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result>> { - self.get_joined_user_ids(room_id).await + async fn get_joined_user_ids(&self, room_id: &RoomId) -> StoreResult>> { + self.get_joined_user_ids(room_id).await.map_err(|e| e.into()) } - async fn get_room_infos(&self) -> Result> { - self.get_room_infos().await + async fn get_room_infos(&self) -> StoreResult> { + self.get_room_infos().await.map_err(|e| e.into()) } - async fn get_stripped_room_infos(&self) -> Result> { - self.get_stripped_room_infos().await + async fn get_stripped_room_infos(&self) -> StoreResult> { + self.get_stripped_room_infos().await.map_err(|e| e.into()) } async fn get_users_with_display_name( &self, room_id: &RoomId, display_name: &str, - ) -> Result>> { - self.get_users_with_display_name(room_id, display_name).await + ) -> StoreResult>> { + self.get_users_with_display_name(room_id, display_name).await.map_err(|e| e.into()) } async fn get_account_data_event( &self, event_type: EventType, - ) -> Result>> { - self.get_account_data_event(event_type).await + ) -> StoreResult>> { + self.get_account_data_event(event_type).await.map_err(|e| e.into()) } async fn get_room_account_data_event( &self, room_id: &RoomId, event_type: EventType, - ) -> Result>> { - self.get_room_account_data_event(room_id, event_type).await + ) -> StoreResult>> { + self.get_room_account_data_event(room_id, event_type).await.map_err(|e| e.into()) } async fn get_user_room_receipt_event( @@ -1177,8 +1200,8 @@ impl StateStore for IndexeddbStore { room_id: &RoomId, receipt_type: ReceiptType, user_id: &UserId, - ) -> Result, Receipt)>> { - self.get_user_room_receipt_event(room_id, receipt_type, user_id).await + ) -> StoreResult, Receipt)>> { + self.get_user_room_receipt_event(room_id, receipt_type, user_id).await.map_err(|e| e.into()) } async fn get_event_room_receipt_events( @@ -1186,43 +1209,45 @@ impl StateStore for IndexeddbStore { room_id: &RoomId, receipt_type: ReceiptType, event_id: &EventId, - ) -> Result, Receipt)>> { - self.get_event_room_receipt_events(room_id, receipt_type, event_id).await + ) -> StoreResult, Receipt)>> { + self.get_event_room_receipt_events(room_id, receipt_type, event_id) + .await + .map_err(|e| e.into()) } - async fn get_custom_value(&self, key: &[u8]) -> Result>> { - self.get_custom_value(key).await + async fn get_custom_value(&self, key: &[u8]) -> StoreResult>> { + self.get_custom_value(key).await.map_err(|e| e.into()) } - async fn set_custom_value(&self, key: &[u8], value: Vec) -> Result>> { - self.set_custom_value(key, value).await + async fn set_custom_value(&self, key: &[u8], value: Vec) -> StoreResult>> { + self.set_custom_value(key, value).await.map_err(|e| e.into()) } - async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> Result<()> { - self.add_media_content(request, data).await + async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> StoreResult<()> { + self.add_media_content(request, data).await.map_err(|e| e.into()) } - async fn get_media_content(&self, request: &MediaRequest) -> Result>> { - self.get_media_content(request).await + async fn get_media_content(&self, request: &MediaRequest) -> StoreResult>> { + self.get_media_content(request).await.map_err(|e| e.into()) } - async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { - self.remove_media_content(request).await + async fn remove_media_content(&self, request: &MediaRequest) -> StoreResult<()> { + self.remove_media_content(request).await.map_err(|e| e.into()) } - async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> { - self.remove_media_content_for_uri(uri).await + async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> StoreResult<()> { + self.remove_media_content_for_uri(uri).await.map_err(|e| e.into()) } - async fn remove_room(&self, room_id: &RoomId) -> Result<()> { - self.remove_room(room_id).await + async fn remove_room(&self, room_id: &RoomId) -> StoreResult<()> { + self.remove_room(room_id).await.map_err(|e| e.into()) } async fn room_timeline( &self, room_id: &RoomId, - ) -> Result>, Option)>> { - self.room_timeline(room_id).await + ) -> StoreResult>, Option)>> { + self.room_timeline(room_id).await.map_err(|e| e.into()) } } @@ -1239,10 +1264,12 @@ mod test { #[cfg(target_arch = "wasm32")] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + use matrix_sdk_base::statestore_integration_tests; + use super::{IndexeddbStore, Result}; async fn get_store() -> Result { - IndexeddbStore::open().await + Ok(IndexeddbStore::open().await?) } statestore_integration_tests! { integration } diff --git a/crates/matrix-sdk-sled/Cargo.toml b/crates/matrix-sdk-sled/Cargo.toml new file mode 100644 index 00000000000..1802966d150 --- /dev/null +++ b/crates/matrix-sdk-sled/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "matrix-sdk-sled" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "state-inspector" +path = "bin/state_inspector.rs" +required-features = ["binary-build"] + +[features] +default = ["encryption"] + +encryption = ["matrix-sdk-crypto"] +binary-build = [ + "atty", + "clap", + "futures", + "rustyline", + "rustyline-derive", + "syntect", +] + +[dependencies] +futures-core = "0.3.15" +futures-util = { version = "0.3.15", default-features = false } +matrix-sdk-base = { path = "../matrix-sdk-base", features = ["store_key"] } +matrix-sdk-common = { path = "../matrix-sdk-common" } +matrix-sdk-crypto = { path = "../matrix-sdk-crypto", optional = true } +async-stream = "0.3.2" +serde = "1" +serde_json = "1.0.64" +sled = { version = "0.34.6" } +thiserror = "1.0.25" +tokio = { version = "1.7.1", default-features = false, features = ["sync", "fs"] } +tracing = "0.1.26" +anyhow = "1" +dashmap = "4.0.2" + +# binary-build only +atty = { version = "0.2.14", optional = true } +clap = { version = "3.1.0", optional = true } +futures = { version = "0.3.15", default-features = false, features = ["executor"], optional = true} +rustyline = { version = "9.0.0", optional = true } +rustyline-derive = { version = "0.6.0", optional = true } +syntect = { version = "4.5.0", optional = true } + +[dev-dependencies] +lazy_static = "1.4" +tempfile = "3.2.0" +matrix-sdk-test = { version = "0.4.0", path = "../matrix-sdk-test" } +matrix-sdk-crypto = { path = "../matrix-sdk-crypto", features = ["testing"] } +matrix-sdk-base = { path = "../matrix-sdk-base", features = ["testing"] } +tokio = { version = "1.7.1", default-features = false, features = [ + "rt-multi-thread", + "macros", +] } diff --git a/crates/matrix-sdk-base/examples/state_inspector.rs b/crates/matrix-sdk-sled/bin/state_inspector.rs similarity index 93% rename from crates/matrix-sdk-base/examples/state_inspector.rs rename to crates/matrix-sdk-sled/bin/state_inspector.rs index 0c801e8183f..f831cb09467 100644 --- a/crates/matrix-sdk-base/examples/state_inspector.rs +++ b/crates/matrix-sdk-sled/bin/state_inspector.rs @@ -1,13 +1,11 @@ use std::{convert::TryFrom, fmt::Debug, sync::Arc}; -#[cfg(not(target_arch = "wasm32"))] use atty::Stream; -#[cfg(not(target_arch = "wasm32"))] use clap::{Arg, ArgMatches, Command as Argparse}; use futures::executor::block_on; use matrix_sdk_base::{RoomInfo, Store}; -use ruma::{events::EventType, RoomId, UserId}; -#[cfg(not(target_arch = "wasm32"))] +use matrix_sdk_common::ruma::{events::EventType, RoomId, UserId}; +use matrix_sdk_sled::StateStore; use rustyline::{ completion::{Completer, Pair}, error::ReadlineError, @@ -16,10 +14,8 @@ use rustyline::{ validate::{MatchingBracketValidator, Validator}, CompletionType, Config, Context, EditMode, Editor, OutputStreamType, }; -#[cfg(not(target_arch = "wasm32"))] use rustyline_derive::Helper; use serde::Serialize; -#[cfg(not(target_arch = "wasm32"))] use syntect::{ dumps::from_binary, easy::HighlightLines, @@ -29,14 +25,12 @@ use syntect::{ }; #[derive(Clone)] -#[cfg(not(target_arch = "wasm32"))] struct Inspector { store: Store, printer: Printer, } #[derive(Helper)] -#[cfg(not(target_arch = "wasm32"))] struct InspectorHelper { store: Store, _highlighter: MatchingBracketHighlighter, @@ -44,7 +38,6 @@ struct InspectorHelper { _hinter: HistoryHinter, } -#[cfg(not(target_arch = "wasm32"))] impl InspectorHelper { const EVENT_TYPES: &'static [&'static str] = &[ "m.room.aliases", @@ -92,7 +85,6 @@ impl InspectorHelper { } } -#[cfg(not(target_arch = "wasm32"))] impl Completer for InspectorHelper { type Candidate = Pair; @@ -151,19 +143,15 @@ impl Completer for InspectorHelper { } } -#[cfg(not(target_arch = "wasm32"))] impl Hinter for InspectorHelper { type Hint = String; } -#[cfg(not(target_arch = "wasm32"))] impl Highlighter for InspectorHelper {} -#[cfg(not(target_arch = "wasm32"))] impl Validator for InspectorHelper {} #[derive(Clone, Debug)] -#[cfg(not(target_arch = "wasm32"))] struct Printer { ps: Arc, ts: Arc, @@ -171,7 +159,6 @@ struct Printer { color: bool, } -#[cfg(not(target_arch = "wasm32"))] impl Printer { fn new(json: bool, color: bool) -> Self { let syntax_set: SyntaxSet = from_binary(include_bytes!("./syntaxes.bin")); @@ -210,11 +197,12 @@ impl Printer { } } -#[cfg(not(target_arch = "wasm32"))] impl Inspector { fn new(database_path: &str, json: bool, color: bool) -> Self { let printer = Printer::new(json, color); - let (store, _) = Store::open_default(database_path, None).unwrap(); + let store = Store::new(Box::new( + StateStore::open_with_path(database_path).expect("Can't open sled database"), + )); Self { store, printer } } @@ -325,7 +313,6 @@ impl Inspector { } } -#[cfg(not(target_arch = "wasm32"))] fn main() { let argparse = Argparse::new("state-inspector") .disable_version_flag(true) @@ -368,8 +355,3 @@ fn main() { block_on(inspector.run(matches)); } } - -#[cfg(target_arch = "wasm32")] -fn main() { - panic!("This example doesn't run on WASM"); -} diff --git a/crates/matrix-sdk-base/examples/syntaxes.bin b/crates/matrix-sdk-sled/bin/syntaxes.bin similarity index 100% rename from crates/matrix-sdk-base/examples/syntaxes.bin rename to crates/matrix-sdk-sled/bin/syntaxes.bin diff --git a/crates/matrix-sdk-base/examples/themes.bin b/crates/matrix-sdk-sled/bin/themes.bin similarity index 100% rename from crates/matrix-sdk-base/examples/themes.bin rename to crates/matrix-sdk-sled/bin/themes.bin diff --git a/crates/matrix-sdk-crypto/src/store/sled.rs b/crates/matrix-sdk-sled/src/cryptostore.rs similarity index 82% rename from crates/matrix-sdk-crypto/src/store/sled.rs rename to crates/matrix-sdk-sled/src/cryptostore.rs index b7d3f286333..c3270ae76f0 100644 --- a/crates/matrix-sdk-crypto/src/store/sled.rs +++ b/crates/matrix-sdk-sled/src/cryptostore.rs @@ -19,13 +19,27 @@ use std::{ sync::{Arc, RwLock}, }; +use anyhow::anyhow; use dashmap::DashSet; -use matrix_sdk_common::{async_trait, locks::Mutex}; -use olm_rs::{account::IdentityKeys, PicklingMode}; -use ruma::{ - encryption::DeviceKeys, - events::{room_key_request::RequestedKeyInfo, secret::request::SecretName}, - DeviceId, DeviceKeyId, EventEncryptionAlgorithm, RoomId, TransactionId, UserId, +use matrix_sdk_common::{ + async_trait, + locks::Mutex, + ruma::{ + encryption::DeviceKeys, + events::{room_key_request::RequestedKeyInfo, secret::request::SecretName}, + DeviceId, DeviceKeyId, EventEncryptionAlgorithm, RoomId, TransactionId, UserId, + }, +}; +use matrix_sdk_crypto::{ + olm::{ + InboundGroupSession, OutboundGroupSession, PickledInboundGroupSession, + PrivateCrossSigningIdentity, Session, + }, + store::{ + caches::SessionStore, BackupKeys, Changes, CryptoStore, CryptoStoreError, IdentityKeys, + PickleKey, PicklingMode, Result, RoomKeyCounts, + }, + GossipRequest, LocalTrust, ReadOnlyAccount, ReadOnlyDevice, ReadOnlyUserIdentities, SecretInfo, }; use serde::{Deserialize, Serialize}; pub use sled::Error; @@ -35,17 +49,6 @@ use sled::{ }; use tracing::debug; -use super::{ - caches::SessionStore, BackupKeys, Changes, CryptoStore, CryptoStoreError, InboundGroupSession, - PickleKey, ReadOnlyAccount, Result, RoomKeyCounts, Session, -}; -use crate::{ - gossiping::{GossipRequest, SecretInfo}, - identities::{ReadOnlyDevice, ReadOnlyUserIdentities}, - olm::{OutboundGroupSession, PickledInboundGroupSession, PrivateCrossSigningIdentity}, - LocalTrust, -}; - /// This needs to be 32 bytes long since AES-GCM requires it, otherwise we will /// panic once we try to pickle a Signing object. const DEFAULT_PICKLE: &str = "DEFAULT_PICKLE_PASSPHRASE_123456"; @@ -204,28 +207,26 @@ impl std::fmt::Debug for SledStore { } } -impl From> for CryptoStoreError { - fn from(e: TransactionError) -> Self { - match e { - TransactionError::Abort(e) => CryptoStoreError::Serialization(e), - TransactionError::Storage(e) => CryptoStoreError::Database(e), - } - } -} - impl SledStore { /// Open the sled based cryptostore at the given path using the given /// passphrase to encrypt private data. - pub fn open_with_passphrase(path: impl AsRef, passphrase: Option<&str>) -> Result { + pub fn open_with_passphrase( + path: impl AsRef, + passphrase: Option<&str>, + ) -> Result { let path = path.as_ref().join("matrix-sdk-crypto"); - let db = Config::new().temporary(false).path(&path).open()?; + let db = Config::new() + .temporary(false) + .path(&path) + .open() + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; SledStore::open_helper(db, Some(path), passphrase) } /// Create a sled based cryptostore using the given sled database. /// The given passphrase will be used to encrypt private data. - pub fn open_with_database(db: Db, passphrase: Option<&str>) -> Result { + pub fn open_with_database(db: Db, passphrase: Option<&str>) -> Result { SledStore::open_helper(db, None, passphrase) } @@ -238,7 +239,7 @@ impl SledStore { .inbound_group_sessions .iter() .map(|p| { - let item = p?; + let item = p.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; Ok(( item.0, serde_json::from_slice(&item.1).map_err(CryptoStoreError::Serialization)?, @@ -262,9 +263,9 @@ impl SledStore { Ok(()) }); - ret?; + ret.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; - self.inner.flush_async().await?; + self.inner.flush_async().await.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; Ok(()) } @@ -272,7 +273,8 @@ impl SledStore { fn upgrade(&self) -> Result<()> { let version = self .inner - .get("store_version")? + .get("store_version") + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))? .map(|v| { let (version_bytes, _) = v.split_at(std::mem::size_of::()); u8::from_be_bytes(version_bytes.try_into().unwrap_or_default()) @@ -287,7 +289,9 @@ impl SledStore { // We changed the schema but migrating this isn't important since we // rotate the group sessions relatively often anyways so we just // clear the tree. - self.outbound_group_sessions.clear()?; + self.outbound_group_sessions + .clear() + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; } if version <= 1 { @@ -322,46 +326,64 @@ impl SledStore { let devices: Vec = self .devices .iter() - .map(|d| serde_json::from_slice(&d?.1).map_err(CryptoStoreError::Serialization)) + .map(|d| { + serde_json::from_slice(&d.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?.1) + .map_err(CryptoStoreError::Serialization) + }) .map(|d| { let d: OldReadOnlyDevice = d?; Ok(d.into()) }) .collect::, CryptoStoreError>>()?; - self.devices.transaction(move |tree| { - for device in &devices { - let key = device.encode(); - let device = - serde_json::to_vec(device).map_err(ConflictableTransactionError::Abort)?; - tree.insert(key, device)?; - } + self.devices + .transaction(move |tree| { + for device in &devices { + let key = device.encode(); + let device = serde_json::to_vec(device) + .map_err(ConflictableTransactionError::Abort)?; + tree.insert(key, device)?; + } - Ok(()) - })?; + Ok(()) + }) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; } if version <= 2 { // We're treating our own device now differently, we're checking if // the keys match to what we have locally, remove the unchecked // device and mark our own user as dirty. - if let Some(pickle) = self.account.get("account".encode())? { + if let Some(pickle) = self + .account + .get("account".encode()) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))? + { let pickle = serde_json::from_slice(&pickle)?; let account = ReadOnlyAccount::from_pickle(pickle, self.get_pickle_mode())?; self.devices - .remove((account.user_id().as_str(), account.device_id.as_str()).encode())?; - self.tracked_users.insert(account.user_id().as_str(), &[true as u8])?; + .remove((account.user_id().as_str(), account.device_id.as_str()).encode()) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; + self.tracked_users + .insert(account.user_id().as_str(), &[true as u8]) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; } } - self.inner.insert("store_version", DATABASE_VERSION.to_be_bytes().as_ref())?; - self.inner.flush()?; + self.inner + .insert("store_version", DATABASE_VERSION.to_be_bytes().as_ref()) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; + self.inner.flush().map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; Ok(()) } - fn open_helper(db: Db, path: Option, passphrase: Option<&str>) -> Result { + fn open_helper( + db: Db, + path: Option, + passphrase: Option<&str>, + ) -> Result { let account = db.open_tree("account")?; let private_identity = db.open_tree("private_identity")?; @@ -417,15 +439,19 @@ impl SledStore { } fn get_or_create_pickle_key(passphrase: &str, database: &Db) -> Result { - let key = if let Some(key) = - database.get("pickle_key".encode())?.map(|v| serde_json::from_slice(&v)) + let key = if let Some(key) = database + .get("pickle_key".encode()) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))? + .map(|v| serde_json::from_slice(&v)) { PickleKey::from_encrypted(passphrase, key?) .map_err(|_| CryptoStoreError::UnpicklingError)? } else { let key = PickleKey::new(); let encrypted = key.encrypt(passphrase); - database.insert("pickle_key".encode(), serde_json::to_vec(&encrypted)?)?; + database + .insert("pickle_key".encode(), serde_json::to_vec(&encrypted)?) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; key }; @@ -442,7 +468,7 @@ impl SledStore { async fn load_tracked_users(&self) -> Result<()> { for value in &self.tracked_users { - let (user, dirty) = value?; + let (user, dirty) = value.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; let user = UserId::parse(String::from_utf8_lossy(&user).to_string())?; let dirty = dirty.get(0).map(|d| *d == 1).unwrap_or(true); @@ -463,7 +489,8 @@ impl SledStore { let account_info = self.get_account_info().ok_or(CryptoStoreError::AccountUnset)?; self.outbound_group_sessions - .get(room_id.encode())? + .get(room_id.encode()) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))? .map(|p| serde_json::from_slice(&p).map_err(CryptoStoreError::Serialization)) .transpose()? .map(|p| { @@ -671,8 +698,8 @@ impl SledStore { }, ); - ret?; - self.inner.flush_async().await?; + ret.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; + self.inner.flush_async().await.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; Ok(()) } @@ -680,12 +707,17 @@ impl SledStore { async fn get_outgoing_key_request_helper(&self, id: &[u8]) -> Result> { let request = self .outgoing_secret_requests - .get(id)? + .get(id) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))? .map(|r| serde_json::from_slice(&r)) .transpose()?; let request = if request.is_none() { - self.unsent_secret_requests.get(id)?.map(|r| serde_json::from_slice(&r)).transpose()? + self.unsent_secret_requests + .get(id) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))? + .map(|r| serde_json::from_slice(&r)) + .transpose()? } else { request }; @@ -697,7 +729,11 @@ impl SledStore { #[async_trait] impl CryptoStore for SledStore { async fn load_account(&self) -> Result> { - if let Some(pickle) = self.account.get("account".encode())? { + if let Some(pickle) = self + .account + .get("account".encode()) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))? + { let pickle = serde_json::from_slice(&pickle)?; self.load_tracked_users().await?; @@ -733,7 +769,11 @@ impl CryptoStore for SledStore { } async fn load_identity(&self) -> Result> { - if let Some(i) = self.private_identity.get("identity".encode())? { + if let Some(i) = self + .private_identity + .get("identity".encode()) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))? + { let pickle = serde_json::from_slice(&i)?; Ok(Some( PrivateCrossSigningIdentity::from_pickle(pickle, self.get_pickle_key()) @@ -756,7 +796,10 @@ impl CryptoStore for SledStore { let sessions: Result> = self .sessions .scan_prefix(sender_key.encode()) - .map(|s| serde_json::from_slice(&s?.1).map_err(CryptoStoreError::Serialization)) + .map(|s| { + serde_json::from_slice(&s.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?.1) + .map_err(CryptoStoreError::Serialization) + }) .map(|p| { Session::from_pickle( account_info.user_id.clone(), @@ -782,7 +825,11 @@ impl CryptoStore for SledStore { session_id: &str, ) -> Result> { let key = (room_id.as_str(), sender_key, session_id).encode(); - let pickle = self.inbound_group_sessions.get(&key)?.map(|p| serde_json::from_slice(&p)); + let pickle = self + .inbound_group_sessions + .get(&key) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))? + .map(|p| serde_json::from_slice(&p)); if let Some(pickle) = pickle { Ok(Some(InboundGroupSession::from_pickle(pickle?, self.get_pickle_mode())?)) @@ -795,7 +842,10 @@ impl CryptoStore for SledStore { let pickles: Result> = self .inbound_group_sessions .iter() - .map(|p| serde_json::from_slice(&p?.1).map_err(CryptoStoreError::Serialization)) + .map(|p| { + serde_json::from_slice(&p.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?.1) + .map_err(CryptoStoreError::Serialization) + }) .collect(); Ok(pickles? @@ -809,7 +859,7 @@ impl CryptoStore for SledStore { .inbound_group_sessions .iter() .map(|p| { - let item = p?; + let item = p.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; serde_json::from_slice(&item.1).map_err(CryptoStoreError::Serialization) }) .collect::>()?; @@ -828,7 +878,7 @@ impl CryptoStore for SledStore { .inbound_group_sessions .iter() .map(|p| { - let item = p?; + let item = p.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; serde_json::from_slice(&item.1).map_err(CryptoStoreError::from) }) .filter_map(|p: Result| match p { @@ -887,7 +937,9 @@ impl CryptoStore for SledStore { self.users_for_key_query_cache.remove(user); } - self.tracked_users.insert(user.as_str(), &[dirty as u8])?; + self.tracked_users + .insert(user.as_str(), &[dirty as u8]) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; Ok(already_added) } @@ -898,7 +950,12 @@ impl CryptoStore for SledStore { device_id: &DeviceId, ) -> Result> { let key = (user_id.as_str(), device_id.as_str()).encode(); - Ok(self.devices.get(key)?.map(|d| serde_json::from_slice(&d)).transpose()?) + Ok(self + .devices + .get(key) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))? + .map(|d| serde_json::from_slice(&d)) + .transpose()?) } async fn get_user_devices( @@ -907,7 +964,10 @@ impl CryptoStore for SledStore { ) -> Result, ReadOnlyDevice>> { self.devices .scan_prefix(user_id.encode()) - .map(|d| serde_json::from_slice(&d?.1).map_err(CryptoStoreError::Serialization)) + .map(|d| { + serde_json::from_slice(&d.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?.1) + .map_err(CryptoStoreError::Serialization) + }) .map(|d| { let d: ReadOnlyDevice = d?; Ok((d.device_id().to_owned(), d)) @@ -918,13 +978,20 @@ impl CryptoStore for SledStore { async fn get_user_identity(&self, user_id: &UserId) -> Result> { Ok(self .identities - .get(user_id.encode())? + .get(user_id.encode()) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))? .map(|i| serde_json::from_slice(&i)) .transpose()?) } - async fn is_message_known(&self, message_hash: &crate::olm::OlmMessageHash) -> Result { - Ok(self.olm_hashes.contains_key(serde_json::to_vec(message_hash)?)?) + async fn is_message_known( + &self, + message_hash: &matrix_sdk_crypto::olm::OlmMessageHash, + ) -> Result { + Ok(self + .olm_hashes + .contains_key(serde_json::to_vec(message_hash)?) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?) } async fn get_outgoing_secret_requests( @@ -940,7 +1007,10 @@ impl CryptoStore for SledStore { &self, key_info: &SecretInfo, ) -> Result> { - let id = self.secret_requests_by_info.get(key_info.encode())?; + let id = self + .secret_requests_by_info + .get(key_info.encode()) + .map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; if let Some(id) = id { self.get_outgoing_key_request_helper(&id).await @@ -953,7 +1023,10 @@ impl CryptoStore for SledStore { let requests: Result> = self .unsent_secret_requests .iter() - .map(|i| serde_json::from_slice(&i?.1).map_err(CryptoStoreError::from)) + .map(|i| { + serde_json::from_slice(&i.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?.1) + .map_err(CryptoStoreError::from) + }) .collect(); requests @@ -991,8 +1064,8 @@ impl CryptoStore for SledStore { }, ); - ret?; - self.inner.flush_async().await?; + ret.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; + self.inner.flush_async().await.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?; Ok(()) } @@ -1031,6 +1104,7 @@ impl CryptoStore for SledStore { #[cfg(test)] mod test { use lazy_static::lazy_static; + use matrix_sdk_crypto::cryptostore_integration_tests; use tempfile::{tempdir, TempDir}; use super::SledStore; diff --git a/crates/matrix-sdk-sled/src/lib.rs b/crates/matrix-sdk-sled/src/lib.rs new file mode 100644 index 00000000000..8b97ccbe4ed --- /dev/null +++ b/crates/matrix-sdk-sled/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(feature = "encryption")] +mod cryptostore; +mod state_store; + +#[cfg(feature = "encryption")] +pub use cryptostore::SledStore as CryptoStore; +pub use state_store::SledStore as StateStore; diff --git a/crates/matrix-sdk-base/src/store/sled_store.rs b/crates/matrix-sdk-sled/src/state_store.rs similarity index 83% rename from crates/matrix-sdk-base/src/store/sled_store.rs rename to crates/matrix-sdk-sled/src/state_store.rs index 8acfd6f638e..14d60ab1b6f 100644 --- a/crates/matrix-sdk-base/src/store/sled_store.rs +++ b/crates/matrix-sdk-sled/src/state_store.rs @@ -20,22 +20,35 @@ use std::{ time::Instant, }; +use anyhow::anyhow; use async_stream::stream; use futures_core::stream::Stream; use futures_util::stream::{self, TryStreamExt}; -use matrix_sdk_common::async_trait; -use ruma::{ - events::{ - presence::PresenceEvent, - receipt::Receipt, - room::member::{MembershipState, RoomMemberEventContent}, - AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncMessageEvent, AnySyncRoomEvent, - AnySyncStateEvent, EventType, +use matrix_sdk_base::{ + deserialized_responses::{MemberEvent, SyncRoomEvent}, + media::{MediaRequest, UniqueKey}, + store::{ + store_key::{self, EncryptedEvent, StoreKey}, + BoxStream, Result as StoreResult, StateChanges, StateStore, StoreError, + }, + RoomInfo, +}; +use matrix_sdk_common::{ + async_trait, + ruma::{ + self, + events::{ + presence::PresenceEvent, + receipt::Receipt, + room::member::{MembershipState, RoomMemberEventContent}, + AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncMessageEvent, + AnySyncRoomEvent, AnySyncStateEvent, EventType, + }, + receipt::ReceiptType, + serde::Raw, + signatures::{redact_in_place, CanonicalJsonObject}, + EventId, MxcUri, RoomId, RoomVersionId, UserId, }, - receipt::ReceiptType, - serde::Raw, - signatures::{redact_in_place, CanonicalJsonObject}, - EventId, MxcUri, RoomId, RoomVersionId, UserId, }; use serde::{Deserialize, Serialize}; use sled::{ @@ -45,13 +58,6 @@ use sled::{ use tokio::task::spawn_blocking; use tracing::{info, warn}; -use self::store_key::{EncryptedEvent, StoreKey}; -use super::{store_key, BoxStream, Result, RoomInfo, StateChanges, StateStore, StoreError}; -use crate::{ - deserialized_responses::{MemberEvent, SyncRoomEvent}, - media::{MediaRequest, UniqueKey}, -}; - #[derive(Debug, Serialize, Deserialize)] pub enum DatabaseType { Unencrypted, @@ -59,35 +65,49 @@ pub enum DatabaseType { } #[derive(Debug, thiserror::Error)] -pub enum SerializationError { +pub enum SledStoreError { #[error(transparent)] Json(#[from] serde_json::Error), #[error(transparent)] Encryption(#[from] store_key::Error), + #[error(transparent)] + StoreError(#[from] StoreError), + #[error(transparent)] + TransactionError(#[from] sled::Error), + #[error(transparent)] + Identifier(#[from] ruma::identifiers::Error), + #[error(transparent)] + Task(#[from] tokio::task::JoinError), } -impl From> for StoreError { - fn from(e: TransactionError) -> Self { +impl From> for SledStoreError { + fn from(e: TransactionError) -> Self { match e { - TransactionError::Abort(e) => e.into(), - TransactionError::Storage(e) => StoreError::Sled(e), + TransactionError::Abort(e) => e, + TransactionError::Storage(e) => SledStoreError::TransactionError(e), } } } -impl From for StoreError { - fn from(e: SerializationError) -> Self { - match e { - SerializationError::Json(e) => StoreError::Json(e), - SerializationError::Encryption(e) => match e { +#[allow(clippy::from_over_into)] +impl Into for SledStoreError { + fn into(self) -> StoreError { + match self { + SledStoreError::Json(e) => StoreError::Json(e), + SledStoreError::Identifier(e) => StoreError::Identifier(e), + SledStoreError::Encryption(e) => match e { store_key::Error::Random(e) => StoreError::Encryption(e.to_string()), store_key::Error::Serialization(e) => StoreError::Json(e), store_key::Error::Encryption(e) => StoreError::Encryption(e), }, + SledStoreError::StoreError(e) => e, + _ => StoreError::Backend(anyhow!(self)), } } } +type Result = std::result::Result; + const ENCODE_SEPARATOR: u8 = 0xff; trait EncodeKey { @@ -295,13 +315,18 @@ impl SledStore { }) } - pub fn open() -> Result { - let db = Config::new().temporary(true).open()?; + pub fn open() -> StoreResult { + let db = + Config::new().temporary(true).open().map_err(|e| StoreError::Backend(anyhow!(e)))?; + + SledStore::open_helper(db, None, None).map_err(|e| e.into()) + } - SledStore::open_helper(db, None, None) + pub fn open_with_passphrase(path: impl AsRef, passphrase: &str) -> StoreResult { + Self::inner_open_with_passphrase(path, passphrase).map_err(|e| e.into()) } - pub fn open_with_passphrase(path: impl AsRef, passphrase: &str) -> Result { + fn inner_open_with_passphrase(path: impl AsRef, passphrase: &str) -> Result { let path = path.as_ref().join("matrix-sdk-state"); let db = Config::new().temporary(false).path(&path).open()?; @@ -314,7 +339,7 @@ impl SledStore { if let DatabaseType::Encrypted(k) = key { StoreKey::import(passphrase, k).map_err(|_| StoreError::StoreLocked)? } else { - return Err(StoreError::UnencryptedStore); + return Err(SledStoreError::StoreError(StoreError::UnencryptedStore)); } } else { let key = StoreKey::new().map_err::(|e| e.into())?; @@ -328,14 +353,18 @@ impl SledStore { SledStore::open_helper(db, Some(path), Some(store_key)) } - pub fn open_with_path(path: impl AsRef) -> Result { + pub fn open_with_path(path: impl AsRef) -> StoreResult { + Self::inner_open_with_path(path).map_err(|e| e.into()) + } + + fn inner_open_with_path(path: impl AsRef) -> Result { let path = path.as_ref().join("matrix-sdk-state"); let db = Config::new().temporary(false).path(&path).open()?; SledStore::open_helper(db, Some(path), None) } - fn serialize_event(&self, event: &impl Serialize) -> Result, SerializationError> { + fn serialize_event(&self, event: &impl Serialize) -> Result, SledStoreError> { if let Some(key) = &*self.store_key { let encrypted = key.encrypt(event)?; Ok(serde_json::to_vec(&encrypted)?) @@ -347,7 +376,7 @@ impl SledStore { fn deserialize_event Deserialize<'b>>( &self, event: &[u8], - ) -> Result { + ) -> Result { if let Some(key) = &*self.store_key { let encrypted: EncryptedEvent = serde_json::from_slice(event)?; Ok(key.decrypt(encrypted)?) @@ -379,7 +408,7 @@ impl SledStore { pub async fn save_changes(&self, changes: &StateChanges) -> Result<()> { let now = Instant::now(); - let ret: Result<(), TransactionError> = ( + let ret: Result<(), TransactionError> = ( &self.session, &self.account_data, &self.members, @@ -549,7 +578,7 @@ impl SledStore { ret?; - let ret: Result<(), TransactionError> = + let ret: Result<(), TransactionError> = (&self.room_user_receipts, &self.room_event_receipts).transaction( |(room_user_receipts, room_event_receipts)| { for (room, content) in &changes.receipts { @@ -613,10 +642,8 @@ impl SledStore { pub async fn get_presence_event(&self, user_id: &UserId) -> Result>> { let db = self.clone(); let key = user_id.encode(); - spawn_blocking(move || { - Ok(db.presence.get(key)?.map(|e| db.deserialize_event(&e)).transpose()?) - }) - .await? + spawn_blocking(move || db.presence.get(key)?.map(|e| db.deserialize_event(&e)).transpose()) + .await? } pub async fn get_state_event( @@ -628,7 +655,7 @@ impl SledStore { let db = self.clone(); let key = (room_id.as_str(), event_type.as_str(), state_key).encode(); spawn_blocking(move || { - Ok(db.room_state.get(key)?.map(|e| db.deserialize_event(&e)).transpose()?) + db.room_state.get(key)?.map(|e| db.deserialize_event(&e)).transpose() }) .await? } @@ -641,11 +668,10 @@ impl SledStore { let db = self.clone(); let key = (room_id.as_str(), event_type.as_str()).encode(); spawn_blocking(move || { - Ok(db - .room_state + db.room_state .scan_prefix(key) .flat_map(|e| e.map(|(_, e)| db.deserialize_event(&e))) - .collect::>()?) + .collect::>() }) .await? } @@ -657,10 +683,8 @@ impl SledStore { ) -> Result> { let db = self.clone(); let key = (room_id.as_str(), user_id.as_str()).encode(); - spawn_blocking(move || { - Ok(db.profiles.get(key)?.map(|p| db.deserialize_event(&p)).transpose()?) - }) - .await? + spawn_blocking(move || db.profiles.get(key)?.map(|p| db.deserialize_event(&p)).transpose()) + .await? } pub async fn get_member_event( @@ -670,17 +694,15 @@ impl SledStore { ) -> Result> { let db = self.clone(); let key = (room_id.as_str(), state_key.as_str()).encode(); - spawn_blocking(move || { - Ok(db.members.get(key)?.map(|v| db.deserialize_event(&v)).transpose()?) - }) - .await? + spawn_blocking(move || db.members.get(key)?.map(|v| db.deserialize_event(&v)).transpose()) + .await? } pub async fn get_user_ids_stream( &self, room_id: &RoomId, - ) -> Result>>> { - let decode = |key: &[u8]| -> Result> { + ) -> StoreResult>>> { + let decode = |key: &[u8]| -> StoreResult> { let mut iter = key.split(|c| c == &ENCODE_SEPARATOR); // Our key is a the room id separated from the user id by a null // byte, discard the first value of the split. @@ -694,48 +716,60 @@ impl SledStore { let members = self.members.clone(); let key = room_id.encode(); - spawn_blocking(move || stream::iter(members.scan_prefix(key).map(move |u| decode(&u?.0)))) - .await - .map_err(Into::into) + spawn_blocking(move || { + stream::iter( + members + .scan_prefix(key) + .map(move |u| decode(&u.map_err(|e| StoreError::Backend(anyhow!(e)))?.0)), + ) + }) + .await + .map_err(|e| StoreError::Backend(anyhow!(e))) } pub async fn get_invited_user_ids( &self, room_id: &RoomId, - ) -> Result>>> { + ) -> StoreResult>>> { let db = self.clone(); let key = room_id.encode(); spawn_blocking(move || { stream::iter(db.invited_user_ids.scan_prefix(key).map(|u| { - UserId::parse(String::from_utf8_lossy(&u?.1).to_string()) - .map_err(StoreError::Identifier) + UserId::parse( + String::from_utf8_lossy(&u.map_err(|e| StoreError::Backend(anyhow!(e)))?.1) + .to_string(), + ) + .map_err(StoreError::Identifier) })) }) .await - .map_err(Into::into) + .map_err(|e| StoreError::Backend(anyhow!(e))) } pub async fn get_joined_user_ids( &self, room_id: &RoomId, - ) -> Result>>> { + ) -> StoreResult>>> { let db = self.clone(); let key = room_id.encode(); spawn_blocking(move || { stream::iter(db.joined_user_ids.scan_prefix(key).map(|u| { - UserId::parse(String::from_utf8_lossy(&u?.1).to_string()) - .map_err(StoreError::Identifier) + UserId::parse( + String::from_utf8_lossy(&u.map_err(|e| StoreError::Backend(anyhow!(e)))?.1) + .to_string(), + ) + .map_err(StoreError::Identifier) })) }) .await - .map_err(Into::into) + .map_err(|e| StoreError::Backend(anyhow!(e))) } pub async fn get_room_infos(&self) -> Result>> { let db = self.clone(); spawn_blocking(move || { stream::iter( - db.room_info.iter().map(move |r| db.deserialize_event(&r?.1).map_err(|e| e.into())), + db.room_info.iter().map(move |r| db.deserialize_event(&r?.1).map_err(|e| e)), ) }) .await @@ -748,7 +782,7 @@ impl SledStore { stream::iter( db.stripped_room_infos .iter() - .map(move |r| db.deserialize_event(&r?.1).map_err(|e| e.into())), + .map(move |r| db.deserialize_event(&r?.1).map_err(|e| e)), ) }) .await @@ -780,7 +814,7 @@ impl SledStore { let db = self.clone(); let key = event_type.encode(); spawn_blocking(move || { - Ok(db.account_data.get(key)?.map(|m| db.deserialize_event(&m)).transpose()?) + db.account_data.get(key)?.map(|m| db.deserialize_event(&m)).transpose() }) .await? } @@ -793,7 +827,7 @@ impl SledStore { let db = self.clone(); let key = (room_id.as_str(), event_type.as_str()).encode(); spawn_blocking(move || { - Ok(db.room_account_data.get(key)?.map(|m| db.deserialize_event(&m)).transpose()?) + db.room_account_data.get(key)?.map(|m| db.deserialize_event(&m)).transpose() }) .await? } @@ -807,7 +841,7 @@ impl SledStore { let db = self.clone(); let key = (room_id.as_str(), receipt_type.as_ref(), user_id.as_str()).encode(); spawn_blocking(move || { - Ok(db.room_user_receipts.get(key)?.map(|m| db.deserialize_event(&m)).transpose()?) + db.room_user_receipts.get(key)?.map(|m| db.deserialize_event(&m)).transpose() }) .await? } @@ -817,25 +851,26 @@ impl SledStore { room_id: &RoomId, receipt_type: ReceiptType, event_id: &EventId, - ) -> Result, Receipt)>> { + ) -> StoreResult, Receipt)>> { let db = self.clone(); let key = (room_id.as_str(), receipt_type.as_ref(), event_id.as_str()).encode(); spawn_blocking(move || { db.room_event_receipts .scan_prefix(key) .map(|u| { - u.map_err(StoreError::Sled).and_then(|(key, value)| { + u.map_err(|e| StoreError::Backend(anyhow!(e))).and_then(|(key, value)| { db.deserialize_event(&value) // TODO remove this unwrapping .map(|receipt| { (decode_key_value(&key, 3).unwrap().try_into().unwrap(), receipt) }) - .map_err(Into::into) + .map_err(|e| StoreError::Backend(anyhow!(e))) }) }) .collect() }) - .await? + .await + .map_err(|e| StoreError::Backend(anyhow!(e)))? } async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> Result<()> { @@ -949,7 +984,7 @@ impl SledStore { room_event_receipts_batch.remove(key?) } - let ret: Result<(), TransactionError> = ( + let ret: Result<(), TransactionError> = ( &self.members, &self.profiles, &self.display_names, @@ -1011,7 +1046,7 @@ impl SledStore { async fn room_timeline( &self, room_id: &RoomId, - ) -> Result>, Option)>> { + ) -> Result>, Option)>> { let db = self.clone(); let key = room_id.encode(); let r_id = room_id.to_owned(); @@ -1036,7 +1071,7 @@ impl SledStore { let stream = stream! { while let Ok(Some(item)) = db.room_timeline.get(&(r_id.as_ref(), position).encode()) { position += 1; - yield db.deserialize_event(&item).map_err(|e| e.into()); + yield db.deserialize_event(&item).map_err(SledStoreError::from).map_err(|e| e.into()); } }; @@ -1057,7 +1092,7 @@ impl SledStore { event_id_to_position_batch.remove(key?) } - let ret: Result<(), TransactionError> = + let ret: Result<(), TransactionError> = (&self.room_timeline, &self.room_timeline_metadata, &self.room_event_id_to_position) .transaction( |(room_timeline, room_timeline_metadata, room_event_id_to_position)| { @@ -1178,7 +1213,7 @@ impl SledStore { .get(position_key.as_ref())? .map(|e| { self.deserialize_event::(&e) - .map_err(StoreError::from) + .map_err(SledStoreError::from) }) .transpose()? { @@ -1218,7 +1253,7 @@ impl SledStore { timeline_metadata_batch.insert(room_key, serde_json::to_vec(&metadata)?); } - let ret: Result<(), TransactionError> = + let ret: Result<(), TransactionError> = (&self.room_timeline, &self.room_timeline_metadata, &self.room_event_id_to_position) .transaction( |(room_timeline, room_timeline_metadata, room_event_id_to_position)| { @@ -1239,24 +1274,27 @@ impl SledStore { #[async_trait] impl StateStore for SledStore { - async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> { - self.save_filter(filter_name, filter_id).await + async fn save_filter(&self, filter_name: &str, filter_id: &str) -> StoreResult<()> { + self.save_filter(filter_name, filter_id).await.map_err(Into::into) } - async fn save_changes(&self, changes: &StateChanges) -> Result<()> { - self.save_changes(changes).await + async fn save_changes(&self, changes: &StateChanges) -> StoreResult<()> { + self.save_changes(changes).await.map_err(Into::into) } - async fn get_filter(&self, filter_id: &str) -> Result> { - self.get_filter(filter_id).await + async fn get_filter(&self, filter_id: &str) -> StoreResult> { + self.get_filter(filter_id).await.map_err(Into::into) } - async fn get_sync_token(&self) -> Result> { - self.get_sync_token().await + async fn get_sync_token(&self) -> StoreResult> { + self.get_sync_token().await.map_err(Into::into) } - async fn get_presence_event(&self, user_id: &UserId) -> Result>> { - self.get_presence_event(user_id).await + async fn get_presence_event( + &self, + user_id: &UserId, + ) -> StoreResult>> { + self.get_presence_event(user_id).await.map_err(Into::into) } async fn get_state_event( @@ -1264,75 +1302,85 @@ impl StateStore for SledStore { room_id: &RoomId, event_type: EventType, state_key: &str, - ) -> Result>> { - self.get_state_event(room_id, event_type, state_key).await + ) -> StoreResult>> { + self.get_state_event(room_id, event_type, state_key).await.map_err(Into::into) } async fn get_state_events( &self, room_id: &RoomId, event_type: EventType, - ) -> Result>> { - self.get_state_events(room_id, event_type).await + ) -> StoreResult>> { + self.get_state_events(room_id, event_type).await.map_err(Into::into) } async fn get_profile( &self, room_id: &RoomId, user_id: &UserId, - ) -> Result> { - self.get_profile(room_id, user_id).await + ) -> StoreResult> { + self.get_profile(room_id, user_id).await.map_err(Into::into) } async fn get_member_event( &self, room_id: &RoomId, state_key: &UserId, - ) -> Result> { - self.get_member_event(room_id, state_key).await + ) -> StoreResult> { + self.get_member_event(room_id, state_key).await.map_err(Into::into) } - async fn get_user_ids(&self, room_id: &RoomId) -> Result>> { + async fn get_user_ids(&self, room_id: &RoomId) -> StoreResult>> { self.get_user_ids_stream(room_id).await?.try_collect().await } - async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result>> { + async fn get_invited_user_ids(&self, room_id: &RoomId) -> StoreResult>> { self.get_invited_user_ids(room_id).await?.try_collect().await } - async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result>> { + async fn get_joined_user_ids(&self, room_id: &RoomId) -> StoreResult>> { self.get_joined_user_ids(room_id).await?.try_collect().await } - async fn get_room_infos(&self) -> Result> { - self.get_room_infos().await?.try_collect().await + async fn get_room_infos(&self) -> StoreResult> { + self.get_room_infos() + .await + .map_err::(Into::into)? + .try_collect() + .await + .map_err::(Into::into) } - async fn get_stripped_room_infos(&self) -> Result> { - self.get_stripped_room_infos().await?.try_collect().await + async fn get_stripped_room_infos(&self) -> StoreResult> { + self.get_stripped_room_infos() + .await + .map_err::(Into::into)? + .try_collect() + .await + .map_err::(Into::into) } async fn get_users_with_display_name( &self, room_id: &RoomId, display_name: &str, - ) -> Result>> { - self.get_users_with_display_name(room_id, display_name).await + ) -> StoreResult>> { + self.get_users_with_display_name(room_id, display_name).await.map_err(Into::into) } async fn get_account_data_event( &self, event_type: EventType, - ) -> Result>> { - self.get_account_data_event(event_type).await + ) -> StoreResult>> { + self.get_account_data_event(event_type).await.map_err(Into::into) } async fn get_room_account_data_event( &self, room_id: &RoomId, event_type: EventType, - ) -> Result>> { - self.get_room_account_data_event(room_id, event_type).await + ) -> StoreResult>> { + self.get_room_account_data_event(room_id, event_type).await.map_err(Into::into) } async fn get_user_room_receipt_event( @@ -1340,8 +1388,8 @@ impl StateStore for SledStore { room_id: &RoomId, receipt_type: ReceiptType, user_id: &UserId, - ) -> Result, Receipt)>> { - self.get_user_room_receipt_event(room_id, receipt_type, user_id).await + ) -> StoreResult, Receipt)>> { + self.get_user_room_receipt_event(room_id, receipt_type, user_id).await.map_err(Into::into) } async fn get_event_room_receipt_events( @@ -1349,43 +1397,45 @@ impl StateStore for SledStore { room_id: &RoomId, receipt_type: ReceiptType, event_id: &EventId, - ) -> Result, Receipt)>> { - self.get_event_room_receipt_events(room_id, receipt_type, event_id).await + ) -> StoreResult, Receipt)>> { + self.get_event_room_receipt_events(room_id, receipt_type, event_id) + .await + .map_err(Into::into) } - async fn get_custom_value(&self, key: &[u8]) -> Result>> { - self.get_custom_value(key).await + async fn get_custom_value(&self, key: &[u8]) -> StoreResult>> { + self.get_custom_value(key).await.map_err(Into::into) } - async fn set_custom_value(&self, key: &[u8], value: Vec) -> Result>> { - self.set_custom_value(key, value).await + async fn set_custom_value(&self, key: &[u8], value: Vec) -> StoreResult>> { + self.set_custom_value(key, value).await.map_err(Into::into) } - async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> Result<()> { - self.add_media_content(request, data).await + async fn add_media_content(&self, request: &MediaRequest, data: Vec) -> StoreResult<()> { + self.add_media_content(request, data).await.map_err(Into::into) } - async fn get_media_content(&self, request: &MediaRequest) -> Result>> { - self.get_media_content(request).await + async fn get_media_content(&self, request: &MediaRequest) -> StoreResult>> { + self.get_media_content(request).await.map_err(Into::into) } - async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { - self.remove_media_content(request).await + async fn remove_media_content(&self, request: &MediaRequest) -> StoreResult<()> { + self.remove_media_content(request).await.map_err(Into::into) } - async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> { - self.remove_media_content_for_uri(uri).await + async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> StoreResult<()> { + self.remove_media_content_for_uri(uri).await.map_err(Into::into) } - async fn remove_room(&self, room_id: &RoomId) -> Result<()> { - self.remove_room(room_id).await + async fn remove_room(&self, room_id: &RoomId) -> StoreResult<()> { + self.remove_room(room_id).await.map_err(Into::into) } async fn room_timeline( &self, room_id: &RoomId, - ) -> Result>, Option)>> { - self.room_timeline(room_id).await + ) -> StoreResult>, Option)>> { + self.room_timeline(room_id).await.map_err(|e| e.into()) } } @@ -1399,10 +1449,13 @@ struct TimelineMetadata { #[cfg(test)] mod test { - use super::{Result, SledStore}; - async fn get_store() -> Result { - SledStore::open() + use matrix_sdk_base::statestore_integration_tests; + + use super::{SledStore, StateStore, StoreResult}; + + async fn get_store() -> StoreResult { + SledStore::open().map_err(Into::into) } statestore_integration_tests! { integration } diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 12dfe28334d..111f30e199b 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -24,12 +24,12 @@ default = [ "native-tls" ] -indexeddb_stores = ["matrix-sdk-base/indexeddb_state_store", "matrix-sdk-base/indexeddb_cryptostore"] +indexeddb_stores = ["matrix-sdk-indexeddb"] encryption = ["matrix-sdk-base/encryption"] qrcode = ["encryption", "matrix-sdk-base/qrcode"] # TODO merge those two sled features -sled_state_store = ["matrix-sdk-base/sled_state_store"] -sled_cryptostore = ["matrix-sdk-base/sled_cryptostore"] +sled_state_store = ["matrix-sdk-sled"] +sled_cryptostore = ["matrix-sdk-sled", "encryption"] markdown = ["ruma/markdown"] native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] @@ -69,6 +69,9 @@ url = "2.2.2" zeroize = "1.3.0" async-stream = "0.3.2" +matrix-sdk-sled = { path = "../matrix-sdk-sled", optional = true } +matrix-sdk-indexeddb = { path = "../matrix-sdk-indexeddb", optional = true } + [dependencies.image] version = "0.24.0" default-features = false diff --git a/crates/matrix-sdk/README.md b/crates/matrix-sdk/README.md index 634f0a9356a..5597949338a 100644 --- a/crates/matrix-sdk/README.md +++ b/crates/matrix-sdk/README.md @@ -59,18 +59,20 @@ More examples can be found in the [examples] directory. The following crate feature flags are available: -| Feature | Default | Description | -| ------------------ | :-----: | -------------------------------------------------------------- | -| `anyhow` | No | Better logging for event handlers that return `anyhow::Result` | -| `encryption` | Yes | End-to-end encryption support | -| `eyre` | No | Better logging for event handlers that return `eyre::Result` | -| `image_proc` | No | Enables image processing to generate thumbnails | -| `image_rayon` | No | Enables faster image processing | -| `markdown` | No | Support to send Markdown-formatted messages | -| `qrcode` | Yes | QR code verification support | -| `sled_cryptostore` | Yes | Persistent storage for E2EE related data | -| `socks` | No | Enables SOCKS support in the default HTTP client, [`reqwest`] | -| `sso_login` | No | Enables SSO login with a local HTTP server | +| Feature | Default | Description | +| ------------------ | :-----: | --------------------------------------------------------------------- | +| `anyhow` | No | Better logging for event handlers that return `anyhow::Result` | +| `encryption` | Yes | End-to-end encryption support | +| `eyre` | No | Better logging for event handlers that return `eyre::Result` | +| `image_proc` | No | Enables image processing to generate thumbnails | +| `image_rayon` | No | Enables faster image processing | +| `markdown` | No | Support to send Markdown-formatted messages | +| `qrcode` | Yes | QR code verification support | +| `sled_cryptostore` | Yes | Persistent storage for E2EE related data | +| `sled_statestore` | No | Persistent storage of state data with sled | +| `socks` | No | Enables SOCKS support in the default HTTP client, [`reqwest`] | +| `sso_login` | No | Enables SSO login with a local HTTP server | +| `indexedb_stores` | No | Persistent storage of state & e2ee data with indexeddb for web/wasm32 | [`reqwest`]: https://docs.rs/reqwest/0.11.5/reqwest/index.html diff --git a/crates/matrix-sdk/examples/autojoin.rs b/crates/matrix-sdk/examples/autojoin.rs index 7152db9d65b..4e311c42939 100644 --- a/crates/matrix-sdk/examples/autojoin.rs +++ b/crates/matrix-sdk/examples/autojoin.rs @@ -48,7 +48,9 @@ async fn login_and_sync( let mut home = dirs::home_dir().expect("no home directory found"); home.push("autojoin_bot"); - let client_config = ClientConfig::new().store_path(home); + let client_config = + ClientConfig::with_named_store(home.to_str().expect("home dir path must be utf-8"), None) + .await?; let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); let client = Client::new_with_config(homeserver_url, client_config).await.unwrap(); diff --git a/crates/matrix-sdk/examples/command_bot.rs b/crates/matrix-sdk/examples/command_bot.rs index 2a17cedd1b7..3f74ada43a0 100644 --- a/crates/matrix-sdk/examples/command_bot.rs +++ b/crates/matrix-sdk/examples/command_bot.rs @@ -41,7 +41,9 @@ async fn login_and_sync( let mut home = dirs::home_dir().expect("no home directory found"); home.push("party_bot"); - let client_config = ClientConfig::new().store_path(home); + let client_config = + ClientConfig::with_named_store(home.to_str().expect("home dir path must be utf-8"), None) + .await?; let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); // create a new Client with the given homeserver url and config diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client.rs index fe9afa9eb8c..92c19061688 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client.rs @@ -163,7 +163,7 @@ impl Client { /// /// * `homeserver_url` - The homeserver that the client should connect to. pub async fn new(homeserver_url: Url) -> Result { - let config = ClientConfig::new(); + let config = ClientConfig::new().await?; Client::new_with_config(homeserver_url, config).await } @@ -242,7 +242,7 @@ impl Client { /// /// [spec]: https://spec.matrix.org/unstable/client-server-api/#well-known-uri pub async fn new_from_user_id(user_id: &UserId) -> Result { - let config = ClientConfig::new(); + let config = ClientConfig::new().await?; Client::new_from_user_id_with_config(user_id, config).await } @@ -2382,7 +2382,8 @@ pub(crate) mod test { device_id: device_id!("DEVICEID").to_owned(), }; let homeserver = url::Url::parse(&mockito::server_url()).unwrap(); - let config = ClientConfig::new().request_config(RequestConfig::new().disable_retry()); + let config = + ClientConfig::new().await.unwrap().request_config(RequestConfig::new().disable_retry()); let client = Client::new_with_config(homeserver, config).await.unwrap(); client.restore_login(session).await.unwrap(); @@ -2480,7 +2481,7 @@ pub(crate) mod test { #[async_test] async fn login_with_discovery() { let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let config = ClientConfig::new().use_discovery_response(); + let config = ClientConfig::new().await.unwrap().use_discovery_response(); let client = Client::new_with_config(homeserver, config).await.unwrap(); @@ -2500,7 +2501,7 @@ pub(crate) mod test { #[async_test] async fn login_no_discovery() { let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let config = ClientConfig::new().use_discovery_response(); + let config = ClientConfig::new().await.unwrap().use_discovery_response(); let client = Client::new_with_config(homeserver.clone(), config).await.unwrap(); @@ -2631,8 +2632,9 @@ pub(crate) mod test { // test store reloads with correct room state from the sled store let path = tempfile::tempdir().unwrap(); - let config = ClientConfig::default() - .store_path(path) + let config = ClientConfig::with_named_store(path.into_path().to_str().unwrap(), None) + .await + .unwrap() .request_config(RequestConfig::new().disable_retry()); let joined_client = Client::new_with_config(homeserver, config).await.unwrap(); joined_client.restore_login(session).await.unwrap(); diff --git a/crates/matrix-sdk/src/config/client.rs b/crates/matrix-sdk/src/config/client.rs index e6d4b6fccbb..752f3e88148 100644 --- a/crates/matrix-sdk/src/config/client.rs +++ b/crates/matrix-sdk/src/config/client.rs @@ -20,7 +20,7 @@ use std::{ }; use http::header::InvalidHeaderValue; -use matrix_sdk_base::BaseClientConfig; +use matrix_sdk_base::{BaseClientConfig, StateStore}; use crate::{config::RequestConfig, HttpSend, Result}; @@ -35,10 +35,13 @@ use crate::{config::RequestConfig, HttpSend, Result}; /// use matrix_sdk::config::ClientConfig; /// // To pass all the request through mitmproxy set the proxy and disable SSL /// // verification -/// let client_config = ClientConfig::new() +/// +/// # futures::executor::block_on(async { +/// let client_config = ClientConfig::new().await? /// .proxy("http://localhost:8080")? /// .disable_ssl_verification(); /// # matrix_sdk::Result::<()>::Ok(()) +/// # }); /// ``` /// /// # Example for using a custom client @@ -57,9 +60,11 @@ use crate::{config::RequestConfig, HttpSend, Result}; /// .no_proxy() /// .user_agent("MyApp/v3.0"); /// -/// let client_config = ClientConfig::new() +/// # futures::executor::block_on(async { +/// let client_config = ClientConfig::new().await? /// .client(Arc::new(builder.build()?)); /// # matrix_sdk::Result::<()>::Ok(()) +/// # }); /// ``` #[derive(Default)] pub struct ClientConfig { @@ -89,11 +94,89 @@ impl Debug for ClientConfig { } } +#[cfg(feature = "sled_state_store")] +mod store_helpers { + use matrix_sdk_sled::StateStore; + + use super::Result; + + /// Build the sled Store with the default settings - as a temporary storage + pub async fn default_store() -> Result> { + Ok(Box::new(StateStore::open()?)) + } + + /// Build a sled store at `name` (being a relative or full path), and open + /// the store with the given passphrase (if given) for encryption + pub async fn default_store_with_name( + name: &str, + passphrase: Option<&str>, + ) -> Result> { + Ok(Box::new(match passphrase { + Some(pass) => StateStore::open_with_passphrase(name, pass)?, + _ => StateStore::open_with_path(&name)?, + })) + } +} + +#[cfg(feature = "indexeddb_stores")] +mod store_helpers { + use matrix_sdk_indexeddb::StateStore; + + use super::Result; + + /// Open the IndexedDB store with the default name, unencrypted + pub async fn default_store() -> Result> { + Ok(Box::new(StateStore::open().await?)) + } + + /// Open the indexeddb store at `name` (IndexedDB Database name), and open + /// the store with the given passphrase (if given) for encryption + pub async fn default_store_with_name( + name: &str, + passphrase: Option<&str>, + ) -> Result> { + Ok(Box::new(match passphrase { + Some(pass) => StateStore::open_with_passphrase(name.to_owned(), pass).await?, + _ => StateStore::open_with_name(name.to_string()).await?, + })) + } +} + +#[cfg(not(any(feature = "indexeddb_stores", feature = "sled_state_store")))] +mod store_helpers { + use matrix_sdk_base::store::MemoryStore as StateStore; + + use super::Result; + /// Open a new in-memory StateStore + pub async fn default_store() -> Result> { + Ok(Box::new(StateStore::new())) + } + + /// Alias for `default_store` - in Memory Stores are never named + pub async fn default_store_with_name( + _name: &str, + _passphrase: Option<&str>, + ) -> Result> { + Ok(Box::new(StateStore::new())) + } +} + +pub use store_helpers::{default_store, default_store_with_name}; + impl ClientConfig { /// Create a new default `ClientConfig`. - #[must_use] - pub fn new() -> Self { - Default::default() + pub async fn new() -> Result { + let mut d = Self::default(); + d.base_config = d.base_config.state_store(default_store().await?); + Ok(d) + } + + /// Create a new ClientConfig with a named state store, encrypted with the + /// given passphrase (if any) + pub async fn with_named_store(name: &str, passphrase: Option<&str>) -> Result { + let mut d = Self::default(); + d.base_config = d.base_config.state_store(default_store_with_name(name, passphrase).await?); + Ok(d) } /// Set the proxy through which all the HTTP requests should go. @@ -107,12 +190,14 @@ impl ClientConfig { /// # Example /// /// ``` + /// # futures::executor::block_on(async { /// use matrix_sdk::{Client, config::ClientConfig}; /// - /// let client_config = ClientConfig::new() + /// let client_config = ClientConfig::new().await? /// .proxy("http://localhost:8080")?; /// /// # Result::<_, matrix_sdk::Error>::Ok(()) + /// # }); /// ``` #[cfg(not(target_arch = "wasm32"))] pub fn proxy(mut self, proxy: &str) -> Result { @@ -133,44 +218,11 @@ impl ClientConfig { Ok(self) } - ///// Set a custom implementation of a `StateStore`. - ///// - ///// The state store should be opened before being set. - //#[must_use] - //pub fn state_store(mut self, store: Box) -> Self { - // 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, and if the corresponding features - /// (`sled_state_store` and `sled_cryptostore`) are enabled, 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 an - /// in-memory store will be opened. - #[must_use] - pub fn store_path(mut self, path: impl AsRef) -> Self { - self.base_config = self.base_config.store_path(path); - self - } - - /// Set the passphrase to encrypt the crypto store. - /// - /// # Argument + /// Set a custom implementation of a `StateStore`. /// - /// * `passphrase` - The passphrase that will be used to encrypt the data in - /// the cryptostore. - /// - /// This is only used if no custom cryptostore is set. - #[must_use] - pub fn passphrase(mut self, passphrase: String) -> Self { - self.base_config = self.base_config.passphrase(passphrase); + /// The state store should be opened before being set. + pub fn state_store(mut self, store: Box) -> Self { + self.base_config = self.base_config.state_store(store); self } diff --git a/crates/matrix-sdk/src/config/mod.rs b/crates/matrix-sdk/src/config/mod.rs index 48830e78f7f..533d0cdb63b 100644 --- a/crates/matrix-sdk/src/config/mod.rs +++ b/crates/matrix-sdk/src/config/mod.rs @@ -20,6 +20,6 @@ mod client; mod request; mod sync; -pub use client::ClientConfig; +pub use client::{default_store, default_store_with_name, ClientConfig}; pub use request::RequestConfig; pub use sync::SyncSettings; diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index a52ad7957b9..c591366e083 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -181,7 +181,8 @@ //! to work. //! //! 1. Make sure the `encryption` feature is enabled. -//! 2. Configure a store path with the [`ClientConfig::store_path`] method. +//! 2. Ensure you have a persistent storage backend, either by activating the +//! `sled_state_store`-feature or providing one via [`ClientConfig.state_store`] //! //! ## Restoring a client //! @@ -231,13 +232,12 @@ //! | Failure | Cause | Fix | //! | ------------------- | ----- | ----------- | //! | No messages get encrypted nor decrypted | The `encryption` feature is disabled | [Enable the feature in your `Cargo.toml` file] | -//! | Messages that were decryptable aren't after a restart | Storage isn't setup to be persistent | Setup storage with [`ClientConfig::store_path`] | +//! | Messages that were decryptable aren't after a restart | Storage isn't setup to be persistent | Ensure you've activated the persistent storage backend feature, e.g. `sled_state_store` | //! | Messages are encrypted but can't be decrypted | The access token that the client is using is tied to another device | Clear storage to create a new device, read the [Restoring a Client] section | //! | Messages don't get encrypted but get decrypted | The `m.room.encryption` event is missing | Make sure encryption is [enabled] for the room and the event isn't [filtered] out, otherwise it might be a deserialization bug | //! //! [Enable the feature in your `Cargo.toml` file]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#choosing-features //! [Megolm]: https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md -//! [`ClientConfig::store_path`]: crate::config::ClientConfig::store_path //! [`UserIdentity`]: #struct.verification.UserIdentity //! [filtered]: crate::config::SyncSettings::filter //! [enabled]: crate::room::Joined::enable_encryption diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 6c43c371b63..871a9ac51e6 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -109,6 +109,12 @@ pub enum Error { #[error("the queried endpoint requires authentication but was called before logging in")] AuthenticationRequired, + /// Attempting to restore a session after the olm-machine has already been + /// set up fails + #[cfg(feature = "encryption")] + #[error("The olm machine has already been initialized")] + BadCryptoStoreState, + /// An error de/serializing type for the `StateStore` #[error(transparent)] SerdeJson(#[from] JsonError), @@ -250,6 +256,8 @@ impl From for Error { #[cfg(feature = "encryption")] SdkBaseError::CryptoStore(e) => Self::CryptoStoreError(e), #[cfg(feature = "encryption")] + SdkBaseError::BadCryptoStoreState => Self::BadCryptoStoreState, + #[cfg(feature = "encryption")] SdkBaseError::OlmError(e) => Self::OlmError(e), #[cfg(feature = "encryption")] SdkBaseError::MegolmError(e) => Self::MegolmError(e),