Merge branch 'main' into jplatte/server-versions

This commit is contained in:
Jonas Platte
2022-03-14 12:56:57 +01:00
57 changed files with 1671 additions and 1379 deletions

View File

@@ -117,6 +117,32 @@ jobs:
command: run
args: -p xtask -- ci test-features ${{ matrix.name }}
test-crypto-features:
name: linux / crypto-crate features
needs: [style]
runs-on: ubuntu-latest
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Load cache
uses: Swatinem/rust-cache@v1
- name: Clippy
uses: actions-rs/cargo@v1
with:
command: run
args: -p xtask -- ci test-crypto
test:
name: ${{ matrix.name }}
if: github.event_name == 'push' || !github.event.pull_request.draft

View File

@@ -90,10 +90,9 @@ pub use matrix_sdk;
pub use matrix_sdk::ruma;
use matrix_sdk::{
bytes::Bytes,
config::ClientConfig,
event_handler::{EventHandler, EventHandlerResult, SyncEvent},
reqwest::Url,
Client, Session,
Client, ClientBuildError, ClientBuilder, Session,
};
use regex::Regex;
use ruma::{
@@ -210,8 +209,8 @@ impl AppService {
/// Create new AppService
///
/// Also creates and caches a [`Client`] for the [`MainUser`].
/// The default [`ClientConfig`] is used, if you want to customize it
/// use [`Self::new_with_config()`] instead.
/// A default [`ClientBuilder`] is used, if you want to customize it
/// use [`with_client_builder()`][Self::with_client_builder] instead.
///
/// # Arguments
///
@@ -227,24 +226,20 @@ impl AppService {
server_name: impl TryInto<Box<ServerName>, Error = identifiers::Error>,
registration: AppServiceRegistration,
) -> Result<Self> {
let appservice = Self::new_with_config(
homeserver_url,
server_name,
registration,
ClientConfig::default(),
)
.await?;
let appservice =
Self::with_client_builder(homeserver_url, server_name, registration, Client::builder())
.await?;
Ok(appservice)
}
/// Same as [`Self::new()`] but lets you provide a [`ClientConfig`] for the
/// [`Client`]
pub async fn new_with_config(
/// Same as [`new()`][Self::new] but lets you provide a [`ClientBuilder`]
/// for the [`Client`]
pub async fn with_client_builder(
homeserver_url: impl TryInto<Url, Error = url::ParseError>,
server_name: impl TryInto<Box<ServerName>, Error = identifiers::Error>,
registration: AppServiceRegistration,
client_config: ClientConfig,
builder: ClientBuilder,
) -> Result<Self> {
let homeserver_url = homeserver_url.try_into()?;
let server_name = server_name.try_into()?;
@@ -257,7 +252,7 @@ impl AppService {
AppService { homeserver_url, server_name, registration, clients, event_handler };
// we create and cache the [`MainUser`] by default
appservice.create_and_cache_client(&sender_localpart, client_config).await?;
appservice.create_and_cache_client(&sender_localpart, builder).await?;
Ok(appservice)
}
@@ -270,8 +265,9 @@ impl AppService {
///
/// This method is a singleton that saves the client internally for re-use
/// based on the `localpart`. The cached [`Client`] can be retrieved either
/// by calling this method again or by calling [`Self::get_cached_client()`]
/// which is non-async convenience wrapper.
/// by calling this method again or by calling
/// [`get_cached_client()`][Self::get_cached_client] which is non-async
/// convenience wrapper.
///
/// Note that if you want to do actions like joining rooms with a virtual
/// user it needs to be registered first. `Self::register_virtual_user()`
@@ -285,20 +281,20 @@ impl AppService {
/// [assert the identity]: https://matrix.org/docs/spec/application_service/r0.1.2#identity-assertion
pub async fn virtual_user_client(&self, localpart: impl AsRef<str>) -> Result<Client> {
let client =
self.virtual_user_client_with_config(localpart, ClientConfig::default()).await?;
self.virtual_user_client_with_client_builder(localpart, Client::builder()).await?;
Ok(client)
}
/// Same as [`Self::virtual_user_client()`] but with the ability to pass in
/// a [`ClientConfig`]
/// Same as [`virtual_user_client()`][Self::virtual_user_client] but with
/// the ability to pass in a [`ClientBuilder`]
///
/// Since this method is a singleton follow-up calls with different
/// [`ClientConfig`]s will be ignored.
pub async fn virtual_user_client_with_config(
/// [`ClientBuilder`]s will be ignored.
pub async fn virtual_user_client_with_client_builder(
&self,
localpart: impl AsRef<str>,
config: ClientConfig,
builder: ClientBuilder,
) -> Result<Client> {
// TODO: check if localpart is covered by namespace?
let localpart = localpart.as_ref();
@@ -306,7 +302,7 @@ impl AppService {
let client = if let Some(client) = self.clients.get(localpart) {
client.clone()
} else {
self.create_and_cache_client(localpart, config).await?
self.create_and_cache_client(localpart, builder).await?
};
Ok(client)
@@ -315,22 +311,23 @@ impl AppService {
async fn create_and_cache_client(
&self,
localpart: &str,
config: ClientConfig,
mut builder: ClientBuilder,
) -> Result<Client> {
let user_id = UserId::parse_with_server_name(localpart, &self.server_name)?;
// The `as_token` in the `Session` maps to the [`MainUser`]
// (`sender_localpart`) by default, so we don't need to assert identity
// in that case
let config = if localpart != self.registration.sender_localpart {
let request_config = config.get_request_config().assert_identity();
config.request_config(request_config)
} else {
config
};
if localpart != self.registration.sender_localpart {
builder = builder.assert_identity();
}
let client =
Client::new_with_config(self.homeserver_url.clone(), config.appservice_mode()).await?;
let client = builder
.homeserver_url(self.homeserver_url.clone())
.appservice_mode()
.build()
.await
.map_err(ClientBuildError::assert_valid_builder_args)?;
let session = Session {
access_token: self.registration.as_token.clone(),
@@ -348,8 +345,9 @@ impl AppService {
/// Get cached [`Client`]
///
/// Will return the client for the given `localpart` if previously
/// constructed with [`Self::virtual_user_client()`] or
/// [`Self::virtual_user_client_with_config()`].
/// constructed with [`virtual_user_client()`][Self::virtual_user_client] or
/// [`virtual_user_client_with_config()`][Self::
/// virtual_user_client_with_client_builder].
///
/// If no `localpart` is given it assumes the [`MainUser`]'s `localpart`. If
/// no client for `localpart` is found it will return an Error.

View File

@@ -4,12 +4,13 @@ use std::{
};
use matrix_sdk::{
config::{ClientConfig, RequestConfig},
config::RequestConfig,
ruma::{api::appservice::Registration, events::room::member::SyncRoomMemberEvent},
Client,
};
use matrix_sdk_appservice::*;
use matrix_sdk_test::{appservice::TransactionBuilder, async_test, EventsJson};
use ruma::room_id;
use ruma::{api::MatrixVersion, room_id};
use serde_json::json;
use warp::{Filter, Reply};
@@ -32,11 +33,17 @@ async fn appservice(registration: Option<Registration>) -> Result<AppService> {
let homeserver_url = mockito::server_url();
let server_name = "localhost";
let client_config =
ClientConfig::default().request_config(RequestConfig::default().disable_retry());
let client_builder = Client::builder()
.request_config(RequestConfig::default().disable_retry())
.server_versions([MatrixVersion::V1_0]);
AppService::new_with_config(homeserver_url.as_ref(), server_name, registration, client_config)
.await
AppService::with_client_builder(
homeserver_url.as_ref(),
server_name,
registration,
client_builder,
)
.await
}
#[async_test]

View File

@@ -13,15 +13,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#[allow(unused_imports)]
#[cfg(feature = "encryption")]
use std::ops::Deref;
use std::{
collections::{BTreeMap, BTreeSet},
convert::TryFrom,
fmt,
sync::Arc,
};
#[allow(unused_imports)]
#[cfg(feature = "encryption")]
use std::{ops::Deref, result::Result as StdResult};
#[cfg(feature = "encryption")]
use matrix_sdk_common::locks::Mutex;
@@ -54,7 +54,7 @@ use ruma::{
events::{
room::member::MembershipState, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent,
AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, AnySyncRoomEvent, AnySyncStateEvent,
EventContent, EventType,
EventContent, GlobalAccountDataEventType, StateEventType,
},
push::{Action, PushConditionRoomCtx, Ruleset},
serde::Raw,
@@ -69,7 +69,7 @@ use crate::{
rooms::{Room, RoomInfo, RoomType},
session::Session,
store::{
ambiguity_map::AmbiguityCache, Result as StoreResult, StateChanges, StateStore, Store,
ambiguity_map::AmbiguityCache, Result as StoreResult, StateChanges, Store, StoreConfig,
},
};
@@ -102,52 +102,6 @@ impl fmt::Debug for BaseClient {
}
}
/// Configuration for the creation of the `BaseClient`.
///
/// # Example
///
/// ```
/// # use matrix_sdk_base::BaseClientConfig;
///
/// let client_config = BaseClientConfig::new();
/// ```
#[derive(Default)]
pub struct BaseClientConfig {
#[cfg(feature = "encryption")]
crypto_store: Option<Box<dyn CryptoStore>>,
state_store: Option<Box<dyn StateStore>>,
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for BaseClientConfig {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
fmt.debug_struct("BaseClientConfig").finish()
}
}
impl BaseClientConfig {
/// Create a new default `BaseClientConfig`.
#[must_use]
pub fn new() -> Self {
Default::default()
}
/// Set a custom implementation of a `CryptoStore`.
///
/// The crypto store should be opened before being set.
#[cfg(feature = "encryption")]
pub fn crypto_store(mut self, store: Box<dyn CryptoStore>) -> Self {
self.crypto_store = Some(store);
self
}
/// Set a custom implementation of a `StateStore`.
pub fn state_store(mut self, store: Box<dyn StateStore>) -> Self {
self.state_store = Some(store);
self
}
}
#[cfg(feature = "encryption")]
enum CryptoHolder {
PreSetupStore(Option<Box<dyn CryptoStore>>),
@@ -169,7 +123,7 @@ impl CryptoHolder {
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(
OlmMachine::with_store(
session.user_id.to_owned(),
session.device_id.as_str().into(),
store.take().expect("We always exist"),
@@ -193,32 +147,31 @@ impl CryptoHolder {
}
impl BaseClient {
/// Create a new default client.
pub fn new() -> Self {
BaseClient::with_store_config(StoreConfig::default())
}
/// 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<Self> {
pub fn with_store_config(config: StoreConfig) -> Self {
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 {
BaseClient {
session: store.session.clone(),
sync_token: store.sync_token.clone(),
store,
#[cfg(feature = "encryption")]
olm: Mutex::new(holder).into(),
})
}
}
}
impl BaseClient {
/// Create a new default client.
pub async fn new() -> Result<Self> {
BaseClient::new_with_config(BaseClientConfig::default()).await
}
/// The current client session containing our user id, device id and access
/// token.
pub fn session(&self) -> &Arc<RwLock<Option<Session>>> {
@@ -1107,7 +1060,7 @@ impl BaseClient {
/// # use futures::executor::block_on;
/// # let alice = user_id!("@alice:example.org").to_owned();
/// # block_on(async {
/// # let client = BaseClient::new().await.unwrap();
/// # let client = BaseClient::new();
/// let device = client.get_device(&alice, device_id!("DEVICEID")).await;
///
/// println!("{:?}", device);
@@ -1161,7 +1114,7 @@ impl BaseClient {
/// # use futures::executor::block_on;
/// # let alice = user_id!("@alice:example.org");
/// # block_on(async {
/// # let client = BaseClient::new().await.unwrap();
/// # let client = BaseClient::new();
/// let devices = client.get_user_devices(alice).await.unwrap();
///
/// for device in devices.devices() {
@@ -1197,13 +1150,13 @@ impl BaseClient {
pub async fn get_push_rules(&self, changes: &StateChanges) -> Result<Ruleset> {
if let Some(AnyGlobalAccountDataEvent::PushRules(event)) = changes
.account_data
.get(EventType::PushRules.as_str())
.get(GlobalAccountDataEventType::PushRules.as_str())
.and_then(|e| e.deserialize().ok())
{
Ok(event.content.global)
} else if let Some(AnyGlobalAccountDataEvent::PushRules(event)) = self
.store
.get_account_data_event(EventType::PushRules)
.get_account_data_event(GlobalAccountDataEventType::PushRules)
.await?
.and_then(|e| e.deserialize().ok())
{
@@ -1246,14 +1199,14 @@ impl BaseClient {
let room_power_levels = if let Some(AnySyncStateEvent::RoomPowerLevels(event)) = changes
.state
.get(room_id)
.and_then(|types| types.get(EventType::RoomPowerLevels.as_str()))
.and_then(|types| types.get(StateEventType::RoomPowerLevels.as_str()))
.and_then(|events| events.get(""))
.and_then(|e| e.deserialize().ok())
{
event.content
} else if let Some(AnySyncStateEvent::RoomPowerLevels(event)) = self
.store
.get_state_event(room_id, EventType::RoomPowerLevels, "")
.get_state_event(room_id, StateEventType::RoomPowerLevels, "")
.await?
.and_then(|e| e.deserialize().ok())
{
@@ -1296,7 +1249,7 @@ impl BaseClient {
if let Some(AnySyncStateEvent::RoomPowerLevels(event)) = changes
.state
.get(&**room_id)
.and_then(|types| types.get(EventType::RoomPowerLevels.as_str()))
.and_then(|types| types.get(StateEventType::RoomPowerLevels.as_str()))
.and_then(|events| events.get(""))
.and_then(|e| e.deserialize().ok())
{
@@ -1309,5 +1262,11 @@ impl BaseClient {
}
}
impl Default for BaseClient {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod test {}

View File

@@ -33,7 +33,7 @@ mod session;
pub mod store;
mod timeline_stream;
pub use client::{BaseClient, BaseClientConfig};
pub use client::BaseClient;
#[cfg(any(test, feature = "testing"))]
pub use http;
#[cfg(feature = "encryption")]

View File

@@ -32,7 +32,8 @@ use ruma::{
tombstone::RoomTombstoneEventContent,
},
tag::Tags,
AnyRoomAccountDataEvent, AnyStateEventContent, AnySyncStateEvent, EventType,
AnyRoomAccountDataEvent, AnyStateEventContent, AnySyncStateEvent, RoomAccountDataEventType,
StateEventType,
},
receipt::ReceiptType,
EventId, MxcUri, RoomAliasId, RoomId, UserId,
@@ -415,7 +416,7 @@ impl Room {
let power =
self.store
.get_state_event(self.room_id(), EventType::RoomPowerLevels, "")
.get_state_event(self.room_id(), StateEventType::RoomPowerLevels, "")
.await?
.and_then(|e| e.deserialize().ok())
.and_then(|e| {
@@ -451,7 +452,7 @@ impl Room {
pub async fn tags(&self) -> StoreResult<Option<Tags>> {
if let Some(AnyRoomAccountDataEvent::Tag(event)) = self
.store
.get_room_account_data_event(self.room_id(), EventType::Tag)
.get_room_account_data_event(self.room_id(), RoomAccountDataEventType::Tag)
.await?
.and_then(|r| r.deserialize().ok())
{

View File

@@ -51,9 +51,10 @@ macro_rules! statestore_integration_tests {
member::{MembershipState, RoomMemberEventContent},
power_levels::RoomPowerLevelsEventContent,
},
AnyEphemeralRoomEventContent, AnySyncEphemeralRoomEvent, AnyStrippedStateEvent,
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent,
AnySyncStateEvent, EventType, Unsigned,
AnyEphemeralRoomEventContent, AnySyncEphemeralRoomEvent,
AnyStrippedStateEvent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent,
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType,
StateEventType, Unsigned,
},
mxc_uri,
receipt::ReceiptType,
@@ -279,10 +280,10 @@ macro_rules! statestore_integration_tests {
assert!(store.get_presence_event(user_id).await?.is_some());
assert_eq!(store.get_room_infos().await?.len(), 1);
assert_eq!(store.get_stripped_room_infos().await?.len(), 1);
assert!(store.get_account_data_event(EventType::PushRules).await?.is_some());
assert!(store.get_account_data_event(GlobalAccountDataEventType::PushRules).await?.is_some());
assert!(store.get_state_event(room_id, EventType::RoomName, "").await?.is_some());
assert_eq!(store.get_state_events(room_id, EventType::RoomTopic).await?.len(), 1);
assert!(store.get_state_event(room_id, StateEventType::RoomName, "").await?.is_some());
assert_eq!(store.get_state_events(room_id, StateEventType::RoomTopic).await?.len(), 1);
assert!(store.get_profile(room_id, user_id).await?.is_some());
assert!(store.get_member_event(room_id, user_id).await?.is_some());
assert_eq!(store.get_user_ids(room_id).await?.len(), 2);
@@ -290,7 +291,7 @@ macro_rules! statestore_integration_tests {
assert_eq!(store.get_joined_user_ids(room_id).await?.len(), 1);
assert_eq!(store.get_users_with_display_name(room_id, "example").await?.len(), 2);
assert!(store
.get_room_account_data_event(room_id, EventType::Tag)
.get_room_account_data_event(room_id, RoomAccountDataEventType::Tag)
.await?
.is_some());
assert!(store
@@ -337,7 +338,7 @@ macro_rules! statestore_integration_tests {
let event = raw_event.deserialize().unwrap();
assert!(store
.get_state_event(room_id, EventType::RoomPowerLevels, "")
.get_state_event(room_id, StateEventType::RoomPowerLevels, "")
.await
.unwrap()
.is_none());
@@ -346,7 +347,7 @@ macro_rules! statestore_integration_tests {
store.save_changes(&changes).await.unwrap();
assert!(store
.get_state_event(room_id, EventType::RoomPowerLevels, "")
.get_state_event(room_id, StateEventType::RoomPowerLevels, "")
.await
.unwrap()
.is_some());
@@ -527,8 +528,8 @@ macro_rules! statestore_integration_tests {
assert_eq!(store.get_room_infos().await?.len(), 0);
assert_eq!(store.get_stripped_room_infos().await?.len(), 1);
assert!(store.get_state_event(room_id, EventType::RoomName, "").await?.is_none());
assert_eq!(store.get_state_events(room_id, EventType::RoomTopic).await?.len(), 0);
assert!(store.get_state_event(room_id, StateEventType::RoomName, "").await?.is_none());
assert_eq!(store.get_state_events(room_id, StateEventType::RoomTopic).await?.len(), 0);
assert!(store.get_profile(room_id, user_id).await?.is_none());
assert!(store.get_member_event(room_id, user_id).await?.is_none());
assert_eq!(store.get_user_ids(room_id).await?.len(), 0);
@@ -536,7 +537,7 @@ macro_rules! statestore_integration_tests {
assert_eq!(store.get_joined_user_ids(room_id).await?.len(), 0);
assert_eq!(store.get_users_with_display_name(room_id, "example").await?.len(), 0);
assert!(store
.get_room_account_data_event(room_id, EventType::Tag)
.get_room_account_data_event(room_id, RoomAccountDataEventType::Tag)
.await?
.is_none());
assert!(store

View File

@@ -28,7 +28,8 @@ use ruma::{
receipt::Receipt,
room::member::{MembershipState, RoomMemberEventContent},
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent, EventType,
AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent, GlobalAccountDataEventType,
RoomAccountDataEventType, StateEventType,
},
receipt::ReceiptType,
serde::Raw,
@@ -408,7 +409,7 @@ impl MemoryStore {
async fn get_state_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
state_key: &str,
) -> Result<Option<Raw<AnySyncStateEvent>>> {
Ok(self.room_state.get(room_id).and_then(|e| {
@@ -419,7 +420,7 @@ impl MemoryStore {
async fn get_state_events(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
) -> Result<Vec<Raw<AnySyncStateEvent>>> {
Ok(self
.room_state
@@ -477,7 +478,7 @@ impl MemoryStore {
async fn get_account_data_event(
&self,
event_type: EventType,
event_type: GlobalAccountDataEventType,
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>> {
Ok(self.account_data.get(event_type.as_ref()).map(|e| e.clone()))
}
@@ -485,7 +486,7 @@ impl MemoryStore {
async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: RoomAccountDataEventType,
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>> {
Ok(self
.room_account_data
@@ -631,7 +632,7 @@ impl StateStore for MemoryStore {
async fn get_state_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
state_key: &str,
) -> Result<Option<Raw<AnySyncStateEvent>>> {
self.get_state_event(room_id, event_type, state_key).await
@@ -640,7 +641,7 @@ impl StateStore for MemoryStore {
async fn get_state_events(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
) -> Result<Vec<Raw<AnySyncStateEvent>>> {
self.get_state_events(room_id, event_type).await
}
@@ -695,7 +696,7 @@ impl StateStore for MemoryStore {
async fn get_account_data_event(
&self,
event_type: EventType,
event_type: GlobalAccountDataEventType,
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>> {
self.get_account_data_event(event_type).await
}
@@ -703,7 +704,7 @@ impl StateStore for MemoryStore {
async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: RoomAccountDataEventType,
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>> {
self.get_room_account_data_event(room_id, event_type).await
}

View File

@@ -24,6 +24,7 @@ use std::{
collections::{BTreeMap, BTreeSet},
ops::Deref,
pin::Pin,
result::Result as StdResult,
sync::Arc,
};
@@ -33,6 +34,8 @@ pub mod integration_tests;
use dashmap::DashMap;
use matrix_sdk_common::{async_trait, locks::RwLock, AsyncTraitDeps};
#[cfg(feature = "encryption")]
use matrix_sdk_crypto::store::CryptoStore;
use ruma::{
api::client::push::get_notifications::v3::Notification,
events::{
@@ -40,7 +43,8 @@ use ruma::{
receipt::{Receipt, ReceiptEventContent},
room::member::RoomMemberEventContent,
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
AnySyncStateEvent, EventContent, EventType,
AnySyncStateEvent, EventContent, GlobalAccountDataEventType, RoomAccountDataEventType,
StateEventType,
},
receipt::ReceiptType,
serde::Raw,
@@ -145,11 +149,11 @@ pub trait StateStore: AsyncTraitDeps {
async fn get_state_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
state_key: &str,
) -> Result<Option<Raw<AnySyncStateEvent>>>;
/// Get a list of state events for a given room and `EventType`.
/// Get a list of state events for a given room and `StateEventType`.
///
/// # Arguments
///
@@ -159,7 +163,7 @@ pub trait StateStore: AsyncTraitDeps {
async fn get_state_events(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
) -> Result<Vec<Raw<AnySyncStateEvent>>>;
/// Get the current profile for the given user in the given room.
@@ -226,7 +230,7 @@ pub trait StateStore: AsyncTraitDeps {
/// * `event_type` - The event type of the account data event.
async fn get_account_data_event(
&self,
event_type: EventType,
event_type: GlobalAccountDataEventType,
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>>;
/// Get an event out of the room account data store.
@@ -241,7 +245,7 @@ pub trait StateStore: AsyncTraitDeps {
async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: RoomAccountDataEventType,
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>>;
/// Get an event out of the user room receipt store.
@@ -605,3 +609,50 @@ impl StateChanges {
self.timeline.insert(room_id.to_owned(), timeline);
}
}
/// Configuration for the state store and, when `encryption` is enabled, for the
/// crypto store.
///
/// # Example
///
/// ```
/// # use matrix_sdk_base::store::StoreConfig;
///
/// let store_config = StoreConfig::new();
/// ```
#[derive(Default)]
pub struct StoreConfig {
#[cfg(feature = "encryption")]
pub(crate) crypto_store: Option<Box<dyn CryptoStore>>,
pub(crate) state_store: Option<Box<dyn StateStore>>,
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for StoreConfig {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> {
fmt.debug_struct("StoreConfig").finish()
}
}
impl StoreConfig {
/// Create a new default `StoreConfig`.
#[must_use]
pub fn new() -> Self {
Default::default()
}
/// Set a custom implementation of a `CryptoStore`.
///
/// The crypto store must be opened before being set.
#[cfg(feature = "encryption")]
pub fn crypto_store(mut self, store: Box<dyn CryptoStore>) -> Self {
self.crypto_store = Some(store);
self
}
/// Set a custom implementation of a `StateStore`.
pub fn state_store(mut self, store: Box<dyn StateStore>) -> Self {
self.state_store = Some(store);
self
}
}

View File

@@ -73,7 +73,7 @@ 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_store(alice_id().into(), alice_device_id().into(), store))
.block_on(OlmMachine::with_store(alice_id().into(), alice_device_id().into(), store))
.unwrap();
group.bench_with_input(BenchmarkId::new("sled store", &name), &response, |b, response| {
@@ -122,7 +122,7 @@ pub fn keys_claiming(c: &mut Criterion) {
let store = Box::new(SledCryptoStore::open_with_passphrase(dir, None).unwrap());
let machine = runtime
.block_on(OlmMachine::new_with_store(
.block_on(OlmMachine::with_store(
alice_id().into(),
alice_device_id().into(),
store,
@@ -188,7 +188,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
let store = Box::new(SledCryptoStore::open_with_passphrase(dir, None).unwrap());
let machine = runtime
.block_on(OlmMachine::new_with_store(alice_id().into(), alice_device_id().into(), store))
.block_on(OlmMachine::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();
@@ -244,7 +244,7 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
let store = Box::new(SledCryptoStore::open_with_passphrase(dir, None).unwrap());
let machine = runtime
.block_on(OlmMachine::new_with_store(alice_id().into(), alice_device_id().into(), store))
.block_on(OlmMachine::with_store(alice_id().into(), alice_device_id().into(), store))
.unwrap();
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();

View File

@@ -20,6 +20,7 @@ use std::{
use olm_rs::pk::OlmPkEncryption;
use ruma::{
api::client::backup::{KeyBackupData, KeyBackupDataInit, SessionDataInit},
serde::Base64,
DeviceKeyId, UserId,
};
use zeroize::Zeroizing;
@@ -127,9 +128,11 @@ impl MegolmV1BackupKey {
let message = pk.encrypt(&key);
let session_data = SessionDataInit {
ephemeral: message.ephemeral_key,
ciphertext: message.ciphertext,
mac: message.mac,
ephemeral: Base64::parse(message.ephemeral_key)
.expect("Can't decode the base64 encoded ephemeral backup key"),
ciphertext: Base64::parse(message.ciphertext)
.expect("Can't decode a base64 encoded libolm ciphertext"),
mac: Base64::parse(message.mac).expect("Can't decode a base64 encoded MAC"),
}
.into();

View File

@@ -51,4 +51,4 @@ mod backup;
mod recovery;
pub use backup::MegolmV1BackupKey;
pub use recovery::{DecodeError, PickledRecoveryKey, RecoveryKey};
pub use recovery::DecodeError;

View File

@@ -17,25 +17,17 @@ use std::{
io::{Cursor, Read},
};
use aes::cipher::generic_array::GenericArray;
use aes_gcm::{
aead::{Aead, NewAead},
Aes256Gcm,
};
use bs58;
use olm_rs::{
errors::OlmPkDecryptionError,
pk::{OlmPkDecryption, PkMessage},
};
use rand::{thread_rng, Error as RandomError, Fill};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zeroize::{Zeroize, Zeroizing};
use zeroize::Zeroizing;
use super::MegolmV1BackupKey;
use crate::utilities::{decode_url_safe, encode, encode_url_safe};
const NONCE_SIZE: usize = 12;
use crate::{store::RecoveryKey, utilities::encode};
/// Error type for the decoding of a RecoveryKey.
#[derive(Debug, Error)]
@@ -64,47 +56,12 @@ pub enum DecodeError {
pub enum UnpicklingError {
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("Couldn't decrypt the pickle: {0}")]
Decryption(String),
// #[error("Couldn't decrypt the pickle: {0}")]
// Decryption(String),
#[error(transparent)]
Decode(#[from] DecodeError),
}
/// The private part of a backup key.
#[derive(Zeroize)]
pub struct RecoveryKey {
inner: [u8; RecoveryKey::KEY_SIZE],
}
impl Drop for RecoveryKey {
fn drop(&mut self) {
self.inner.zeroize()
}
}
impl std::fmt::Debug for RecoveryKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RecoveryKey").finish()
}
}
/// The pickled version of a recovery key.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PickledRecoveryKey(String);
impl AsRef<str> for PickledRecoveryKey {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct InnerPickle {
version: u8,
nonce: String,
ciphertext: String,
}
impl TryFrom<String> for RecoveryKey {
type Error = DecodeError;
@@ -132,7 +89,6 @@ impl std::fmt::Display for RecoveryKey {
}
impl RecoveryKey {
const KEY_SIZE: usize = 32;
const PREFIX: [u8; 2] = [0x8b, 0x01];
const PREFIX_PARITY: u8 = Self::PREFIX[0] ^ Self::PREFIX[1];
const DISPLAY_CHUNK_SIZE: usize = 4;
@@ -235,57 +191,6 @@ impl RecoveryKey {
public_key
}
/// Export this [`RecoveryKey`] as an encrypted pickle that can be safely
/// stored.
pub fn pickle(&self, pickle_key: &[u8]) -> PickledRecoveryKey {
let key = GenericArray::from_slice(pickle_key);
let cipher = Aes256Gcm::new(key);
let mut nonce = vec![0u8; NONCE_SIZE];
let mut rng = thread_rng();
nonce.try_fill(&mut rng).expect("Can't generate random nocne to pickle the recovery key");
let nonce = GenericArray::from_slice(nonce.as_slice());
let ciphertext =
cipher.encrypt(nonce, self.inner.as_ref()).expect("Can't encrypt recovery key");
let ciphertext = encode_url_safe(ciphertext);
let pickle =
InnerPickle { version: 1, nonce: encode_url_safe(nonce.as_slice()), ciphertext };
PickledRecoveryKey(serde_json::to_string(&pickle).expect("Can't encode pickled signing"))
}
/// Try to import a `RecoveryKey` from a previously exported pickle.
pub fn from_pickle(
pickle: PickledRecoveryKey,
pickle_key: &[u8],
) -> Result<Self, UnpicklingError> {
let pickled: InnerPickle = serde_json::from_str(pickle.as_ref())?;
let key = GenericArray::from_slice(pickle_key);
let cipher = Aes256Gcm::new(key);
let nonce = decode_url_safe(pickled.nonce).map_err(DecodeError::from)?;
let nonce = GenericArray::from_slice(&nonce);
let ciphertext = &decode_url_safe(pickled.ciphertext).map_err(DecodeError::from)?;
let decrypted = cipher
.decrypt(nonce, ciphertext.as_slice())
.map_err(|e| UnpicklingError::Decryption(e.to_string()))?;
if decrypted.len() != Self::KEY_SIZE {
Err(DecodeError::Length(decrypted.len(), Self::KEY_SIZE).into())
} else {
let mut key = [0u8; Self::KEY_SIZE];
key.copy_from_slice(&decrypted);
Ok(Self { inner: key })
}
}
/// Try to decrypt the given ciphertext using this `RecoveryKey`.
///
/// This will use the [`m.megolm_backup.v1.curve25519-aes-sha2`] algorithm

View File

@@ -39,13 +39,13 @@ use tracing::{debug, info, instrument, trace, warn};
use crate::{
olm::{Account, InboundGroupSession},
store::{BackupKeys, Changes, RoomKeyCounts, Store},
store::{BackupKeys, Changes, RecoveryKey, RoomKeyCounts, Store},
CryptoStoreError, KeysBackupRequest, OutgoingRequest,
};
mod keys;
pub use keys::{DecodeError, MegolmV1BackupKey, PickledRecoveryKey, RecoveryKey};
pub use keys::{DecodeError, MegolmV1BackupKey};
pub use olm_rs::errors::OlmPkDecryptionError;
/// A state machine that handles backing up room keys.
@@ -64,7 +64,7 @@ pub struct BackupMachine {
#[derive(Debug, Clone)]
struct PendingBackup {
request_id: &TransactionId,
request_id: Box<TransactionId>,
request: KeysBackupRequest,
sessions: BTreeMap<Box<RoomId>, BTreeMap<String, BTreeSet<String>>>,
}
@@ -388,8 +388,7 @@ mod test {
use matrix_sdk_test::async_test;
use ruma::{device_id, room_id, user_id, DeviceId, RoomId, UserId};
use super::RecoveryKey;
use crate::{OlmError, OlmMachine};
use crate::{store::RecoveryKey, OlmError, OlmMachine};
fn alice_id() -> &'static UserId {
user_id!("@alice:example.org")
@@ -430,12 +429,12 @@ mod test {
let request =
backup_machine.backup().await?.expect("Created a backup request successfully");
assert_eq!(
Some(request.request_id),
backup_machine.backup().await?.map(|r| r.request_id),
Some(&*request.request_id),
backup_machine.backup().await?.as_ref().map(|r| &*r.request_id),
"Calling backup again without uploading creates the same backup request"
);
backup_machine.mark_request_as_sent(request.request_id).await?;
backup_machine.mark_request_as_sent(&request.request_id).await?;
let counts = backup_machine.store.inbound_group_session_counts().await?;
assert_eq!(counts.total, 2);
@@ -471,13 +470,9 @@ mod test {
use tempfile::tempdir;
let tmpdir = tempdir().expect("Can't create a temporary dir");
let machine = OlmMachine::new_with_default_store(
alice_id(),
alice_device_id(),
tmpdir.as_ref(),
None,
)
.await?;
let machine =
OlmMachine::with_default_store(alice_id(), alice_device_id(), tmpdir.as_ref(), None)
.await?;
backup_flow(machine).await
}
@@ -488,7 +483,7 @@ mod test {
use tempfile::tempdir;
let tmpdir = tempdir().expect("Can't create a temporary dir");
let machine = OlmMachine::new_with_default_store(
let machine = OlmMachine::with_default_store(
alice_id(),
alice_device_id(),
tmpdir.as_ref(),

View File

@@ -150,7 +150,7 @@ impl GossipRequest {
}
};
let request = ToDeviceRequest::new_with_id(
let request = ToDeviceRequest::with_id(
&self.request_recipient,
DeviceIdOrAllDevices::AllDevices,
content,

View File

@@ -1071,7 +1071,7 @@ pub(crate) mod test {
let (_, device) = device(&response);
let account = ReadOnlyAccount::new(device.user_id(), device.device_id());
let (identity, _, _) = PrivateCrossSigningIdentity::new_with_account(&account).await;
let (identity, _, _) = PrivateCrossSigningIdentity::with_account(&account).await;
let id = Arc::new(Mutex::new(identity.clone()));

View File

@@ -220,7 +220,7 @@ impl OlmMachine {
/// the encryption keys.
///
/// [`Cryptostore`]: trait.CryptoStore.html
pub async fn new_with_store(
pub async fn with_store(
user_id: Box<UserId>,
device_id: Box<DeviceId>,
store: Box<dyn CryptoStore>,
@@ -272,7 +272,7 @@ impl OlmMachine {
///
/// * `device_id` - The unique id of the device that owns this machine.
#[cfg(feature = "sled_cryptostore")]
pub async fn new_with_default_store(
pub async fn with_default_store(
user_id: &UserId,
device_id: &DeviceId,
path: impl AsRef<Path>,
@@ -280,7 +280,7 @@ impl OlmMachine {
) -> StoreResult<Self> {
let store = SledStore::open_with_passphrase(path, passphrase)?;
OlmMachine::new_with_store(user_id.to_owned(), device_id.into(), Box::new(store)).await
OlmMachine::with_store(user_id.to_owned(), device_id.into(), Box::new(store)).await
}
/// The unique user id that owns this `OlmMachine` instance.
@@ -376,7 +376,7 @@ impl OlmMachine {
}
IncomingResponse::KeysBackup(_) => {
#[cfg(feature = "backups_v1")]
self.backup_machine.mark_request_as_sent(*request_id).await?;
self.backup_machine.mark_request_as_sent(request_id).await?;
}
};
@@ -2036,7 +2036,7 @@ pub(crate) mod test {
let tmpdir = tempdir().unwrap();
let machine = OlmMachine::new_with_default_store(
let machine = OlmMachine::with_default_store(
user_id(),
alice_device_id(),
tmpdir.as_ref(),
@@ -2053,7 +2053,7 @@ pub(crate) mod test {
drop(machine);
let machine = OlmMachine::new_with_default_store(
let machine = OlmMachine::with_default_store(
&user_id,
alice_device_id(),
tmpdir.as_ref(),

View File

@@ -696,6 +696,7 @@ impl ReadOnlyAccount {
self.inner.lock().await.sign(string)
}
/// Check that the given json value is signed by this account.
#[cfg(feature = "backups_v1")]
pub fn is_signed(&self, json: &mut Value) -> Result<(), SignatureError> {
let signing_key = self.identity_keys.ed25519();
@@ -807,7 +808,7 @@ impl ReadOnlyAccount {
pub async fn bootstrap_cross_signing(
&self,
) -> (PrivateCrossSigningIdentity, UploadSigningKeysRequest, SignatureUploadRequest) {
PrivateCrossSigningIdentity::new_with_account(self).await
PrivateCrossSigningIdentity::with_account(self).await
}
/// Sign the given CrossSigning Key in place

View File

@@ -238,8 +238,6 @@ impl InboundGroupSession {
self.backed_up.store(false, SeqCst)
}
#[cfg(any(test, feature = "testing"))]
#[allow(dead_code)]
/// For testing, allow to manually mark this GroupSession to have been
/// backed up
pub fn mark_as_backed_up(&self) {
@@ -342,6 +340,8 @@ impl InboundGroupSession {
self.inner.lock().await.decrypt(message)
}
/// Export the inbound group session into a format that can be uploaded to
/// the server as a backup.
#[cfg(feature = "backups_v1")]
pub async fn to_backup(&self) -> BackedUpRoomKey {
self.export().await.into()

View File

@@ -476,7 +476,7 @@ impl PrivateCrossSigningIdentity {
/// * `account` - The Olm account that is creating the new identity. The
/// account will sign the master key and the self signing key will sign the
/// account.
pub(crate) async fn new_with_account(
pub(crate) async fn with_account(
account: &ReadOnlyAccount,
) -> (Self, UploadSigningKeysRequest, SignatureUploadRequest) {
let master = Signing::new();
@@ -739,7 +739,7 @@ mod test {
#[async_test]
async fn private_identity_signed_by_account() {
let account = ReadOnlyAccount::new(user_id(), device_id!("DEVICEID"));
let (identity, _, _) = PrivateCrossSigningIdentity::new_with_account(&account).await;
let (identity, _, _) = PrivateCrossSigningIdentity::with_account(&account).await;
let master = identity.master_key.lock().await;
let master = master.as_ref().unwrap();
@@ -749,7 +749,7 @@ mod test {
#[async_test]
async fn sign_device() {
let account = ReadOnlyAccount::new(user_id(), device_id!("DEVICEID"));
let (identity, _, _) = PrivateCrossSigningIdentity::new_with_account(&account).await;
let (identity, _, _) = PrivateCrossSigningIdentity::with_account(&account).await;
let mut device = ReadOnlyDevice::from_account(&account).await;
let self_signing = identity.self_signing_key.lock().await;
@@ -766,10 +766,10 @@ mod test {
#[async_test]
async fn sign_user_identity() {
let account = ReadOnlyAccount::new(user_id(), device_id!("DEVICEID"));
let (identity, _, _) = PrivateCrossSigningIdentity::new_with_account(&account).await;
let (identity, _, _) = PrivateCrossSigningIdentity::with_account(&account).await;
let bob_account = ReadOnlyAccount::new(user_id!("@bob:localhost"), device_id!("DEVICEID"));
let (bob_private, _, _) = PrivateCrossSigningIdentity::new_with_account(&bob_account).await;
let (bob_private, _, _) = PrivateCrossSigningIdentity::with_account(&bob_account).await;
let mut bob_public = ReadOnlyUserIdentity::from_private(&bob_private).await;
let user_signing = identity.user_signing_key.lock().await;

View File

@@ -30,7 +30,7 @@ use ruma::{
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
},
encryption::CrossSigningKey,
events::{AnyMessageEventContent, AnyToDeviceEventContent, EventContent, EventType},
events::{AnyMessageEventContent, AnyToDeviceEventContent, EventContent, ToDeviceEventType},
serde::Raw,
to_device::DeviceIdOrAllDevices,
DeviceId, RoomId, TransactionId, UserId,
@@ -42,7 +42,7 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ToDeviceRequest {
/// Type of event being sent to each device.
pub event_type: EventType,
pub event_type: ToDeviceEventType,
/// A request identifier unique to the access token used to send the
/// request.
@@ -75,10 +75,10 @@ impl ToDeviceRequest {
recipient_device: impl Into<DeviceIdOrAllDevices>,
content: AnyToDeviceEventContent,
) -> Self {
Self::new_with_id(recipient, recipient_device, content, TransactionId::new())
Self::with_id(recipient, recipient_device, content, TransactionId::new())
}
pub(crate) fn new_for_recipients(
pub(crate) fn for_recipients(
recipient: &UserId,
recipient_devices: Vec<Box<DeviceId>>,
content: AnyToDeviceEventContent,
@@ -102,13 +102,13 @@ impl ToDeviceRequest {
}
}
pub(crate) fn new_with_id(
pub(crate) fn with_id(
recipient: &UserId,
recipient_device: impl Into<DeviceIdOrAllDevices>,
content: AnyToDeviceEventContent,
txn_id: Box<TransactionId>,
) -> Self {
let event_type = EventType::from(content.event_type());
let event_type = ToDeviceEventType::from(content.event_type());
let raw_content = Raw::new(&content).expect("Failed to serialize to-device event");
let user_messages = iter::once((recipient_device.into(), raw_content)).collect();

View File

@@ -24,7 +24,7 @@ use matrix_sdk_common::executor::spawn;
use ruma::{
events::{
room::{encrypted::RoomEncryptedEventContent, history_visibility::HistoryVisibility},
AnyToDeviceEventContent, EventType,
AnyToDeviceEventContent, ToDeviceEventType,
},
serde::Raw,
to_device::DeviceIdOrAllDevices,
@@ -300,7 +300,7 @@ impl GroupSessionManager {
let txn_id = TransactionId::new();
let request = ToDeviceRequest {
event_type: EventType::RoomEncrypted,
event_type: ToDeviceEventType::RoomEncrypted,
txn_id: txn_id.clone(),
messages,
};

View File

@@ -31,7 +31,7 @@
//! # let device_id = device_id!("TEST").to_owned();
//! let store = Box::new(MemoryStore::new());
//!
//! let machine = OlmMachine::new_with_store(user_id, device_id, store);
//! let machine = OlmMachine::with_store(user_id, device_id, store);
//! ```
//!
//! [`OlmMachine`]: /matrix_sdk_crypto/struct.OlmMachine.html
@@ -65,6 +65,7 @@ use ruma::{
events::secret::request::SecretName, identifiers::Error as IdentifierValidationError, DeviceId,
DeviceKeyAlgorithm, RoomId, TransactionId, UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::Error as SerdeError;
use thiserror::Error;
use tracing::{info, warn};
@@ -108,10 +109,8 @@ pub struct Store {
pub struct Changes {
pub account: Option<ReadOnlyAccount>,
pub private_identity: Option<PrivateCrossSigningIdentity>,
#[cfg(feature = "backups_v1")]
pub backup_version: Option<String>,
#[cfg(feature = "backups_v1")]
pub recovery_key: Option<crate::backups::RecoveryKey>,
pub recovery_key: Option<RecoveryKey>,
pub sessions: Vec<Session>,
pub message_hashes: Vec<OlmMessageHash>,
pub inbound_group_sessions: Vec<InboundGroupSession>,
@@ -157,6 +156,103 @@ pub struct DeviceChanges {
pub deleted: Vec<ReadOnlyDevice>,
}
/// The private part of a backup key.
#[derive(Zeroize)]
#[zeroize(drop)]
pub struct RecoveryKey {
pub(crate) inner: [u8; RecoveryKey::KEY_SIZE],
}
/// The pickled version of a recovery key.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PickledRecoveryKey(String);
impl AsRef<str> for PickledRecoveryKey {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct InnerPickle {
version: u8,
nonce: String,
ciphertext: String,
}
impl RecoveryKey {
/// The number of bytes the recovery key will hold.
pub const KEY_SIZE: usize = 32;
const NONCE_SIZE: usize = 12;
/// Export this [`RecoveryKey`] as an encrypted pickle that can be safely
/// stored.
pub fn pickle(&self, pickle_key: &[u8]) -> PickledRecoveryKey {
use aes::cipher::generic_array::GenericArray;
use aes_gcm::aead::{Aead, NewAead};
use rand::Fill;
let key = GenericArray::from_slice(pickle_key);
let cipher = aes_gcm::Aes256Gcm::new(key);
let mut nonce = vec![0u8; Self::NONCE_SIZE];
let mut rng = rand::thread_rng();
nonce.try_fill(&mut rng).expect("Can't generate random nocne to pickle the recovery key");
let nonce = GenericArray::from_slice(nonce.as_slice());
let ciphertext =
cipher.encrypt(nonce, self.inner.as_ref()).expect("Can't encrypt recovery key");
let ciphertext = crate::utilities::encode_url_safe(ciphertext);
let pickle = InnerPickle {
version: 1,
nonce: crate::utilities::encode_url_safe(nonce.as_slice()),
ciphertext,
};
PickledRecoveryKey(serde_json::to_string(&pickle).expect("Can't encode pickled signing"))
}
/// Try to import a `RecoveryKey` from a previously exported pickle.
pub fn from_pickle(
pickle: PickledRecoveryKey,
pickle_key: &[u8],
) -> Result<Self, CryptoStoreError> {
use aes::cipher::generic_array::GenericArray;
use aes_gcm::aead::{Aead, NewAead};
let pickled: InnerPickle = serde_json::from_str(pickle.as_ref())?;
let key = GenericArray::from_slice(pickle_key);
let cipher = aes_gcm::Aes256Gcm::new(key);
let nonce = crate::utilities::decode_url_safe(pickled.nonce).unwrap();
let nonce = GenericArray::from_slice(&nonce);
let ciphertext = &crate::utilities::decode_url_safe(pickled.ciphertext).unwrap();
let decrypted = cipher
.decrypt(nonce, ciphertext.as_slice())
.map_err(|_| CryptoStoreError::UnpicklingError)?;
if decrypted.len() != Self::KEY_SIZE {
Err(CryptoStoreError::UnpicklingError)
} else {
let mut key = [0u8; Self::KEY_SIZE];
key.copy_from_slice(&decrypted);
Ok(Self { inner: key })
}
}
}
impl Debug for RecoveryKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RecoveryKey").finish()
}
}
impl DeviceChanges {
/// Merge the given `DeviceChanges` into this instance of `DeviceChanges`.
pub fn extend(&mut self, other: DeviceChanges) {
@@ -183,10 +279,8 @@ pub struct RoomKeyCounts {
#[derive(Default, Debug)]
pub struct BackupKeys {
/// The recovery key, the one used to decrypt backed up room keys.
#[cfg(feature = "backups_v1")]
pub recovery_key: Option<crate::backups::RecoveryKey>,
pub recovery_key: Option<RecoveryKey>,
/// The version that we are using for backups.
#[cfg(feature = "backups_v1")]
pub backup_version: Option<String>,
}

View File

@@ -727,7 +727,7 @@ impl TryFrom<ToDeviceRequest> for OutgoingContent {
type Error = String;
fn try_from(request: ToDeviceRequest) -> Result<Self, Self::Error> {
use ruma::events::EventType;
use ruma::events::ToDeviceEventType;
use serde_json::Value;
let json: Value = serde_json::from_str(
@@ -742,30 +742,40 @@ impl TryFrom<ToDeviceRequest> for OutgoingContent {
.map_err(|e| e.to_string())?;
let content = match request.event_type {
EventType::KeyVerificationStart => AnyToDeviceEventContent::KeyVerificationStart(
ToDeviceEventType::KeyVerificationStart => {
AnyToDeviceEventContent::KeyVerificationStart(
serde_json::from_value(json).map_err(|e| e.to_string())?,
)
}
ToDeviceEventType::KeyVerificationKey => AnyToDeviceEventContent::KeyVerificationKey(
serde_json::from_value(json).map_err(|e| e.to_string())?,
),
EventType::KeyVerificationKey => AnyToDeviceEventContent::KeyVerificationKey(
ToDeviceEventType::KeyVerificationAccept => {
AnyToDeviceEventContent::KeyVerificationAccept(
serde_json::from_value(json).map_err(|e| e.to_string())?,
)
}
ToDeviceEventType::KeyVerificationMac => AnyToDeviceEventContent::KeyVerificationMac(
serde_json::from_value(json).map_err(|e| e.to_string())?,
),
EventType::KeyVerificationAccept => AnyToDeviceEventContent::KeyVerificationAccept(
serde_json::from_value(json).map_err(|e| e.to_string())?,
),
EventType::KeyVerificationMac => AnyToDeviceEventContent::KeyVerificationMac(
serde_json::from_value(json).map_err(|e| e.to_string())?,
),
EventType::KeyVerificationCancel => AnyToDeviceEventContent::KeyVerificationCancel(
serde_json::from_value(json).map_err(|e| e.to_string())?,
),
EventType::KeyVerificationReady => AnyToDeviceEventContent::KeyVerificationReady(
serde_json::from_value(json).map_err(|e| e.to_string())?,
),
EventType::KeyVerificationDone => AnyToDeviceEventContent::KeyVerificationDone(
serde_json::from_value(json).map_err(|e| e.to_string())?,
),
EventType::KeyVerificationRequest => AnyToDeviceEventContent::KeyVerificationRequest(
ToDeviceEventType::KeyVerificationCancel => {
AnyToDeviceEventContent::KeyVerificationCancel(
serde_json::from_value(json).map_err(|e| e.to_string())?,
)
}
ToDeviceEventType::KeyVerificationReady => {
AnyToDeviceEventContent::KeyVerificationReady(
serde_json::from_value(json).map_err(|e| e.to_string())?,
)
}
ToDeviceEventType::KeyVerificationDone => AnyToDeviceEventContent::KeyVerificationDone(
serde_json::from_value(json).map_err(|e| e.to_string())?,
),
ToDeviceEventType::KeyVerificationRequest => {
AnyToDeviceEventContent::KeyVerificationRequest(
serde_json::from_value(json).map_err(|e| e.to_string())?,
)
}
e => return Err(format!("Unsupported event type {}", e)),
};

View File

@@ -164,7 +164,7 @@ impl VerificationRequest {
milli_seconds_since_unix_epoch(),
);
ToDeviceRequest::new_for_recipients(
ToDeviceRequest::for_recipients(
self.other_user(),
self.recipient_devices.to_vec(),
AnyToDeviceEventContent::KeyVerificationRequest(content),
@@ -442,7 +442,7 @@ impl VerificationRequest {
let request = content.map(|c| match c {
OutgoingContent::ToDevice(content) => {
if send_to_everyone {
ToDeviceRequest::new_for_recipients(
ToDeviceRequest::for_recipients(
self.other_user(),
self.recipient_devices.to_vec(),
content,
@@ -537,7 +537,7 @@ impl VerificationRequest {
if recipients.is_empty() && filter_device.is_some() {
None
} else {
Some(ToDeviceRequest::new_for_recipients(
Some(ToDeviceRequest::for_recipients(
self.other_user(),
recipients,
c,

View File

@@ -5,7 +5,7 @@ edition = "2021"
[features]
default = ["encryption"]
encryption = ["matrix-sdk-crypto"]
encryption = ["matrix-sdk-base/encryption", "matrix-sdk-crypto"]
[package.metadata.docs.rs]
default-target = "wasm32-unknown-unknown"

View File

@@ -1,3 +1,6 @@
#[cfg(target_arch = "wasm32")]
use matrix_sdk_base::store::StoreConfig;
mod safe_encode;
#[cfg(target_arch = "wasm32")]
@@ -12,3 +15,52 @@ mod cryptostore;
pub use cryptostore::IndexeddbStore as CryptoStore;
#[cfg(target_arch = "wasm32")]
pub use state_store::IndexeddbStore as StateStore;
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "encryption")]
/// Create a [`StateStore`] and a [`CryptoStore`] that use the same name and
/// passphrase.
async fn open_stores_with_name(
name: impl Into<String>,
passphrase: Option<&str>,
) -> Result<(Box<StateStore>, Box<CryptoStore>), anyhow::Error> {
let name = name.into();
if let Some(passphrase) = passphrase {
let state_store = StateStore::open_with_passphrase(name.clone(), passphrase).await?;
let crypto_store = CryptoStore::open_with_passphrase(name, passphrase).await?;
Ok((Box::new(state_store), Box::new(crypto_store)))
} else {
let state_store = StateStore::open_with_name(name.clone()).await?;
let crypto_store = CryptoStore::open_with_name(name).await?;
Ok((Box::new(state_store), Box::new(crypto_store)))
}
}
#[cfg(target_arch = "wasm32")]
/// Create a [`StoreConfig`] with an opened indexeddb [`StateStore`] that uses
/// the given name and passphrase. If `encryption` is enabled, a [`CryptoStore`]
/// with the same parameters is also opened.
pub async fn make_store_config(
name: impl Into<String>,
passphrase: Option<&str>,
) -> Result<StoreConfig, anyhow::Error> {
let name = name.into();
#[cfg(feature = "encryption")]
{
let (state_store, crypto_store) = open_stores_with_name(name, passphrase).await?;
Ok(StoreConfig::new().state_store(state_store).crypto_store(crypto_store))
}
#[cfg(not(feature = "encryption"))]
{
let state_store = if let Some(passphrase) = passphrase {
StateStore::open_with_passphrase(name, passphrase).await?
} else {
StateStore::open_with_name(name).await?
};
Ok(StoreConfig::new().state_store(Box::new(state_store)))
}
}

View File

@@ -1,7 +1,7 @@
#![allow(dead_code)]
use matrix_sdk_base::ruma::events::StateEventType;
use matrix_sdk_common::ruma::{
events::EventType, receipt::ReceiptType, DeviceId, EventId, MxcUri, RoomId, TransactionId,
UserId,
receipt::ReceiptType, DeviceId, EventId, MxcUri, RoomId, TransactionId, UserId,
};
use wasm_bindgen::JsValue;
use web_sys::IdbKeyRange;
@@ -135,7 +135,7 @@ impl SafeEncode for TransactionId {
}
}
impl SafeEncode for EventType {
impl SafeEncode for StateEventType {
fn as_encoded_string(&self) -> String {
self.as_str().as_encoded_string()
}

View File

@@ -34,7 +34,8 @@ use matrix_sdk_common::{
receipt::Receipt,
room::member::{MembershipState, RoomMemberEventContent},
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncMessageEvent,
AnySyncRoomEvent, AnySyncStateEvent, EventType,
AnySyncRoomEvent, AnySyncStateEvent, GlobalAccountDataEventType,
RoomAccountDataEventType, StateEventType,
},
receipt::ReceiptType,
serde::Raw,
@@ -711,7 +712,7 @@ impl IndexeddbStore {
pub async fn get_state_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
state_key: &str,
) -> Result<Option<Raw<AnySyncStateEvent>>> {
self.inner
@@ -726,7 +727,7 @@ impl IndexeddbStore {
pub async fn get_state_events(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
) -> Result<Vec<Raw<AnySyncStateEvent>>> {
let range = (room_id, &event_type).encode_to_range().map_err(StoreError::Codec)?;
Ok(self
@@ -860,7 +861,7 @@ impl IndexeddbStore {
pub async fn get_account_data_event(
&self,
event_type: EventType,
event_type: GlobalAccountDataEventType,
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>> {
self.inner
.transaction_on_one_with_mode(KEYS::ACCOUNT_DATA, IdbTransactionMode::Readonly)?
@@ -874,7 +875,7 @@ impl IndexeddbStore {
pub async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: RoomAccountDataEventType,
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>> {
self.inner
.transaction_on_one_with_mode(KEYS::ROOM_ACCOUNT_DATA, IdbTransactionMode::Readonly)?
@@ -1122,7 +1123,7 @@ impl StateStore for IndexeddbStore {
async fn get_state_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
state_key: &str,
) -> StoreResult<Option<Raw<AnySyncStateEvent>>> {
self.get_state_event(room_id, event_type, state_key).await.map_err(|e| e.into())
@@ -1131,7 +1132,7 @@ impl StateStore for IndexeddbStore {
async fn get_state_events(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
) -> StoreResult<Vec<Raw<AnySyncStateEvent>>> {
self.get_state_events(room_id, event_type).await.map_err(|e| e.into())
}
@@ -1182,7 +1183,7 @@ impl StateStore for IndexeddbStore {
async fn get_account_data_event(
&self,
event_type: EventType,
event_type: GlobalAccountDataEventType,
) -> StoreResult<Option<Raw<AnyGlobalAccountDataEvent>>> {
self.get_account_data_event(event_type).await.map_err(|e| e.into())
}
@@ -1190,7 +1191,7 @@ impl StateStore for IndexeddbStore {
async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: RoomAccountDataEventType,
) -> StoreResult<Option<Raw<AnyRoomAccountDataEvent>>> {
self.get_room_account_data_event(room_id, event_type).await.map_err(|e| e.into())
}

View File

@@ -11,7 +11,7 @@ required-features = ["binary-build"]
[features]
default = ["encryption"]
encryption = ["matrix-sdk-crypto"]
encryption = ["matrix-sdk-base/encryption", "matrix-sdk-crypto"]
binary-build = [
"atty",
"clap",

View File

@@ -4,7 +4,7 @@ use atty::Stream;
use clap::{Arg, ArgMatches, Command as Argparse};
use futures::executor::block_on;
use matrix_sdk_base::{RoomInfo, Store};
use matrix_sdk_common::ruma::{events::EventType, RoomId, UserId};
use matrix_sdk_common::ruma::{events::StateEventType, RoomId, UserId};
use matrix_sdk_sled::StateStore;
use rustyline::{
completion::{Completer, Pair},
@@ -228,7 +228,8 @@ impl Inspector {
}
Some(("get-state", args)) => {
let room_id = RoomId::parse(args.value_of("room-id").unwrap()).unwrap();
let event_type = EventType::try_from(args.value_of("event-type").unwrap()).unwrap();
let event_type =
StateEventType::try_from(args.value_of("event-type").unwrap()).unwrap();
self.get_state(room_id, event_type).await;
}
_ => unreachable!(),
@@ -263,7 +264,7 @@ impl Inspector {
}
}
async fn get_state(&self, room_id: Box<RoomId>, event_type: EventType) {
async fn get_state(&self, room_id: Box<RoomId>, event_type: StateEventType) {
self.printer.pretty_print_struct(
&self.store.get_state_event(&room_id, event_type, "").await.unwrap(),
);
@@ -288,7 +289,9 @@ impl Inspector {
RoomId::parse(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned())
}))
.arg(Arg::new("event-type").required(true).validator(|e| {
EventType::try_from(e).map(|_| ()).map_err(|_| "Invalid event type".to_owned())
StateEventType::try_from(e)
.map(|_| ())
.map_err(|_| "Invalid event type".to_owned())
})),
]
}

View File

@@ -37,7 +37,7 @@ use matrix_sdk_crypto::{
},
store::{
caches::SessionStore, BackupKeys, Changes, CryptoStore, CryptoStoreError, IdentityKeys,
PickleKey, PicklingMode, Result, RoomKeyCounts,
PickleKey, PicklingMode, RecoveryKey, Result, RoomKeyCounts,
},
GossipRequest, LocalTrust, ReadOnlyAccount, ReadOnlyDevice, ReadOnlyUserIdentities, SecretInfo,
};
@@ -518,7 +518,6 @@ impl SledStore {
None
};
#[cfg(feature = "backups_v1")]
let recovery_key_pickle = changes.recovery_key.map(|r| r.pickle(self.get_pickle_key()));
let device_changes = changes.devices;
@@ -559,7 +558,6 @@ impl SledStore {
let identity_changes = changes.identities;
let olm_hashes = changes.message_hashes;
let key_requests = changes.key_requests;
#[cfg(feature = "backups_v1")]
let backup_version = changes.backup_version;
let ret: Result<(), TransactionError<serde_json::Error>> = (
@@ -603,7 +601,6 @@ impl SledStore {
)?;
}
#[cfg(feature = "backups_v1")]
if let Some(r) = &recovery_key_pickle {
account.insert(
"recovery_key_v1".encode(),
@@ -611,7 +608,6 @@ impl SledStore {
)?;
}
#[cfg(feature = "backups_v1")]
if let Some(b) = &backup_version {
account.insert(
"backup_version_v1".encode(),
@@ -1071,21 +1067,22 @@ impl CryptoStore for SledStore {
}
async fn load_backup_keys(&self) -> Result<BackupKeys> {
#[cfg(feature = "backups_v1")]
let key = {
let backup_version = self
.account
.get("backup_version_v1".encode())?
.get("backup_version_v1".encode())
.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?
.map(|v| serde_json::from_slice(&v))
.transpose()?;
let recovery_key = {
self.account
.get("recovery_key_v1".encode())?
.get("recovery_key_v1".encode())
.map_err(|e| CryptoStoreError::Backend(anyhow!(e)))?
.map(|p| serde_json::from_slice(&p))
.transpose()?
.map(|p| {
crate::backups::RecoveryKey::from_pickle(p, self.get_pickle_key())
RecoveryKey::from_pickle(p, self.get_pickle_key())
.map_err(|_| CryptoStoreError::UnpicklingError)
})
.transpose()?
@@ -1094,9 +1091,6 @@ impl CryptoStore for SledStore {
BackupKeys { backup_version, recovery_key }
};
#[cfg(not(feature = "backups_v1"))]
let key = BackupKeys {};
Ok(key)
}
}

View File

@@ -1,3 +1,7 @@
use std::path::Path;
use matrix_sdk_base::store::StoreConfig;
#[cfg(feature = "encryption")]
mod cryptostore;
mod state_store;
@@ -5,3 +9,46 @@ mod state_store;
#[cfg(feature = "encryption")]
pub use cryptostore::SledStore as CryptoStore;
pub use state_store::SledStore as StateStore;
#[cfg(feature = "encryption")]
/// Create a [`StateStore`] and a [`CryptoStore`] that use the same database and
/// passphrase.
fn open_stores_with_path(
path: impl AsRef<Path>,
passphrase: Option<&str>,
) -> Result<(Box<StateStore>, Box<CryptoStore>), anyhow::Error> {
if let Some(passphrase) = passphrase {
let state_store = StateStore::open_with_passphrase(path, passphrase)?;
let crypto_store = state_store.get_crypto_store(Some(passphrase))?;
Ok((Box::new(state_store), Box::new(crypto_store)))
} else {
let state_store = StateStore::open_with_path(path)?;
let crypto_store = state_store.get_crypto_store(None)?;
Ok((Box::new(state_store), Box::new(crypto_store)))
}
}
/// Create a [`StoreConfig`] with an opened sled [`StateStore`] that uses the
/// given path and passphrase. If `encryption` is enabled, a [`CryptoStore`]
/// with the same parameters is also opened.
pub fn make_store_config(
path: impl AsRef<Path>,
passphrase: Option<&str>,
) -> Result<StoreConfig, anyhow::Error> {
#[cfg(feature = "encryption")]
{
let (state_store, crypto_store) = open_stores_with_path(path, passphrase)?;
Ok(StoreConfig::new().state_store(state_store).crypto_store(crypto_store))
}
#[cfg(not(feature = "encryption"))]
{
let state_store = if let Some(passphrase) = passphrase {
StateStore::open_with_passphrase(path, passphrase)?
} else {
StateStore::open_with_path(path)?
};
Ok(StoreConfig::new().state_store(Box::new(state_store)))
}
}

View File

@@ -42,7 +42,8 @@ use matrix_sdk_common::{
receipt::Receipt,
room::member::{MembershipState, RoomMemberEventContent},
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncMessageEvent,
AnySyncRoomEvent, AnySyncStateEvent, EventType,
AnySyncRoomEvent, AnySyncStateEvent, GlobalAccountDataEventType,
RoomAccountDataEventType, StateEventType,
},
receipt::ReceiptType,
serde::Raw,
@@ -58,6 +59,9 @@ use sled::{
use tokio::task::spawn_blocking;
use tracing::{info, warn};
#[cfg(feature = "encryption")]
pub use crate::CryptoStore;
#[derive(Debug, Serialize, Deserialize)]
pub enum DatabaseType {
Unencrypted,
@@ -186,12 +190,24 @@ impl EncodeKey for (&str, &str, &str, &str) {
}
}
impl EncodeKey for EventType {
impl EncodeKey for StateEventType {
fn encode(&self) -> Vec<u8> {
self.as_str().encode()
}
}
impl EncodeKey for GlobalAccountDataEventType {
fn encode(&self) -> Vec<u8> {
self.as_str().encode()
}
}
/* impl EncodeKey for RoomAccountDataEventType {
fn encode(&self) -> Vec<u8> {
self.as_str().encode()
}
} */
impl EncodeKey for EventId {
fn encode(&self) -> Vec<u8> {
self.as_str().encode()
@@ -364,6 +380,14 @@ impl SledStore {
SledStore::open_helper(db, Some(path), None)
}
#[cfg(feature = "encryption")]
/// Open a `CryptoStore` that uses the same database as this store.
///
/// The given passphrase will be used to encrypt private data.
pub fn get_crypto_store(&self, passphrase: Option<&str>) -> Result<CryptoStore, anyhow::Error> {
CryptoStore::open_with_database(self.inner.clone(), passphrase)
}
fn serialize_event(&self, event: &impl Serialize) -> Result<Vec<u8>, SledStoreError> {
if let Some(key) = &*self.store_key {
let encrypted = key.encrypt(event)?;
@@ -649,7 +673,7 @@ impl SledStore {
pub async fn get_state_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
state_key: &str,
) -> Result<Option<Raw<AnySyncStateEvent>>> {
let db = self.clone();
@@ -663,7 +687,7 @@ impl SledStore {
pub async fn get_state_events(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
) -> Result<Vec<Raw<AnySyncStateEvent>>> {
let db = self.clone();
let key = (room_id.as_str(), event_type.as_str()).encode();
@@ -809,7 +833,7 @@ impl SledStore {
pub async fn get_account_data_event(
&self,
event_type: EventType,
event_type: GlobalAccountDataEventType,
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>> {
let db = self.clone();
let key = event_type.encode();
@@ -822,7 +846,7 @@ impl SledStore {
pub async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: RoomAccountDataEventType,
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>> {
let db = self.clone();
let key = (room_id.as_str(), event_type.as_str()).encode();
@@ -1300,7 +1324,7 @@ impl StateStore for SledStore {
async fn get_state_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
state_key: &str,
) -> StoreResult<Option<Raw<AnySyncStateEvent>>> {
self.get_state_event(room_id, event_type, state_key).await.map_err(Into::into)
@@ -1309,7 +1333,7 @@ impl StateStore for SledStore {
async fn get_state_events(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: StateEventType,
) -> StoreResult<Vec<Raw<AnySyncStateEvent>>> {
self.get_state_events(room_id, event_type).await.map_err(Into::into)
}
@@ -1370,7 +1394,7 @@ impl StateStore for SledStore {
async fn get_account_data_event(
&self,
event_type: EventType,
event_type: GlobalAccountDataEventType,
) -> StoreResult<Option<Raw<AnyGlobalAccountDataEvent>>> {
self.get_account_data_event(event_type).await.map_err(Into::into)
}
@@ -1378,7 +1402,7 @@ impl StateStore for SledStore {
async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: EventType,
event_type: RoomAccountDataEventType,
) -> StoreResult<Option<Raw<AnyRoomAccountDataEvent>>> {
self.get_room_account_data_event(room_id, event_type).await.map_err(Into::into)
}
@@ -1449,7 +1473,6 @@ struct TimelineMetadata {
#[cfg(test)]
mod test {
use matrix_sdk_base::statestore_integration_tests;
use super::{SledStore, StateStore, StoreResult};

View File

@@ -24,12 +24,13 @@ default = [
"native-tls"
]
indexeddb_stores = ["matrix-sdk-indexeddb"]
indexeddb_state_store = ["matrix-sdk-indexeddb"]
indexeddb_cryptostore = ["matrix-sdk-indexeddb/encryption", "encryption"]
encryption = ["matrix-sdk-base/encryption"]
qrcode = ["encryption", "matrix-sdk-base/qrcode"]
# TODO merge those two sled features
sled_state_store = ["matrix-sdk-sled"]
sled_cryptostore = ["matrix-sdk-sled", "encryption"]
sled_cryptostore = ["matrix-sdk-sled/encryption", "encryption"]
markdown = ["ruma/markdown"]
native-tls = ["reqwest/native-tls"]
rustls-tls = ["reqwest/rustls-tls"]
@@ -69,8 +70,8 @@ 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 }
matrix-sdk-sled = { path = "../matrix-sdk-sled", default-features = false, optional = true }
matrix-sdk-indexeddb = { path = "../matrix-sdk-indexeddb", default-features = false, optional = true }
[dependencies.image]
version = "0.24.0"

View File

@@ -27,14 +27,14 @@ This is demonstrated in the example below.
```rust,no_run
use std::convert::TryFrom;
use matrix_sdk::{
Client, config::SyncSettings, Result,
Client, config::SyncSettings,
ruma::{user_id, events::room::message::SyncRoomMessageEvent},
};
#[tokio::main]
async fn main() -> Result<()> {
async fn main() -> anyhow::Result<()> {
let alice = user_id!("@alice:example.org");
let client = Client::new_from_user_id(alice).await?;
let client = Client::builder().user_id(alice).build().await?;
// First we need to log in.
client.login(alice, "password", None, None).await?;

View File

@@ -1,13 +1,9 @@
use std::{env, process::exit};
use matrix_sdk::{
config::{ClientConfig, SyncSettings},
room::Room,
ruma::events::room::member::StrippedRoomMemberEvent,
Client,
config::SyncSettings, room::Room, ruma::events::room::member::StrippedRoomMemberEvent, Client,
};
use tokio::time::{sleep, Duration};
use url::Url;
async fn on_stripped_state_member(
room_member: StrippedRoomMemberEvent,
@@ -44,16 +40,26 @@ async fn login_and_sync(
homeserver_url: String,
username: &str,
password: &str,
) -> Result<(), matrix_sdk::Error> {
let mut home = dirs::home_dir().expect("no home directory found");
home.push("autojoin_bot");
) -> anyhow::Result<()> {
#[allow(unused_mut)]
let mut client_builder = Client::builder().homeserver_url(homeserver_url);
let client_config =
ClientConfig::with_named_store(home.to_str().expect("home dir path must be utf-8"), None)
.await?;
#[cfg(feature = "sled_state_store")]
{
// The location to save files to
let mut home = dirs::home_dir().expect("no home directory found");
home.push("autojoin_bot");
let state_store = matrix_sdk_sled::StateStore::open_with_path(home)?;
client_builder = client_builder.state_store(Box::new(state_store));
}
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();
#[cfg(feature = "indexeddb_state_store")]
{
let state_store = matrix_sdk_indexeddb::StateStore::open();
client_builder = client_builder.state_store(Box::new(state_store));
}
let client = client_builder.build().await?;
client.login(username, password, None, Some("autojoin bot")).await?;
@@ -67,7 +73,7 @@ async fn login_and_sync(
}
#[tokio::main]
async fn main() -> Result<(), matrix_sdk::Error> {
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let (homeserver_url, username, password) =

View File

@@ -1,14 +1,13 @@
use std::{env, process::exit};
use matrix_sdk::{
config::{ClientConfig, SyncSettings},
config::SyncSettings,
room::Room,
ruma::events::room::message::{
MessageType, RoomMessageEventContent, SyncRoomMessageEvent, TextMessageEventContent,
},
Client,
};
use url::Url;
async fn on_room_message(event: SyncRoomMessageEvent, room: Room) {
if let Room::Joined(room) = room {
@@ -36,19 +35,26 @@ async fn login_and_sync(
homeserver_url: String,
username: String,
password: String,
) -> Result<(), matrix_sdk::Error> {
// the location for `JsonStore` to save files to
let mut home = dirs::home_dir().expect("no home directory found");
home.push("party_bot");
) -> anyhow::Result<()> {
#[allow(unused_mut)]
let mut client_builder = Client::builder().homeserver_url(homeserver_url);
let client_config =
ClientConfig::with_named_store(home.to_str().expect("home dir path must be utf-8"), None)
.await?;
#[cfg(feature = "sled_state_store")]
{
// The location to save files to
let mut home = dirs::home_dir().expect("no home directory found");
home.push("party_bot");
let state_store = matrix_sdk_sled::StateStore::open_with_path(home)?;
client_builder = client_builder.state_store(Box::new(state_store));
}
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
let client = Client::new_with_config(homeserver_url, client_config).await.unwrap();
#[cfg(feature = "indexeddb_state_store")]
{
let state_store = matrix_sdk_indexeddb::StateStore::open();
client_builder = client_builder.state_store(Box::new(state_store));
}
let client = client_builder.build().await.unwrap();
client.login(&username, &password, None, Some("command bot")).await?;
println!("logged in as {}", username);
@@ -72,7 +78,7 @@ async fn login_and_sync(
}
#[tokio::main]
async fn main() -> Result<(), matrix_sdk::Error> {
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let (homeserver_url, username, password) =

View File

@@ -14,7 +14,7 @@ async fn bootstrap(client: Client, user_id: Box<UserId>, password: String) {
io::stdin().read_line(&mut input).expect("error: unable to read user input");
if let Err(e) = client.bootstrap_cross_signing(None).await {
if let Err(e) = client.encryption().bootstrap_cross_signing(None).await {
use matrix_sdk::ruma::{api::client::uiaa, assign};
if let Some(response) = e.uiaa_response() {
@@ -27,6 +27,7 @@ async fn bootstrap(client: Client, user_id: Box<UserId>, password: String) {
));
client
.encryption()
.bootstrap_cross_signing(Some(auth_data))
.await
.expect("Couldn't bootstrap cross signing")

View File

@@ -54,7 +54,7 @@ fn print_result(sas: &SasVerification) {
async fn print_devices(user_id: &UserId, client: &Client) {
println!("Devices of user {}", user_id);
for device in client.get_user_devices(user_id).await.unwrap().devices() {
for device in client.encryption().get_user_devices(user_id).await.unwrap().devices() {
println!(
" {:<10} {:<30} {:<}",
device.device_id(),
@@ -87,6 +87,7 @@ async fn login(
match event {
AnyToDeviceEvent::KeyVerificationStart(e) => {
if let Some(Verification::SasV1(sas)) = client
.encryption()
.get_verification(&e.sender, e.content.transaction_id.as_str())
.await
{
@@ -102,6 +103,7 @@ async fn login(
AnyToDeviceEvent::KeyVerificationKey(e) => {
if let Some(Verification::SasV1(sas)) = client
.encryption()
.get_verification(&e.sender, e.content.transaction_id.as_str())
.await
{
@@ -111,6 +113,7 @@ async fn login(
AnyToDeviceEvent::KeyVerificationMac(e) => {
if let Some(Verification::SasV1(sas)) = client
.encryption()
.get_verification(&e.sender, e.content.transaction_id.as_str())
.await
{
@@ -136,6 +139,7 @@ async fn login(
if let MessageType::VerificationRequest(_) = &m.content.msgtype
{
let request = client
.encryption()
.get_verification_request(&m.sender, &m.event_id)
.await
.expect("Request object wasn't created");
@@ -148,6 +152,7 @@ async fn login(
}
AnySyncMessageEvent::KeyVerificationKey(e) => {
if let Some(Verification::SasV1(sas)) = client
.encryption()
.get_verification(
&e.sender,
e.content.relates_to.event_id.as_str(),
@@ -159,6 +164,7 @@ async fn login(
}
AnySyncMessageEvent::KeyVerificationMac(e) => {
if let Some(Verification::SasV1(sas)) = client
.encryption()
.get_verification(
&e.sender,
e.content.relates_to.event_id.as_str(),

View File

@@ -25,9 +25,9 @@ getrandom = { version = "0.2.4", features = ["js"] }
[dependencies.matrix-sdk]
path = "../.."
default-features = false
features = ["native-tls", "encryption", "indexeddb_stores"]
features = ["native-tls", "encryption", "indexeddb_state_store", "indexeddb_cryptostore"]
[workspace]
[dev-dependencies]
wasm-bindgen-test = "0.2"
wasm-bindgen-test = "0.2"

View File

@@ -0,0 +1,433 @@
use std::sync::Arc;
use matrix_sdk_base::{locks::RwLock, store::StoreConfig, BaseClient, StateStore};
use ruma::{
api::{
client::discover::{discover_homeserver, get_supported_versions},
MatrixVersion,
},
ServerName, UserId,
};
use thiserror::Error;
use url::Url;
use super::{Client, ClientInner};
use crate::{
config::RequestConfig,
http_client::{HttpClient, HttpSend, HttpSettings},
HttpError,
};
/// Builder that allows creating and configuring various parts of a [`Client`].
///
/// When setting the `StateStore` it is up to the user to open/connect
/// the storage backend before client creation.
///
/// # Example
///
/// ```
/// use matrix_sdk::Client;
/// // To pass all the request through mitmproxy set the proxy and disable SSL
/// // verification
///
/// let client_builder = Client::builder()
/// .proxy("http://localhost:8080")
/// .disable_ssl_verification();
/// ```
///
/// # Example for using a custom http client
///
/// Note: setting a custom http client will ignore `user_agent`, `proxy`, and
/// `disable_ssl_verification` - you'd need to set these yourself if you want
/// them.
///
/// ```
/// use matrix_sdk::Client;
/// use std::sync::Arc;
///
/// // setting up a custom http client
/// let reqwest_builder = reqwest::ClientBuilder::new()
/// .https_only(true)
/// .no_proxy()
/// .user_agent("MyApp/v3.0");
///
/// let client_builder = Client::builder()
/// .http_client(Arc::new(reqwest_builder.build()?));
/// # anyhow::Ok(())
/// ```
#[must_use]
#[derive(Debug)]
pub struct ClientBuilder {
homeserver_cfg: Option<HomeserverConfig>,
http_cfg: Option<HttpConfig>,
store_config: StoreConfig,
request_config: RequestConfig,
respect_login_well_known: bool,
appservice_mode: bool,
server_versions: Option<Arc<[MatrixVersion]>>,
}
impl ClientBuilder {
pub(crate) fn new() -> Self {
Self {
homeserver_cfg: None,
http_cfg: None,
store_config: Default::default(),
request_config: Default::default(),
respect_login_well_known: true,
appservice_mode: false,
server_versions: None,
}
}
/// Set the homeserver URL to use.
///
/// This method is mutually exclusive with [`user_id()`][Self::user_id], if
/// you set both whatever was set last will be used.
pub fn homeserver_url(mut self, url: impl AsRef<str>) -> Self {
self.homeserver_cfg = Some(HomeserverConfig::Url(url.as_ref().to_owned()));
self
}
/// Set the user ID to discover the homeserver from.
///
/// `builder.user_id(id)` is a shortcut for
/// `builder.server_name(id.server_name())`.
///
/// This method is mutually exclusive with
/// [`homeserver_url()`][Self::homeserver_url], if you set both whatever was
/// set last will be used.
pub fn user_id(self, user_id: &UserId) -> Self {
self.server_name(user_id.server_name())
}
/// Set the server name to discover the homeserver from.
///
/// This method is mutually exclusive with
/// [`homeserver_url()`][Self::homeserver_url], if you set both whatever was
/// set last will be used.
pub fn server_name(mut self, server_name: &ServerName) -> Self {
self.homeserver_cfg = Some(HomeserverConfig::ServerName(server_name.to_owned()));
self
}
/// Create a new `ClientConfig` with the given [`StoreConfig`].
///
/// The easiest way to get a [`StoreConfig`] is to use the
/// [`make_store_config`] method from the [`store`] module or directly from
/// one of the store crates.
///
/// # Arguments
///
/// * `store_config` - The configuration of the store.
///
/// # Example
///
/// ```
/// # use matrix_sdk_base::store::MemoryStore;
/// # let custom_state_store = Box::new(MemoryStore::new());
/// use matrix_sdk::{Client, config::StoreConfig};
///
/// let store_config = StoreConfig::new().state_store(custom_state_store);
/// let client_builder = Client::builder().store_config(store_config);
/// ```
/// [`make_store_config`]: crate::store::make_store_config
/// [`store`]: crate::store
pub fn store_config(mut self, store_config: StoreConfig) -> Self {
self.store_config = store_config;
self
}
/// Set a custom implementation of a `StateStore`.
///
/// The state store should be opened before being set.
pub fn state_store(mut self, store: Box<dyn StateStore>) -> Self {
self.store_config = self.store_config.state_store(store);
self
}
/// Set a custom implementation of a `CryptoStore`.
///
/// The crypto store should be opened before being set.
#[cfg(feature = "encryption")]
pub fn crypto_store(
mut self,
store: Box<dyn matrix_sdk_base::crypto::store::CryptoStore>,
) -> Self {
self.store_config = self.store_config.crypto_store(store);
self
}
/// Update the client's homeserver URL with the discovery information
/// present in the login response, if any.
pub fn respect_login_well_known(mut self, value: bool) -> Self {
self.respect_login_well_known = value;
self
}
/// Set the default timeout, fail and retry behavior for all HTTP requests.
pub fn request_config(mut self, request_config: RequestConfig) -> Self {
self.request_config = request_config;
self
}
/// Set the proxy through which all the HTTP requests should go.
///
/// Note, only HTTP proxies are supported.
///
/// # Arguments
///
/// * `proxy` - The HTTP URL of the proxy.
///
/// # Example
///
/// ```
/// # futures::executor::block_on(async {
/// use matrix_sdk::Client;
///
/// let client_config = Client::builder()
/// .proxy("http://localhost:8080");
///
/// # Result::<_, matrix_sdk::Error>::Ok(())
/// # });
/// ```
#[cfg(not(target_arch = "wasm32"))]
pub fn proxy(mut self, proxy: impl AsRef<str>) -> Self {
self.http_settings().proxy = Some(proxy.as_ref().to_owned());
self
}
/// Disable SSL verification for the HTTP requests.
#[cfg(not(target_arch = "wasm32"))]
pub fn disable_ssl_verification(mut self) -> Self {
self.http_settings().disable_ssl_verification = true;
self
}
/// Set a custom HTTP user agent for the client.
#[cfg(not(target_arch = "wasm32"))]
pub fn user_agent(mut self, user_agent: impl AsRef<str>) -> Self {
self.http_settings().user_agent = Some(user_agent.as_ref().to_owned());
self
}
/// Specify an HTTP client to handle sending requests and receiving
/// responses.
///
/// Any type that implements the `HttpSend` trait can be used to send /
/// receive `http` types.
///
/// This method is mutually exclusive with
/// [`user_agent()`][Self::user_agent],
pub fn http_client(mut self, client: Arc<dyn HttpSend>) -> Self {
self.http_cfg = Some(HttpConfig::Custom(client));
self
}
/// Puts the client into application service mode
///
/// This is low-level functionality. For an high-level API check the
/// `matrix_sdk_appservice` crate.
#[doc(hidden)]
#[cfg(feature = "appservice")]
pub fn appservice_mode(mut self) -> Self {
self.appservice_mode = true;
self
}
/// All outgoing http requests will have a GET query key-value appended with
/// `user_id` being the key and the `user_id` from the `Session` being
/// the value. Will error if there's no `Session`. This is called
/// [identity assertion] in the Matrix Application Service Spec
///
/// [identity assertion]: https://spec.matrix.org/unstable/application-service-api/#identity-assertion
#[doc(hidden)]
#[cfg(feature = "appservice")]
pub fn assert_identity(mut self) -> Self {
self.request_config.assert_identity = true;
self
}
/// Specify the Matrix versions supported by the homeserver manually, rather
/// than `build()` doing it using a `get_supported_versions` request.
///
/// This is helpful for test code that doesn't care to mock that endpoint.
pub fn server_versions(mut self, value: impl IntoIterator<Item = MatrixVersion>) -> Self {
self.server_versions = Some(value.into_iter().collect());
self
}
#[cfg(not(target_arch = "wasm32"))]
fn http_settings(&mut self) -> &mut HttpSettings {
self.http_cfg.get_or_insert_with(Default::default).settings()
}
/// Create a [`Client`] with the options set on this builder.
///
/// # Errors
///
/// This method can fail for two general reasons:
///
/// * Invalid input: a missing or invalid homeserver URL or invalid proxy
/// URL
/// * HTTP error: If you supplied a user ID instead of a homeserver URL, a
/// server discovery request is made which can fail; if you didn't set
/// [`server_versions(false)`][Self::server_versions], that amounts to
/// another request that can fail
pub async fn build(self) -> Result<Client, ClientBuildError> {
let homeserver_cfg = self.homeserver_cfg.ok_or(ClientBuildError::MissingHomeserver)?;
let inner_http_client = match self.http_cfg.unwrap_or_default() {
#[allow(unused_mut)]
HttpConfig::Settings(mut settings) => {
#[cfg(not(target_arch = "wasm32"))]
{
settings.timeout = self.request_config.timeout;
}
Arc::new(settings.make_client()?)
}
HttpConfig::Custom(c) => c,
};
let base_client = BaseClient::with_store_config(self.store_config);
let mk_http_client = |homeserver| {
HttpClient::new(
inner_http_client.clone(),
homeserver,
base_client.session().clone(),
self.request_config,
)
};
let homeserver = match homeserver_cfg {
HomeserverConfig::Url(url) => url,
HomeserverConfig::ServerName(server_name) => {
let homeserver = homeserver_from_name(&server_name)?;
let http_client = mk_http_client(Arc::new(RwLock::new(homeserver)));
let well_known = http_client
.send(
discover_homeserver::Request::new(),
None,
[MatrixVersion::V1_0].into_iter().collect(),
)
.await?;
well_known.homeserver.base_url
}
};
let homeserver = Arc::new(RwLock::new(Url::parse(&homeserver)?));
let http_client = mk_http_client(homeserver.clone());
let server_versions = match self.server_versions {
Some(vs) => vs,
None => http_client
.send(
get_supported_versions::Request::new(),
None,
[MatrixVersion::V1_0].into_iter().collect(),
)
.await?
.known_versions()
.collect(),
};
let inner = Arc::new(ClientInner {
homeserver,
http_client,
base_client,
server_versions,
#[cfg(feature = "encryption")]
group_session_locks: Default::default(),
#[cfg(feature = "encryption")]
key_claim_lock: Default::default(),
members_request_locks: Default::default(),
typing_notice_times: Default::default(),
event_handlers: Default::default(),
event_handler_data: Default::default(),
notification_handlers: Default::default(),
appservice_mode: self.appservice_mode,
respect_login_well_known: self.respect_login_well_known,
sync_beat: event_listener::Event::new(),
});
Ok(Client { inner })
}
}
fn homeserver_from_name(server_name: &ServerName) -> Result<Url, url::ParseError> {
#[cfg(not(test))]
let homeserver = format!("https://{}", server_name);
// Mockito only knows how to test http endpoints:
// https://github.com/lipanski/mockito/issues/127
#[cfg(test)]
let homeserver = format!("http://{}", server_name);
Url::parse(&homeserver)
}
#[derive(Debug)]
enum HomeserverConfig {
Url(String),
ServerName(Box<ServerName>),
}
#[derive(Debug)]
enum HttpConfig {
Settings(HttpSettings),
Custom(Arc<dyn HttpSend>),
}
#[cfg(not(target_arch = "wasm32"))]
impl HttpConfig {
fn settings(&mut self) -> &mut HttpSettings {
match self {
Self::Settings(s) => s,
Self::Custom(_) => {
*self = Self::default();
match self {
Self::Settings(s) => s,
Self::Custom(_) => unreachable!(),
}
}
}
}
}
impl Default for HttpConfig {
fn default() -> Self {
Self::Settings(HttpSettings::default())
}
}
/// Errors that can happen in [`ClientBuilder::build`].
#[derive(Debug, Error)]
pub enum ClientBuildError {
/// No homeserver or user ID was configured
#[error("no homeserver or user ID was configured")]
MissingHomeserver,
/// An error encountered when trying to parse the homeserver url.
#[error(transparent)]
Url(#[from] url::ParseError),
/// Error doing an HTTP request.
#[error(transparent)]
Http(#[from] HttpError),
}
impl ClientBuildError {
/// Assert that a valid homeserver URL was given to the builder and no other
/// invalid options were specified, which means the only possible error
/// case is [`Self::Http`].
#[doc(hidden)]
pub fn assert_valid_builder_args(self) -> HttpError {
match self {
ClientBuildError::Http(e) => e,
_ => unreachable!("homeserver URL was asserted to be valid"),
}
}
}

View File

@@ -44,7 +44,7 @@ use ruma::{
capabilities::{get_capabilities, Capabilities},
device::{delete_devices, get_devices},
directory::{get_public_rooms, get_public_rooms_filtered},
discover::{discover_homeserver, get_supported_versions},
discover::get_supported_versions,
filter::{create_filter::v3::Request as FilterUploadRequest, FilterDefinition},
media::{create_content, get_content, get_content_thumbnail},
membership::{join_room_by_id, join_room_by_id_or_alias},
@@ -65,15 +65,21 @@ use serde::de::DeserializeOwned;
use tracing::{error, info, instrument, warn};
use url::Url;
#[cfg(feature = "encryption")]
use crate::encryption::Encryption;
use crate::{
attachment::{AttachmentInfo, Thumbnail},
config::{ClientConfig, RequestConfig},
config::RequestConfig,
error::{HttpError, HttpResult},
event_handler::{EventHandler, EventHandlerData, EventHandlerResult, EventKind, SyncEvent},
http_client::{client_with_config, HttpClient},
http_client::HttpClient,
room, Account, Error, Result,
};
mod builder;
pub use self::builder::{ClientBuildError, ClientBuilder};
/// A conservative upload speed of 1Mbps
const DEFAULT_UPLOAD_SPEED: u64 = 125_000;
/// 5 min minimal upload request timeout, used to clamp the request timeout.
@@ -141,7 +147,7 @@ pub(crate) struct ClientInner {
appservice_mode: bool,
/// Whether the client should update its homeserver URL with the discovery
/// information present in the login response.
use_discovery_response: bool,
respect_login_well_known: bool,
/// An event that can be listened on to wait for a successful sync. The
/// event will only be fired if a sync loop is running. Can be used for
/// synchronization, e.g. if we send out a request to create a room, we can
@@ -163,130 +169,17 @@ impl Client {
/// # Arguments
///
/// * `homeserver_url` - The homeserver that the client should connect to.
pub async fn new(homeserver_url: Url) -> Result<Self> {
let config = ClientConfig::new().await?;
Client::new_with_config(homeserver_url, config).await
pub async fn new(homeserver_url: Url) -> Result<Self, HttpError> {
Self::builder()
.homeserver_url(homeserver_url)
.build()
.await
.map_err(ClientBuildError::assert_valid_builder_args)
}
/// Create a new [`Client`] for the given homeserver and use the given
/// configuration.
///
/// # Arguments
///
/// * `homeserver_url` - The homeserver that the client should connect to.
///
/// * `config` - Configuration for the client.
pub async fn new_with_config(homeserver_url: Url, config: ClientConfig) -> Result<Self> {
let homeserver = Arc::new(RwLock::new(homeserver_url));
let client = if let Some(client) = config.client {
client
} else {
Arc::new(client_with_config(&config)?)
};
let base_client = BaseClient::new_with_config(config.base_config).await?;
let session = base_client.session().clone();
let http_client =
HttpClient::new(client, homeserver.clone(), session, config.request_config);
let server_versions = match config.server_versions {
Some(vs) => vs,
None => http_client
.send(
get_supported_versions::Request::new(),
None,
vec![MatrixVersion::V1_0].into(),
)
.await?
.known_versions()
.collect(),
};
let inner = Arc::new(ClientInner {
homeserver,
http_client,
base_client,
server_versions,
#[cfg(feature = "encryption")]
group_session_locks: Default::default(),
#[cfg(feature = "encryption")]
key_claim_lock: Default::default(),
members_request_locks: Default::default(),
typing_notice_times: Default::default(),
event_handlers: Default::default(),
event_handler_data: Default::default(),
notification_handlers: Default::default(),
appservice_mode: config.appservice_mode,
use_discovery_response: config.use_discovery_response,
sync_beat: event_listener::Event::new(),
});
Ok(Self { inner })
}
/// Create a new [`Client`] using homeserver auto discovery.
///
/// This method will create a [`Client`] object that will attempt to
/// discover and configure the homeserver for the given user. Follows the
/// homeserver discovery directions described in the [spec].
///
/// # Arguments
///
/// * `user_id` - The id of the user whose homeserver the client should
/// connect to.
///
/// # Example
/// ```no_run
/// # use std::convert::TryFrom;
/// # use futures::executor::block_on;
/// # block_on(async {
/// use matrix_sdk::{Client, ruma::UserId};
///
/// // First let's try to construct an user id, presumably from user input.
/// let alice = UserId::parse("@alice:example.org")?;
///
/// // Now let's try to discover the homeserver and create a client object.
/// let client = Client::new_from_user_id(&alice).await?;
///
/// // Finally let's try to login.
/// client.login(alice, "password", None, None).await?;
/// # Result::<_, matrix_sdk::Error>::Ok(()) });
/// ```
///
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#well-known-uri
pub async fn new_from_user_id(user_id: &UserId) -> Result<Self> {
let config = ClientConfig::new().await?;
Client::new_from_user_id_with_config(user_id, config).await
}
/// Create a new [`Client`] using homeserver auto discovery.
///
/// This method will create a [`Client`] object that will attempt to
/// discover and configure the homeserver for the given user. Follows the
/// homeserver discovery directions described in the [spec].
///
/// # Arguments
///
/// * `user_id` - The id of the user whose homeserver the client should
/// connect to.
///
/// * `config` - Configuration for the client.
///
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#well-known-uri
pub async fn new_from_user_id_with_config(
user_id: &UserId,
config: ClientConfig,
) -> Result<Self> {
let homeserver = Client::homeserver_from_user_id(user_id)?;
let client = Client::new_with_config(homeserver, config).await?;
let well_known = client.discover_homeserver().await?;
let well_known = Url::parse(well_known.homeserver.base_url.as_ref())?;
client.set_homeserver(well_known).await;
client.get_supported_versions().await?;
Ok(client)
/// Create a new [`ClientBuilder`].
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
pub(crate) fn base_client(&self) -> &BaseClient {
@@ -307,22 +200,6 @@ impl Client {
self.base_client().mark_request_as_sent(request_id, response).await
}
fn homeserver_from_user_id(user_id: &UserId) -> Result<Url> {
let homeserver = format!("https://{}", user_id.server_name());
#[allow(unused_mut)]
let mut result = Url::parse(homeserver.as_str())?;
// Mockito only knows how to test http endpoints:
// https://github.com/lipanski/mockito/issues/127
#[cfg(test)]
let _ = result.set_scheme("http");
Ok(result)
}
async fn discover_homeserver(&self) -> HttpResult<discover_homeserver::Response> {
self.send(discover_homeserver::Request::new(), Some(RequestConfig::new().disable_retry()))
.await
}
/// Change the homeserver URL used by this client.
///
/// # Arguments
@@ -445,6 +322,12 @@ impl Client {
Account::new(self.clone())
}
/// Get the encryption manager of the client.
#[cfg(feature = "encryption")]
pub fn encryption(&self) -> Encryption {
Encryption::new(self.clone())
}
/// Register a handler for a specific event type.
///
/// The handler is a function or closure with one or more arguments. The
@@ -488,7 +371,12 @@ impl Client {
/// use serde::{Deserialize, Serialize};
///
/// # block_on(async {
/// # let client = Client::new(homeserver).await.unwrap();
/// # let client = matrix_sdk::Client::builder()
/// # .homeserver_url(homeserver)
/// # .check_supported_versions(false)
/// # .build()
/// # .await
/// # .unwrap();
/// client
/// .register_event_handler(
/// |ev: SyncRoomMessageEvent, room: Room, client: Client| async move {
@@ -602,7 +490,12 @@ impl Client {
/// # fn obtain_gui_handle() -> SomeType { SomeType }
/// # let homeserver = url::Url::parse("http://localhost:8080").unwrap();
/// # block_on(async {
/// # let client = matrix_sdk::Client::new(homeserver).await.unwrap();
/// # let client = matrix_sdk::Client::builder()
/// # .homeserver_url(homeserver)
/// # .check_supported_versions(false)
/// # .build()
/// # .await
/// # .unwrap();
///
/// // Handle used to send messages to the UI part of the app
/// let my_gui_handle: SomeType = obtain_gui_handle();
@@ -1149,7 +1042,7 @@ impl Client {
///
/// * `response` - A successful login response.
async fn receive_login_response(&self, response: &login::v3::Response) -> Result<()> {
if self.inner.use_discovery_response {
if self.inner.respect_login_well_known {
if let Some(well_known) = &response.well_known {
if let Ok(homeserver) = Url::parse(&well_known.homeserver.base_url) {
self.set_homeserver(homeserver).await;
@@ -2344,6 +2237,7 @@ impl Client {
})
}
}
// mockito (the http mocking library) is not supported for wasm32
#[cfg(all(test, not(target_arch = "wasm32")))]
pub(crate) mod test {
@@ -2382,35 +2276,43 @@ pub(crate) mod test {
message::{ImageMessageEventContent, RoomMessageEventContent},
ImageInfo,
},
AnySyncStateEvent, EventType,
AnySyncStateEvent, StateEventType,
},
mxc_uri, room_id, thirdparty, uint, user_id, TransactionId, UserId,
};
use serde_json::json;
use url::Url;
use super::{Client, Session, Url};
use super::{Client, ClientBuilder, Session};
use crate::{
attachment::{
AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo,
Thumbnail,
},
config::{ClientConfig, RequestConfig, SyncSettings},
config::{RequestConfig, SyncSettings},
HttpError, RoomMember,
};
fn test_client_builder() -> ClientBuilder {
let homeserver = Url::parse(&mockito::server_url()).unwrap();
Client::builder().homeserver_url(homeserver).server_versions([MatrixVersion::V1_0])
}
async fn no_retry_test_client() -> Client {
test_client_builder()
.request_config(RequestConfig::new().disable_retry())
.build()
.await
.unwrap()
}
pub(crate) async fn logged_in_client() -> Client {
let session = Session {
access_token: "1234".to_owned(),
user_id: user_id!("@example:localhost").to_owned(),
device_id: device_id!("DEVICEID").to_owned(),
};
let homeserver = url::Url::parse(&mockito::server_url()).unwrap();
let config = ClientConfig::new()
.await
.unwrap()
.request_config(RequestConfig::new().disable_retry())
.server_versions([MatrixVersion::V1_0]);
let client = Client::new_with_config(homeserver, config).await.unwrap();
let client = no_retry_test_client().await;
client.restore_login(session).await.unwrap();
client
@@ -2418,12 +2320,8 @@ pub(crate) mod test {
#[async_test]
async fn set_homeserver() {
let client = no_retry_test_client().await;
let homeserver = Url::from_str("http://example.com/").unwrap();
let client = Client::new(homeserver).await.unwrap();
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
client.set_homeserver(homeserver.clone()).await;
assert_eq!(client.homeserver().await, homeserver);
@@ -2446,7 +2344,7 @@ pub(crate) mod test {
.with_status(200)
.with_body(test_json::VERSIONS.to_string())
.create();
let client = Client::new_from_user_id(&alice).await.unwrap();
let client = Client::builder().user_id(&alice).build().await.unwrap();
assert_eq!(client.homeserver().await, Url::parse(server_url.as_ref()).unwrap());
}
@@ -2465,7 +2363,7 @@ pub(crate) mod test {
.create();
assert!(
Client::new_from_user_id(&alice).await.is_err(),
Client::builder().user_id(&alice).build().await.is_err(),
"Creating a client from a user ID should fail when the \
.well-known server returns no version information."
);
@@ -2474,9 +2372,7 @@ pub(crate) mod test {
#[async_test]
async fn login() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
let config = ClientConfig::new().await.unwrap().server_versions([MatrixVersion::V1_0]);
let client = Client::new_with_config(homeserver.clone(), config).await.unwrap();
let client = no_retry_test_client().await;
let _m_types = mock("GET", "/_matrix/client/r0/login")
.with_status(200)
@@ -2507,14 +2403,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()
.await
.unwrap()
.use_discovery_response()
.server_versions([MatrixVersion::V1_0]);
let client = Client::new_with_config(homeserver, config).await.unwrap();
let client = no_retry_test_client().await;
let _m_login = mock("POST", "/_matrix/client/r0/login")
.with_status(200)
@@ -2531,14 +2420,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()
.await
.unwrap()
.use_discovery_response()
.server_versions([MatrixVersion::V1_0]);
let client = Client::new_with_config(homeserver.clone(), config).await.unwrap();
let client = no_retry_test_client().await;
let _m_login = mock("POST", "/_matrix/client/r0/login")
.with_status(200)
@@ -2550,7 +2432,7 @@ pub(crate) mod test {
let logged_in = client.logged_in().await;
assert!(logged_in, "Client should be logged in");
assert_eq!(client.homeserver().await, homeserver);
assert_eq!(client.homeserver().await, Url::parse(&mockito::server_url()).unwrap());
}
#[cfg(feature = "sso_login")]
@@ -2562,8 +2444,7 @@ pub(crate) mod test {
.create();
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
let config = ClientConfig::new().await.unwrap().server_versions([MatrixVersion::V1_0]);
let client = Client::new_with_config(homeserver, config).await.unwrap();
let client = no_retry_test_client().await;
let idp = crate::client::get_login_types::v3::IdentityProvider::new(
"some-id".to_owned(),
"idp-name".to_owned(),
@@ -2598,10 +2479,7 @@ pub(crate) mod test {
#[async_test]
async fn login_with_sso_token() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
let config = ClientConfig::new().await.unwrap().server_versions([MatrixVersion::V1_0]);
let client = Client::new_with_config(homeserver, config).await.unwrap();
let client = no_retry_test_client().await;
let _m = mock("GET", "/_matrix/client/r0/login")
.with_status(200)
@@ -2645,7 +2523,6 @@ pub(crate) mod test {
#[async_test]
async fn test_join_leave_room() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost");
let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned()))
@@ -2667,13 +2544,8 @@ pub(crate) mod test {
let room = client.get_joined_room(room_id);
assert!(room.is_some());
// test store reloads with correct room state from the sled store
let path = tempfile::tempdir().unwrap();
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();
// test store reloads with correct room state from the state store
let joined_client = no_retry_test_client().await;
joined_client.restore_login(session).await.unwrap();
// joined room reloaded from state store
@@ -2733,9 +2605,7 @@ pub(crate) mod test {
#[async_test]
async fn login_error() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
let config = ClientConfig::default().request_config(RequestConfig::new().disable_retry());
let client = Client::new_with_config(homeserver, config).await.unwrap();
let client = no_retry_test_client().await;
let _m = mock("POST", "/_matrix/client/r0/login")
.with_status(403)
@@ -2763,8 +2633,7 @@ pub(crate) mod test {
#[async_test]
async fn register_error() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
let client = Client::new(homeserver).await.unwrap();
let client = no_retry_test_client().await;
let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/register\?.*$".to_owned()))
.with_status(403)
@@ -2907,8 +2776,7 @@ pub(crate) mod test {
#[async_test]
async fn room_search_all() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
let client = Client::new(homeserver).await.unwrap();
let client = no_retry_test_client().await;
let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/publicRooms".to_owned()))
.with_status(200)
@@ -3593,8 +3461,7 @@ pub(crate) mod test {
#[async_test]
async fn delete_devices() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
let client = Client::new(homeserver).await.unwrap();
let client = no_retry_test_client().await;
let _m = mock("POST", "/_matrix/client/r0/delete_devices")
.with_status(401)
@@ -3649,10 +3516,13 @@ pub(crate) mod test {
#[async_test]
async fn retry_limit_http_requests() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
let config = ClientConfig::default().request_config(RequestConfig::new().retry_limit(3));
assert!(config.request_config.retry_limit.unwrap() == 3);
let client = Client::new_with_config(homeserver, config).await.unwrap();
let client = test_client_builder()
.request_config(RequestConfig::new().retry_limit(3))
.build()
.await
.unwrap();
assert!(client.inner.http_client.request_config.retry_limit.unwrap() == 3);
let m = mock("POST", "/_matrix/client/r0/login").with_status(501).expect(3).create();
@@ -3665,13 +3535,15 @@ pub(crate) mod test {
#[async_test]
async fn retry_timeout_http_requests() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
// Keep this timeout small so that the test doesn't take long
let retry_timeout = Duration::from_secs(5);
let config = ClientConfig::default()
.request_config(RequestConfig::new().retry_timeout(retry_timeout));
assert!(config.request_config.retry_timeout.unwrap() == retry_timeout);
let client = Client::new_with_config(homeserver, config).await.unwrap();
let client = test_client_builder()
.request_config(RequestConfig::new().retry_timeout(retry_timeout))
.build()
.await
.unwrap();
assert!(client.inner.http_client.request_config.retry_timeout.unwrap() == retry_timeout);
let m =
mock("POST", "/_matrix/client/r0/login").with_status(501).expect_at_least(2).create();
@@ -3685,8 +3557,7 @@ pub(crate) mod test {
#[async_test]
async fn short_retry_initial_http_requests() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
let client = Client::new(homeserver).await.unwrap();
let client = test_client_builder().build().await.unwrap();
let m =
mock("POST", "/_matrix/client/r0/login").with_status(501).expect_at_least(3).create();
@@ -3798,7 +3669,6 @@ pub(crate) mod test {
#[async_test]
async fn test_state_event_getting() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost");
let session = Session {
@@ -3867,8 +3737,11 @@ pub(crate) mod test {
.with_body(sync.to_string())
.create();
let config = ClientConfig::default().request_config(RequestConfig::new().retry_limit(3));
let client = Client::new_with_config(homeserver.clone(), config).await.unwrap();
let client = test_client_builder()
.request_config(RequestConfig::new().retry_limit(3))
.build()
.await
.unwrap();
client.restore_login(session.clone()).await.unwrap();
let room = client.get_joined_room(room_id);
@@ -3878,14 +3751,14 @@ pub(crate) mod test {
let room = client.get_joined_room(room_id).unwrap();
let state_events = room.get_state_events(EventType::RoomEncryption).await.unwrap();
let state_events = room.get_state_events(StateEventType::RoomEncryption).await.unwrap();
assert_eq!(state_events.len(), 1);
let state_events = room.get_state_events("m.custom.note".into()).await.unwrap();
assert_eq!(state_events.len(), 2);
let encryption_event = room
.get_state_event(EventType::RoomEncryption, "")
.get_state_event(StateEventType::RoomEncryption, "")
.await
.unwrap()
.unwrap()

View File

@@ -1,293 +0,0 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[allow(unused_imports)]
use std::{
fmt::{self, Debug},
path::Path,
sync::Arc,
};
use http::header::InvalidHeaderValue;
use matrix_sdk_base::{BaseClientConfig, StateStore};
use ruma::api::MatrixVersion;
use crate::{config::RequestConfig, HttpSend, Result};
/// Configuration for the creation of the `Client`.
///
/// When setting the `StateStore` it is up to the user to open/connect
/// the storage backend before client creation.
///
/// # Example
///
/// ```
/// use matrix_sdk::config::ClientConfig;
/// // To pass all the request through mitmproxy set the proxy and disable SSL
/// // verification
///
/// # 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
/// Note: setting a custom client will ignore `user_agent`, `proxy`, and
/// `disable_ssl_verification` - you'd need to set these yourself if you
/// want them.
///
/// ```
/// use matrix_sdk::config::ClientConfig;
/// use reqwest::ClientBuilder;
/// use std::sync::Arc;
///
/// // setting up a custom builder
/// let builder = ClientBuilder::new()
/// .https_only(true)
/// .no_proxy()
/// .user_agent("MyApp/v3.0");
///
/// # 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 {
#[cfg(not(target_arch = "wasm32"))]
pub(crate) proxy: Option<reqwest::Proxy>,
pub(crate) user_agent: Option<String>,
pub(crate) disable_ssl_verification: bool,
pub(crate) base_config: BaseClientConfig,
pub(crate) request_config: RequestConfig,
pub(crate) client: Option<Arc<dyn HttpSend>>,
pub(crate) appservice_mode: bool,
pub(crate) use_discovery_response: bool,
pub(crate) server_versions: Option<Arc<[MatrixVersion]>>,
}
#[cfg(not(tarpaulin_include))]
impl Debug for ClientConfig {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut res = fmt.debug_struct("ClientConfig");
#[cfg(not(target_arch = "wasm32"))]
let res = res.field("proxy", &self.proxy);
res.field("user_agent", &self.user_agent)
.field("disable_ssl_verification", &self.disable_ssl_verification)
.field("request_config", &self.request_config)
.finish()
}
}
#[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<Box<StateStore>> {
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<Box<StateStore>> {
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<Box<StateStore>> {
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<Box<StateStore>> {
Ok(Box::new(match passphrase {
Some(pass) => StateStore::open_with_passphrase(name.to_owned(), pass).await?,
_ => StateStore::open_with_name(name.to_owned()).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<Box<StateStore>> {
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<Box<StateStore>> {
Ok(Box::new(StateStore::new()))
}
}
pub use store_helpers::{default_store, default_store_with_name};
impl ClientConfig {
/// Create a new default `ClientConfig`.
pub async fn new() -> Result<Self> {
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<Self> {
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.
///
/// Note, only HTTP proxies are supported.
///
/// # Arguments
///
/// * `proxy` - The HTTP URL of the proxy.
///
/// # Example
///
/// ```
/// # futures::executor::block_on(async {
/// use matrix_sdk::{Client, config::ClientConfig};
///
/// 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<Self> {
self.proxy = Some(reqwest::Proxy::all(proxy)?);
Ok(self)
}
/// Disable SSL verification for the HTTP requests.
#[must_use]
pub fn disable_ssl_verification(mut self) -> Self {
self.disable_ssl_verification = true;
self
}
/// Set a custom HTTP user agent for the client.
pub fn user_agent(mut self, user_agent: &str) -> Result<Self, InvalidHeaderValue> {
self.user_agent = Some(user_agent.to_owned());
Ok(self)
}
/// Set a custom implementation of a `StateStore`.
///
/// The state store should be opened before being set.
pub fn state_store(mut self, store: Box<dyn StateStore>) -> Self {
self.base_config = self.base_config.state_store(store);
self
}
/// Set the default timeout, fail and retry behavior for all HTTP requests.
#[must_use]
pub fn request_config(mut self, request_config: RequestConfig) -> Self {
self.request_config = request_config;
self
}
/// Get the [`RequestConfig`]
pub fn get_request_config(&self) -> &RequestConfig {
&self.request_config
}
/// Specify a client to handle sending requests and receiving responses.
///
/// Any type that implements the `HttpSend` trait can be used to
/// send/receive `http` types.
#[must_use]
pub fn client(mut self, client: Arc<dyn HttpSend>) -> Self {
self.client = Some(client);
self
}
/// Puts the client into application service mode
///
/// This is low-level functionality. For an high-level API check the
/// `matrix_sdk_appservice` crate.
#[cfg(feature = "appservice")]
#[must_use]
pub fn appservice_mode(mut self) -> Self {
self.appservice_mode = true;
self
}
/// Set a custom implementation of a `CryptoStore`.
///
/// The crypto store should be opened before being set.
#[cfg(feature = "encryption")]
#[must_use]
pub fn crypto_store(
mut self,
store: Box<dyn matrix_sdk_base::crypto::store::CryptoStore>,
) -> Self {
self.base_config = self.base_config.crypto_store(store);
self
}
/// Update the client's homeserver URL with the discovery information
/// present in the login response, if any.
#[must_use]
pub fn use_discovery_response(mut self) -> Self {
self.use_discovery_response = true;
self
}
#[cfg(test)]
pub(crate) fn server_versions(
mut self,
server_versions: impl IntoIterator<Item = MatrixVersion>,
) -> Self {
self.server_versions = Some(server_versions.into_iter().collect());
self
}
}

View File

@@ -12,14 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Configuration to change the behaviour of the [`Client`].
//!
//! [`Client`]: #crate.Client
//! Configuration to change the behaviour of the [`Client`][crate::Client].
mod client;
mod request;
mod sync;
pub use client::{default_store, default_store_with_name, ClientConfig};
pub use matrix_sdk_base::store::StoreConfig;
pub use request::RequestConfig;
pub use sync::SyncSettings;

View File

@@ -17,7 +17,7 @@ use std::{
time::Duration,
};
const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
use crate::http_client::DEFAULT_REQUEST_TIMEOUT;
/// Configuration for requests the `Client` makes.
///
@@ -121,17 +121,4 @@ impl RequestConfig {
self.force_auth = true;
self
}
/// All outgoing http requests will have a GET query key-value appended with
/// `user_id` being the key and the `user_id` from the `Session` being
/// the value. Will error if there's no `Session`. This is called
/// [identity assertion] in the Matrix Application Service Spec
///
/// [identity assertion]: https://spec.matrix.org/unstable/application-service-api/#identity-assertion
#[cfg(feature = "appservice")]
#[must_use]
pub fn assert_identity(mut self) -> Self {
self.assert_identity = true;
self
}
}

View File

@@ -87,7 +87,7 @@ stored, otherwise we won't be able to decrypt historical messages. The SDK
stores all room keys locally in a encrypted manner.
Besides storing them as part of the SDK store, users can export room keys
using the [`Client::export_keys`] method.
using the [`Encryption::export_keys`] method.
# Verification
@@ -165,8 +165,10 @@ unverified devices, verifying devices is **not** necessary for encryption
to work.
1. Make sure the `encryption` feature is enabled.
2. Ensure you have a persistent storage backend, either by activating the
`sled_state_store`-feature or providing one via [`ClientConfig.state_store`]
2. To persist the encryption keys, you can use one of the provided backend
constructors as described in the documentation of the [`store`] module or you
can provide your own backend that implements [`CryptoStore`] in a
[`StoreConfig`] or via [`ClientBuilder::crypto_store()`].
## Restoring a client
@@ -228,3 +230,7 @@ is **not** supported using the default store.
[Restoring a Client]: #restoring-a-client
[spec]: https://spec.matrix.org/unstable/client-server-api/#relationship-between-access-tokens-and-devices
[device keys]: https://spec.matrix.org/unstable/client-server-api/#device-keys
[`store`]: crate::store
[`CryptoStore`]: matrix_sdk_base::crypto::store::CryptoStore
[`StoreConfig`]: crate::config::StoreConfig
[`ClientBuilder::crypto_store()`]: crate::ClientBuilder::crypto_store()

View File

@@ -89,7 +89,7 @@ impl Device {
/// # let alice = user_id!("@alice:example.org");
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let device = client.get_device(alice, device_id!("DEVICEID")).await?;
/// let device = client.encryption().get_device(alice, device_id!("DEVICEID")).await?;
///
/// if let Some(device) = device {
/// let verification = device.request_verification().await?;
@@ -137,7 +137,7 @@ impl Device {
/// # let alice = user_id!("@alice:example.org");
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let device = client.get_device(alice, device_id!("DEVICEID")).await?;
/// let device = client.encryption().get_device(alice, device_id!("DEVICEID")).await?;
///
/// // We don't want to support showing a QR code, we only support SAS
/// // verification
@@ -179,7 +179,7 @@ impl Device {
/// # let alice = user_id!("@alice:example.org");
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let device = client.get_device(alice, device_id!("DEVICEID")).await?;
/// let device = client.encryption().get_device(alice, device_id!("DEVICEID")).await?;
///
/// if let Some(device) = device {
/// let verification = device.start_verification().await?;
@@ -212,9 +212,9 @@ impl Device {
/// key.
///
/// The state of our private cross signing keys can be inspected using the
/// [`Client::cross_signing_status()`] method.
/// [`Encryption::cross_signing_status()`] method.
///
/// [`Client::cross_signing_status()`]: crate::Client::cross_signing_status
/// [`Encryption::cross_signing_status()`]: crate::encryption::Encryption::cross_signing_status
///
/// ### Problems of manual verification
///
@@ -243,7 +243,7 @@ impl Device {
/// # let alice = user_id!("@alice:example.org");
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let device = client.get_device(alice, device_id!("DEVICEID")).await?;
/// let device = client.encryption().get_device(alice, device_id!("DEVICEID")).await?;
///
/// if let Some(device) = device {
/// device.verify().await?;
@@ -358,7 +358,7 @@ impl Device {
/// # let alice = user_id!("@alice:example.org");
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let device = client.get_device(alice, device_id!("DEVICEID")).await?;
/// let device = client.encryption().get_device(alice, device_id!("DEVICEID")).await?;
///
/// if let Some(device) = device {
/// if device.verified() {

View File

@@ -43,7 +43,7 @@
//! # let homeserver = Url::parse("http://example.com").unwrap();
//! # block_on(async {
//! # let client = Client::new(homeserver).await.unwrap();
//! let device = client.get_device(alice, device_id!("DEVICEID")).await?;
//! let device = client.encryption().get_device(alice, device_id!("DEVICEID")).await?;
//!
//! if let Some(device) = device {
//! // Let's request the device to be verified.
@@ -69,7 +69,7 @@
//! # let homeserver = Url::parse("http://example.com").unwrap();
//! # block_on(async {
//! # let client = Client::new(homeserver).await.unwrap();
//! let user = client.get_user_identity(alice).await?;
//! let user = client.encryption().get_user_identity(alice).await?;
//!
//! if let Some(user) = user {
//! // Let's request the user to be verified.

View File

@@ -35,8 +35,8 @@ use crate::{encryption::verification::VerificationRequest, room::Joined, Client}
///
/// The identity is backed by public [cross signing] keys that users upload. If
/// our own user doesn't yet have such an identity, a new one can be created and
/// uploaded to the server using [`Client::bootstrap_cross_signing()`]. The user
/// identity can be also reset using the same method.
/// uploaded to the server using [`Encryption::bootstrap_cross_signing()`]. The
/// user identity can be also reset using the same method.
///
/// The user identity consists of three separate `Ed25519` keypairs:
///
@@ -63,6 +63,7 @@ use crate::{encryption::verification::VerificationRequest, room::Joined, Client}
/// let us know whom the user verified.
///
/// [cross signing]: https://spec.matrix.org/unstable/client-server-api/#cross-signing
/// [`Encryption::bootstrap_cross_signing()`]: crate::encryption::Encryption::bootstrap_cross_signing
#[derive(Debug, Clone)]
pub struct UserIdentity {
inner: UserIdentities,
@@ -97,7 +98,7 @@ impl UserIdentity {
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # futures::executor::block_on(async {
/// # let client = Client::new(homeserver).await.unwrap();
/// let user = client.get_user_identity(alice).await?;
/// let user = client.encryption().get_user_identity(alice).await?;
///
/// if let Some(user) = user {
/// println!("This user identity belongs to {}", user.user_id().as_str());
@@ -149,7 +150,7 @@ impl UserIdentity {
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # futures::executor::block_on(async {
/// # let client = Client::new(homeserver).await.unwrap();
/// let user = client.get_user_identity(alice).await?;
/// let user = client.encryption().get_user_identity(alice).await?;
///
/// if let Some(user) = user {
/// let verification = user.request_verification().await?;
@@ -208,7 +209,7 @@ impl UserIdentity {
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # block_on(async {
/// # let client = Client::new(homeserver).await.unwrap();
/// let user = client.get_user_identity(alice).await?;
/// let user = client.encryption().get_user_identity(alice).await?;
///
/// // We don't want to support showing a QR code, we only support SAS
/// // verification
@@ -252,7 +253,7 @@ impl UserIdentity {
/// course fail if the private part of the User-signing key isn't available.
///
/// The availability of the User-signing key can be checked using the
/// [`Client::cross_signing_status()`] method.
/// [`Encryption::cross_signing_status()`] method.
///
/// ### Manually verifying our own user
///
@@ -287,13 +288,14 @@ impl UserIdentity {
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # block_on(async {
/// # let client = Client::new(homeserver).await.unwrap();
/// let user = client.get_user_identity(alice).await?;
/// let user = client.encryption().get_user_identity(alice).await?;
///
/// if let Some(user) = user {
/// user.verify().await?;
/// }
/// # anyhow::Result::<()>::Ok(()) });
/// ```
/// [`Encryption::cross_signing_status()`]: crate::encryption::Encryption::cross_signing_status
pub async fn verify(&self) -> Result<(), ManualVerifyError> {
match &self.inner {
UserIdentities::Own(i) => i.verify().await,
@@ -330,7 +332,7 @@ impl UserIdentity {
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # block_on(async {
/// # let client = Client::new(homeserver).await.unwrap();
/// let user = client.get_user_identity(alice).await?;
/// let user = client.encryption().get_user_identity(alice).await?;
///
/// if let Some(user) = user {
/// if user.verified() {
@@ -370,7 +372,7 @@ impl UserIdentity {
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # block_on(async {
/// # let client = Client::new(homeserver).await.unwrap();
/// let user = client.get_user_identity(alice).await?;
/// let user = client.encryption().get_user_identity(alice).await?;
///
/// if let Some(user) = user {
/// // Let's verify the user after we confirm that the master key

View File

@@ -48,7 +48,7 @@ use ruma::{
uiaa::AuthData,
},
assign,
events::{AnyMessageEvent, AnyRoomEvent, AnySyncMessageEvent, EventType},
events::{AnyMessageEvent, AnyRoomEvent, AnySyncMessageEvent, GlobalAccountDataEventType},
serde::Raw,
DeviceId, TransactionId, UserId,
};
@@ -65,395 +65,6 @@ use crate::{
};
impl Client {
/// Get the public ed25519 key of our own device. This is usually what is
/// called the fingerprint of the device.
#[cfg(feature = "encryption")]
pub async fn ed25519_key(&self) -> Option<String> {
self.olm_machine().await.map(|o| o.identity_keys().ed25519().to_owned())
}
/// Get the status of the private cross signing keys.
///
/// This can be used to check which private cross signing keys we have
/// stored locally.
#[cfg(feature = "encryption")]
pub async fn cross_signing_status(&self) -> Option<CrossSigningStatus> {
if let Some(machine) = self.olm_machine().await {
Some(machine.cross_signing_status().await)
} else {
None
}
}
/// Get all the tracked users we know about
///
/// Tracked users are users for which we keep the device list of E2EE
/// capable devices up to date.
#[cfg(feature = "encryption")]
pub async fn tracked_users(&self) -> HashSet<Box<UserId>> {
self.olm_machine().await.map(|o| o.tracked_users()).unwrap_or_default()
}
/// Get a verification object with the given flow id.
#[cfg(feature = "encryption")]
pub async fn get_verification(&self, user_id: &UserId, flow_id: &str) -> Option<Verification> {
let olm = self.olm_machine().await?;
olm.get_verification(user_id, flow_id).map(|v| match v {
matrix_sdk_base::crypto::Verification::SasV1(s) => {
SasVerification { inner: s, client: self.clone() }.into()
}
#[cfg(feature = "qrcode")]
matrix_sdk_base::crypto::Verification::QrV1(qr) => {
verification::QrVerification { inner: qr, client: self.clone() }.into()
}
})
}
/// Get a `VerificationRequest` object for the given user with the given
/// flow id.
#[cfg(feature = "encryption")]
pub async fn get_verification_request(
&self,
user_id: &UserId,
flow_id: impl AsRef<str>,
) -> Option<VerificationRequest> {
let olm = self.olm_machine().await?;
olm.get_verification_request(user_id, flow_id)
.map(|r| VerificationRequest { inner: r, client: self.clone() })
}
/// Get a specific device of a user.
///
/// # Arguments
///
/// * `user_id` - The unique id of the user that the device belongs to.
///
/// * `device_id` - The unique id of the device.
///
/// Returns a `Device` if one is found and the crypto store didn't throw an
/// error.
///
/// This will always return None if the client hasn't been logged in.
///
/// # Example
///
/// ```no_run
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, ruma::{device_id, user_id}};
/// # use url::Url;
/// # use futures::executor::block_on;
/// # block_on(async {
/// # let alice = user_id!("@alice:example.org");
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// if let Some(device) = client.get_device(alice, device_id!("DEVICEID")).await? {
/// println!("{:?}", device.verified());
///
/// if !device.verified() {
/// let verification = device.request_verification().await?;
/// }
/// }
/// # anyhow::Result::<()>::Ok(()) });
/// ```
#[cfg(feature = "encryption")]
pub async fn get_device(
&self,
user_id: &UserId,
device_id: &DeviceId,
) -> Result<Option<Device>, CryptoStoreError> {
let device = self.base_client().get_device(user_id, device_id).await?;
Ok(device.map(|d| Device { inner: d, client: self.clone() }))
}
/// Get a map holding all the devices of an user.
///
/// This will always return an empty map if the client hasn't been logged
/// in.
///
/// # Arguments
///
/// * `user_id` - The unique id of the user that the devices belong to.
///
/// # Example
///
/// ```no_run
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, ruma::user_id};
/// # use url::Url;
/// # use futures::executor::block_on;
/// # block_on(async {
/// # let alice = user_id!("@alice:example.org");
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let devices = client.get_user_devices(alice).await?;
///
/// for device in devices.devices() {
/// println!("{:?}", device);
/// }
/// # anyhow::Result::<()>::Ok(()) });
/// ```
#[cfg(feature = "encryption")]
pub async fn get_user_devices(
&self,
user_id: &UserId,
) -> Result<UserDevices, CryptoStoreError> {
let devices = self.base_client().get_user_devices(user_id).await?;
Ok(UserDevices { inner: devices, client: self.clone() })
}
/// Get a E2EE identity of an user.
///
/// # Arguments
///
/// * `user_id` - The unique id of the user that the identity belongs to.
///
/// Returns a `UserIdentity` if one is found and the crypto store
/// didn't throw an error.
///
/// This will always return None if the client hasn't been logged in.
///
/// # Example
///
/// ```no_run
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, ruma::user_id};
/// # use url::Url;
/// # use futures::executor::block_on;
/// # block_on(async {
/// # let alice = user_id!("@alice:example.org");
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let user = client.get_user_identity(alice).await?;
///
/// if let Some(user) = user {
/// println!("{:?}", user.verified());
///
/// let verification = user.request_verification().await?;
/// }
/// # anyhow::Result::<()>::Ok(()) });
/// ```
#[cfg(feature = "encryption")]
pub async fn get_user_identity(
&self,
user_id: &UserId,
) -> Result<Option<crate::encryption::identities::UserIdentity>, CryptoStoreError> {
use crate::encryption::identities::UserIdentity;
if let Some(olm) = self.olm_machine().await {
let identity = olm.get_identity(user_id).await?;
Ok(identity.map(|i| match i {
matrix_sdk_base::crypto::UserIdentities::Own(i) => {
UserIdentity::new_own(self.clone(), i)
}
matrix_sdk_base::crypto::UserIdentities::Other(i) => {
UserIdentity::new(self.clone(), i, self.get_dm_room(user_id))
}
}))
} else {
Ok(None)
}
}
/// Create and upload a new cross signing identity.
///
/// # Arguments
///
/// * `auth_data` - This request requires user interactive auth, the first
/// request needs to set this to `None` and will always fail with an
/// `UiaaResponse`. The response will contain information for the
/// interactive auth and the same request needs to be made but this time
/// with some `auth_data` provided.
///
/// # Examples
/// ```no_run
/// # use std::{convert::TryFrom, collections::BTreeMap};
/// # use matrix_sdk::{
/// # ruma::{api::client::uiaa, assign},
/// # Client,
/// # };
/// # use url::Url;
/// # use futures::executor::block_on;
/// # use serde_json::json;
/// # block_on(async {
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// if let Err(e) = client.bootstrap_cross_signing(None).await {
/// if let Some(response) = e.uiaa_response() {
/// let auth_data = uiaa::AuthData::Password(assign!(
/// uiaa::Password::new(
/// uiaa::UserIdentifier::UserIdOrLocalpart("example"),
/// "wordpass",
/// ), {
/// session: response.session.as_deref(),
/// }
/// ));
///
/// client
/// .bootstrap_cross_signing(Some(auth_data))
/// .await
/// .expect("Couldn't bootstrap cross signing")
/// } else {
/// panic!("Error durign cross signing bootstrap {:#?}", e);
/// }
/// }
/// # anyhow::Result::<()>::Ok(()) });
#[cfg(feature = "encryption")]
pub async fn bootstrap_cross_signing(&self, auth_data: Option<AuthData<'_>>) -> Result<()> {
let olm = self.olm_machine().await.ok_or(Error::AuthenticationRequired)?;
let (request, signature_request) = olm.bootstrap_cross_signing(false).await?;
let to_raw = |k| Raw::new(&k).expect("Can't serialize newly created cross signing keys");
let request = assign!(UploadSigningKeysRequest::new(), {
auth: auth_data,
master_key: request.master_key.map(to_raw),
self_signing_key: request.self_signing_key.map(to_raw),
user_signing_key: request.user_signing_key.map(to_raw),
});
self.send(request, None).await?;
self.send(signature_request, None).await?;
Ok(())
}
/// Export E2EE keys that match the given predicate encrypting them with the
/// given passphrase.
///
/// # Arguments
///
/// * `path` - The file path where the exported key file will be saved.
///
/// * `passphrase` - The passphrase that will be used to encrypt the
/// exported
/// room keys.
///
/// * `predicate` - A closure that will be called for every known
/// `InboundGroupSession`, which represents a room key. If the closure
/// returns `true` the `InboundGroupSessoin` will be included in the export,
/// if the closure returns `false` it will not be included.
///
/// # Panics
///
/// This method will panic if it isn't run on a Tokio runtime.
///
/// This method will panic if it can't get enough randomness from the OS to
/// encrypt the exported keys securely.
///
/// # Examples
///
/// ```no_run
/// # use std::{path::PathBuf, time::Duration};
/// # use matrix_sdk::{
/// # Client, config::SyncSettings,
/// # ruma::room_id,
/// # };
/// # use futures::executor::block_on;
/// # use url::Url;
/// # block_on(async {
/// # let homeserver = Url::parse("http://localhost:8080")?;
/// # let mut client = Client::new(homeserver).await?;
/// let path = PathBuf::from("/home/example/e2e-keys.txt");
/// // Export all room keys.
/// client
/// .export_keys(path, "secret-passphrase", |_| true)
/// .await?;
///
/// // Export only the room keys for a certain room.
/// let path = PathBuf::from("/home/example/e2e-room-keys.txt");
/// let room_id = room_id!("!test:localhost");
///
/// client
/// .export_keys(path, "secret-passphrase", |s| s.room_id() == room_id)
/// .await?;
/// # anyhow::Result::<()>::Ok(()) });
/// ```
#[cfg(all(feature = "encryption", not(target_arch = "wasm32")))]
pub async fn export_keys(
&self,
path: PathBuf,
passphrase: &str,
predicate: impl FnMut(&matrix_sdk_base::crypto::olm::InboundGroupSession) -> bool,
) -> Result<()> {
let olm = self.olm_machine().await.ok_or(Error::AuthenticationRequired)?;
let keys = olm.export_keys(predicate).await?;
let passphrase = zeroize::Zeroizing::new(passphrase.to_owned());
let encrypt = move || -> Result<()> {
let export: String =
matrix_sdk_base::crypto::encrypt_key_export(&keys, &passphrase, 500_000)?;
let mut file = std::fs::File::create(path)?;
file.write_all(&export.into_bytes())?;
Ok(())
};
let task = tokio::task::spawn_blocking(encrypt);
task.await.expect("Task join error")
}
/// Import E2EE keys from the given file path.
///
/// # Arguments
///
/// * `path` - The file path where the exported key file will can be found.
///
/// * `passphrase` - The passphrase that should be used to decrypt the
/// exported room keys.
///
/// Returns a tuple of numbers that represent the number of sessions that
/// were imported and the total number of sessions that were found in the
/// key export.
///
/// # Panics
///
/// This method will panic if it isn't run on a Tokio runtime.
///
/// ```no_run
/// # use std::{path::PathBuf, time::Duration};
/// # use matrix_sdk::{
/// # Client, config::SyncSettings,
/// # ruma::room_id,
/// # };
/// # use futures::executor::block_on;
/// # use url::Url;
/// # block_on(async {
/// # let homeserver = Url::parse("http://localhost:8080")?;
/// # let mut client = Client::new(homeserver).await?;
/// let path = PathBuf::from("/home/example/e2e-keys.txt");
/// let result = client.import_keys(path, "secret-passphrase").await?;
///
/// println!(
/// "Imported {} room keys out of {}",
/// result.imported_count, result.total_count
/// );
/// # anyhow::Result::<()>::Ok(()) });
/// ```
#[cfg(all(feature = "encryption", not(target_arch = "wasm32")))]
pub async fn import_keys(
&self,
path: PathBuf,
passphrase: &str,
) -> Result<RoomKeyImportResult, RoomKeyImportError> {
let olm = self.olm_machine().await.ok_or(RoomKeyImportError::StoreClosed)?;
let passphrase = zeroize::Zeroizing::new(passphrase.to_owned());
let decrypt = move || {
let file = std::fs::File::open(path)?;
matrix_sdk_base::crypto::decrypt_key_export(file, &passphrase)
};
let task = tokio::task::spawn_blocking(decrypt);
let import = task.await.expect("Task join error")?;
Ok(olm.import_keys(import, false, |_, _| {}).await?)
}
/// Tries to decrypt a `AnyRoomEvent`. Returns undecrypted room event when
/// decryption fails.
#[cfg(feature = "encryption")]
@@ -666,7 +277,7 @@ impl Client {
// have with this user.
let mut content = self
.store()
.get_account_data_event(EventType::Direct)
.get_account_data_event(GlobalAccountDataEventType::Direct)
.await?
.map(|e| e.deserialize())
.transpose()?
@@ -877,3 +488,406 @@ impl Client {
Ok(())
}
}
/// A high-level API to manage the client's encryption.
///
/// To get this, use [`Client::encryption()`].
#[cfg(feature = "encryption")]
#[derive(Debug, Clone)]
pub struct Encryption {
/// The underlying client.
client: Client,
}
#[cfg(feature = "encryption")]
impl Encryption {
pub(crate) fn new(client: Client) -> Self {
Self { client }
}
/// Get the public ed25519 key of our own device. This is usually what is
/// called the fingerprint of the device.
pub async fn ed25519_key(&self) -> Option<String> {
self.client.olm_machine().await.map(|o| o.identity_keys().ed25519().to_owned())
}
/// Get the status of the private cross signing keys.
///
/// This can be used to check which private cross signing keys we have
/// stored locally.
pub async fn cross_signing_status(&self) -> Option<CrossSigningStatus> {
if let Some(machine) = self.client.olm_machine().await {
Some(machine.cross_signing_status().await)
} else {
None
}
}
/// Get all the tracked users we know about
///
/// Tracked users are users for which we keep the device list of E2EE
/// capable devices up to date.
pub async fn tracked_users(&self) -> HashSet<Box<UserId>> {
self.client.olm_machine().await.map(|o| o.tracked_users()).unwrap_or_default()
}
/// Get a verification object with the given flow id.
pub async fn get_verification(&self, user_id: &UserId, flow_id: &str) -> Option<Verification> {
let olm = self.client.olm_machine().await?;
olm.get_verification(user_id, flow_id).map(|v| match v {
matrix_sdk_base::crypto::Verification::SasV1(s) => {
SasVerification { inner: s, client: self.client.clone() }.into()
}
#[cfg(feature = "qrcode")]
matrix_sdk_base::crypto::Verification::QrV1(qr) => {
verification::QrVerification { inner: qr, client: self.client.clone() }.into()
}
})
}
/// Get a `VerificationRequest` object for the given user with the given
/// flow id.
pub async fn get_verification_request(
&self,
user_id: &UserId,
flow_id: impl AsRef<str>,
) -> Option<VerificationRequest> {
let olm = self.client.olm_machine().await?;
olm.get_verification_request(user_id, flow_id)
.map(|r| VerificationRequest { inner: r, client: self.client.clone() })
}
/// Get a specific device of a user.
///
/// # Arguments
///
/// * `user_id` - The unique id of the user that the device belongs to.
///
/// * `device_id` - The unique id of the device.
///
/// Returns a `Device` if one is found and the crypto store didn't throw an
/// error.
///
/// This will always return None if the client hasn't been logged in.
///
/// # Example
///
/// ```no_run
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, ruma::{device_id, user_id}};
/// # use url::Url;
/// # use futures::executor::block_on;
/// # block_on(async {
/// # let alice = user_id!("@alice:example.org");
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// if let Some(device) = client
/// .encryption()
/// .get_device(alice, device_id!("DEVICEID"))
/// .await? {
/// println!("{:?}", device.verified());
///
/// if !device.verified() {
/// let verification = device.request_verification().await?;
/// }
/// }
/// # anyhow::Result::<()>::Ok(()) });
/// ```
pub async fn get_device(
&self,
user_id: &UserId,
device_id: &DeviceId,
) -> Result<Option<Device>, CryptoStoreError> {
let device = self.client.base_client().get_device(user_id, device_id).await?;
Ok(device.map(|d| Device { inner: d, client: self.client.clone() }))
}
/// Get a map holding all the devices of an user.
///
/// This will always return an empty map if the client hasn't been logged
/// in.
///
/// # Arguments
///
/// * `user_id` - The unique id of the user that the devices belong to.
///
/// # Example
///
/// ```no_run
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, ruma::user_id};
/// # use url::Url;
/// # use futures::executor::block_on;
/// # block_on(async {
/// # let alice = user_id!("@alice:example.org");
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let devices = client.encryption().get_user_devices(alice).await?;
///
/// for device in devices.devices() {
/// println!("{:?}", device);
/// }
/// # anyhow::Result::<()>::Ok(()) });
/// ```
pub async fn get_user_devices(
&self,
user_id: &UserId,
) -> Result<UserDevices, CryptoStoreError> {
let devices = self.client.base_client().get_user_devices(user_id).await?;
Ok(UserDevices { inner: devices, client: self.client.clone() })
}
/// Get a E2EE identity of an user.
///
/// # Arguments
///
/// * `user_id` - The unique id of the user that the identity belongs to.
///
/// Returns a `UserIdentity` if one is found and the crypto store
/// didn't throw an error.
///
/// This will always return None if the client hasn't been logged in.
///
/// # Example
///
/// ```no_run
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, ruma::user_id};
/// # use url::Url;
/// # use futures::executor::block_on;
/// # block_on(async {
/// # let alice = user_id!("@alice:example.org");
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let user = client.encryption().get_user_identity(alice).await?;
///
/// if let Some(user) = user {
/// println!("{:?}", user.verified());
///
/// let verification = user.request_verification().await?;
/// }
/// # anyhow::Result::<()>::Ok(()) });
/// ```
pub async fn get_user_identity(
&self,
user_id: &UserId,
) -> Result<Option<crate::encryption::identities::UserIdentity>, CryptoStoreError> {
use crate::encryption::identities::UserIdentity;
if let Some(olm) = self.client.olm_machine().await {
let identity = olm.get_identity(user_id).await?;
Ok(identity.map(|i| match i {
matrix_sdk_base::crypto::UserIdentities::Own(i) => {
UserIdentity::new_own(self.client.clone(), i)
}
matrix_sdk_base::crypto::UserIdentities::Other(i) => {
UserIdentity::new(self.client.clone(), i, self.client.get_dm_room(user_id))
}
}))
} else {
Ok(None)
}
}
/// Create and upload a new cross signing identity.
///
/// # Arguments
///
/// * `auth_data` - This request requires user interactive auth, the first
/// request needs to set this to `None` and will always fail with an
/// `UiaaResponse`. The response will contain information for the
/// interactive auth and the same request needs to be made but this time
/// with some `auth_data` provided.
///
/// # Examples
/// ```no_run
/// # use std::{convert::TryFrom, collections::BTreeMap};
/// # use matrix_sdk::{
/// # ruma::{api::client::uiaa, assign},
/// # Client,
/// # };
/// # use url::Url;
/// # use futures::executor::block_on;
/// # use serde_json::json;
/// # block_on(async {
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// if let Err(e) = client.encryption().bootstrap_cross_signing(None).await {
/// if let Some(response) = e.uiaa_response() {
/// let auth_data = uiaa::AuthData::Password(assign!(
/// uiaa::Password::new(
/// uiaa::UserIdentifier::UserIdOrLocalpart("example"),
/// "wordpass",
/// ), {
/// session: response.session.as_deref(),
/// }
/// ));
///
/// client
/// .encryption()
/// .bootstrap_cross_signing(Some(auth_data))
/// .await
/// .expect("Couldn't bootstrap cross signing")
/// } else {
/// panic!("Error durign cross signing bootstrap {:#?}", e);
/// }
/// }
/// # anyhow::Result::<()>::Ok(()) });
pub async fn bootstrap_cross_signing(&self, auth_data: Option<AuthData<'_>>) -> Result<()> {
let olm = self.client.olm_machine().await.ok_or(Error::AuthenticationRequired)?;
let (request, signature_request) = olm.bootstrap_cross_signing(false).await?;
let to_raw = |k| Raw::new(&k).expect("Can't serialize newly created cross signing keys");
let request = assign!(UploadSigningKeysRequest::new(), {
auth: auth_data,
master_key: request.master_key.map(to_raw),
self_signing_key: request.self_signing_key.map(to_raw),
user_signing_key: request.user_signing_key.map(to_raw),
});
self.client.send(request, None).await?;
self.client.send(signature_request, None).await?;
Ok(())
}
/// Export E2EE keys that match the given predicate encrypting them with the
/// given passphrase.
///
/// # Arguments
///
/// * `path` - The file path where the exported key file will be saved.
///
/// * `passphrase` - The passphrase that will be used to encrypt the
/// exported
/// room keys.
///
/// * `predicate` - A closure that will be called for every known
/// `InboundGroupSession`, which represents a room key. If the closure
/// returns `true` the `InboundGroupSessoin` will be included in the export,
/// if the closure returns `false` it will not be included.
///
/// # Panics
///
/// This method will panic if it isn't run on a Tokio runtime.
///
/// This method will panic if it can't get enough randomness from the OS to
/// encrypt the exported keys securely.
///
/// # Examples
///
/// ```no_run
/// # use std::{path::PathBuf, time::Duration};
/// # use matrix_sdk::{
/// # Client, config::SyncSettings,
/// # ruma::room_id,
/// # };
/// # use futures::executor::block_on;
/// # use url::Url;
/// # block_on(async {
/// # let homeserver = Url::parse("http://localhost:8080")?;
/// # let mut client = Client::new(homeserver).await?;
/// let path = PathBuf::from("/home/example/e2e-keys.txt");
/// // Export all room keys.
/// client
/// .encryption()
/// .export_keys(path, "secret-passphrase", |_| true)
/// .await?;
///
/// // Export only the room keys for a certain room.
/// let path = PathBuf::from("/home/example/e2e-room-keys.txt");
/// let room_id = room_id!("!test:localhost");
///
/// client
/// .encryption()
/// .export_keys(path, "secret-passphrase", |s| s.room_id() == room_id)
/// .await?;
/// # anyhow::Result::<()>::Ok(()) });
/// ```
#[cfg(not(target_arch = "wasm32"))]
pub async fn export_keys(
&self,
path: PathBuf,
passphrase: &str,
predicate: impl FnMut(&matrix_sdk_base::crypto::olm::InboundGroupSession) -> bool,
) -> Result<()> {
let olm = self.client.olm_machine().await.ok_or(Error::AuthenticationRequired)?;
let keys = olm.export_keys(predicate).await?;
let passphrase = zeroize::Zeroizing::new(passphrase.to_owned());
let encrypt = move || -> Result<()> {
let export: String =
matrix_sdk_base::crypto::encrypt_key_export(&keys, &passphrase, 500_000)?;
let mut file = std::fs::File::create(path)?;
file.write_all(&export.into_bytes())?;
Ok(())
};
let task = tokio::task::spawn_blocking(encrypt);
task.await.expect("Task join error")
}
/// Import E2EE keys from the given file path.
///
/// # Arguments
///
/// * `path` - The file path where the exported key file will can be found.
///
/// * `passphrase` - The passphrase that should be used to decrypt the
/// exported room keys.
///
/// Returns a tuple of numbers that represent the number of sessions that
/// were imported and the total number of sessions that were found in the
/// key export.
///
/// # Panics
///
/// This method will panic if it isn't run on a Tokio runtime.
///
/// ```no_run
/// # use std::{path::PathBuf, time::Duration};
/// # use matrix_sdk::{
/// # Client, config::SyncSettings,
/// # ruma::room_id,
/// # };
/// # use futures::executor::block_on;
/// # use url::Url;
/// # block_on(async {
/// # let homeserver = Url::parse("http://localhost:8080")?;
/// # let mut client = Client::new(homeserver).await?;
/// let path = PathBuf::from("/home/example/e2e-keys.txt");
/// let result = client.encryption().import_keys(path, "secret-passphrase").await?;
///
/// println!(
/// "Imported {} room keys out of {}",
/// result.imported_count, result.total_count
/// );
/// # anyhow::Result::<()>::Ok(()) });
/// ```
#[cfg(not(target_arch = "wasm32"))]
pub async fn import_keys(
&self,
path: PathBuf,
passphrase: &str,
) -> Result<RoomKeyImportResult, RoomKeyImportError> {
let olm = self.client.olm_machine().await.ok_or(RoomKeyImportError::StoreClosed)?;
let passphrase = zeroize::Zeroizing::new(passphrase.to_owned());
let decrypt = move || {
let file = std::fs::File::open(path)?;
matrix_sdk_base::crypto::decrypt_key_export(file, &passphrase)
};
let task = tokio::task::spawn_blocking(decrypt);
let import = task.await.expect("Task join error")?;
Ok(olm.import_keys(import, false, |_, _| {}).await?)
}
}

View File

@@ -54,6 +54,7 @@ impl SasVerification {
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let sas = client
/// .encryption()
/// .get_verification(&user_id, flow_id)
/// .await
/// .and_then(|v| v.sas());
@@ -128,6 +129,7 @@ impl SasVerification {
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let sas_verification = client
/// .encryption()
/// .get_verification(&user_id, flow_id)
/// .await
/// .and_then(|v| v.sas());

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{convert::TryFrom, fmt::Debug, sync::Arc};
use std::{convert::TryFrom, fmt::Debug, sync::Arc, time::Duration};
use bytes::{Bytes, BytesMut};
use http::Response as HttpResponse;
@@ -25,11 +25,9 @@ use ruma::api::{
use tracing::trace;
use url::Url;
use crate::{
config::{ClientConfig, RequestConfig},
error::HttpError,
Session,
};
use crate::{config::RequestConfig, error::HttpError, Session};
pub(crate) const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
/// Abstraction around the http layer. The allows implementors to use different
/// http libraries.
@@ -206,33 +204,58 @@ impl HttpClient {
}
}
/// Build a client with the specified configuration.
pub(crate) fn client_with_config(config: &ClientConfig) -> Result<Client, HttpError> {
let http_client = reqwest::Client::builder();
#[derive(Debug)]
pub(crate) struct HttpSettings {
#[cfg(not(target_arch = "wasm32"))]
let http_client = {
let http_client = if config.disable_ssl_verification {
http_client.danger_accept_invalid_certs(true)
} else {
http_client
pub(crate) disable_ssl_verification: bool,
#[cfg(not(target_arch = "wasm32"))]
pub(crate) proxy: Option<String>,
#[cfg(not(target_arch = "wasm32"))]
pub(crate) user_agent: Option<String>,
#[cfg(not(target_arch = "wasm32"))]
pub(crate) timeout: Duration,
}
#[allow(clippy::derivable_impls)]
impl Default for HttpSettings {
fn default() -> Self {
Self {
#[cfg(not(target_arch = "wasm32"))]
disable_ssl_verification: false,
#[cfg(not(target_arch = "wasm32"))]
proxy: None,
#[cfg(not(target_arch = "wasm32"))]
user_agent: None,
#[cfg(not(target_arch = "wasm32"))]
timeout: DEFAULT_REQUEST_TIMEOUT,
}
}
}
impl HttpSettings {
/// Build a client with the specified configuration.
pub(crate) fn make_client(&self) -> Result<Client, HttpError> {
#[allow(unused_mut)]
let mut http_client = reqwest::Client::builder();
#[cfg(not(target_arch = "wasm32"))]
{
if self.disable_ssl_verification {
http_client = http_client.danger_accept_invalid_certs(true)
}
if let Some(p) = &self.proxy {
http_client = http_client.proxy(reqwest::Proxy::all(p.as_str())?);
}
let user_agent =
self.user_agent.clone().unwrap_or_else(|| "matrix-rust-sdk".to_owned());
http_client = http_client.user_agent(user_agent).timeout(self.timeout);
};
let http_client = match &config.proxy {
Some(p) => http_client.proxy(p.clone()),
None => http_client,
};
let user_agent = config.user_agent.clone().unwrap_or_else(|| "matrix-rust-sdk".to_owned());
http_client.user_agent(user_agent).timeout(config.request_config.timeout)
};
#[cfg(target_arch = "wasm32")]
#[allow(unused)]
let _ = config;
Ok(http_client.build()?)
Ok(http_client.build()?)
}
}
async fn response_to_http_response(

View File

@@ -50,13 +50,14 @@ mod http_client;
/// High-level room API
pub mod room;
mod room_member;
pub mod store;
mod sync;
#[cfg(feature = "encryption")]
pub mod encryption;
pub use account::Account;
pub use client::{Client, LoopCtrl};
pub use client::{Client, ClientBuildError, ClientBuilder, LoopCtrl};
#[cfg(feature = "image_proc")]
pub use error::ImageError;
pub use error::{Error, HttpError, HttpResult, Result};

View File

@@ -18,7 +18,7 @@ use ruma::{
events::{
room::history_visibility::HistoryVisibility,
tag::{TagInfo, TagName},
AnyStateEvent, AnySyncStateEvent, EventType,
AnyStateEvent, AnySyncStateEvent, StateEventType,
},
serde::Raw,
uint, EventId, RoomId, UInt, UserId,
@@ -640,7 +640,7 @@ impl Common {
/// Get all state events of a given type in this room.
pub async fn get_state_events(
&self,
event_type: EventType,
event_type: StateEventType,
) -> Result<Vec<Raw<AnySyncStateEvent>>> {
self.client.store().get_state_events(self.room_id(), event_type).await.map_err(Into::into)
}
@@ -648,7 +648,7 @@ impl Common {
/// Get a specific state event in this room.
pub async fn get_state_event(
&self,
event_type: EventType,
event_type: StateEventType,
state_key: &str,
) -> Result<Option<Raw<AnySyncStateEvent>>> {
self.client
@@ -667,7 +667,7 @@ impl Common {
let user_ids = self.client.store().get_user_ids(self.room_id()).await?;
for user_id in user_ids {
let devices = self.client.get_user_devices(&user_id).await?;
let devices = self.client.encryption().get_user_devices(&user_id).await?;
let any_unverified = devices.devices().any(|d| !d.verified());
if any_unverified {

View File

@@ -0,0 +1,35 @@
// Copyright 2022 Kévin Commaille
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Functions and types to initialize a store.
//!
//! The re-exports present here depend on the store-related features that are
//! enabled:
//!
//! 1. `sled_state_store` provides a `StateStore`, while
//! `sled_cryptostore` provides also a `CryptoStore` for encryption data. This
//! is the default persistent store implementation for non-WebAssembly.
//! 2. `indexeddb_store` provides both a `StateStore` and a `CryptoStore` if
//! `encryption` is also enabled. This is the default persistent store
//! implementation for WebAssembly.
//!
//! Both options provide a `make_store_config` convenience method to create a
//! [`StoreConfig`] for [`ClientBuilder::store_config()`].
//!
//! [`StoreConfig`]: crate::config::StoreConfig
//! [`ClientBuilder::store_config()`]: crate::ClientBuilder::store_config
#[cfg(any(feature = "indexeddb_state_store", feature = "indexeddb_cryptostore"))]
pub use matrix_sdk_indexeddb::*;
#[cfg(any(feature = "sled_state_store", feature = "sled_cryptostore"))]
pub use matrix_sdk_sled::*;

View File

@@ -36,6 +36,8 @@ enum CiCommand {
#[clap(subcommand)]
cmd: Option<WasmFeatureSet>,
},
/// Run tests for the different crypto crate features
TestCrypto,
}
#[derive(Subcommand, PartialEq, Eq, PartialOrd, Ord)]
@@ -76,6 +78,7 @@ impl CiArgs {
CiCommand::TestFeatures { cmd } => run_feature_tests(cmd),
CiCommand::TestAppservice => run_appservice_tests(),
CiCommand::Wasm { cmd } => run_wasm_checks(cmd),
CiCommand::TestCrypto => run_crypto_tests(),
},
None => {
check_style()?;
@@ -86,6 +89,7 @@ impl CiArgs {
run_feature_tests(None)?;
run_appservice_tests()?;
run_wasm_checks(None)?;
run_crypto_tests()?;
Ok(())
}
@@ -108,11 +112,17 @@ fn check_typos() -> Result<()> {
fn check_clippy() -> Result<()> {
cmd!("rustup run nightly cargo clippy --all-targets -- -D warnings").run()?;
cmd!(
"rustup run nightly cargo clippy --all-targets
"rustup run nightly cargo clippy --workspace --all-targets
--exclude matrix-sdk-crypto --exclude xtask
--no-default-features --features native-tls,warp
-- -D warnings"
)
.run()?;
cmd!(
"rustup run nightly cargo clippy --all-targets -p matrix-sdk-crypto
--no-default-features -- -D warnings"
)
.run()?;
Ok(())
}
@@ -159,6 +169,16 @@ fn run_feature_tests(cmd: Option<FeatureSet>) -> Result<()> {
Ok(())
}
fn run_crypto_tests() -> Result<()> {
cmd!(
"rustup run stable cargo clippy -p matrix-sdk-crypto --features=backups_v1 -- -D warnings"
)
.run()?;
cmd!("rustup run stable cargo test -p matrix-sdk-crypto --features=backups_v1").run()?;
Ok(())
}
fn run_appservice_tests() -> Result<()> {
cmd!("rustup run stable cargo clippy -p matrix-sdk-appservice -- -D warnings").run()?;
cmd!("rustup run stable cargo test -p matrix-sdk-appservice").run()?;
@@ -173,14 +193,14 @@ fn run_wasm_checks(cmd: Option<WasmFeatureSet>) -> Result<()> {
WasmFeatureSet::MatrixSdkNoDefault,
"-p matrix-sdk \
--no-default-features \
--features qrcode,encryption,indexeddb_stores,rustls-tls",
--features qrcode,encryption,indexeddb_state_store,indexeddb_cryptostore,rustls-tls",
),
(WasmFeatureSet::MatrixSdkBase, "-p matrix-sdk-base"),
(WasmFeatureSet::MatrixSdkCommon, "-p matrix-sdk-common"),
(WasmFeatureSet::MatrixSdkCrypto, "-p matrix-sdk-crypto"),
(
WasmFeatureSet::MatrixSdkIndexeddbStores,
"-p matrix-sdk --no-default-features --features indexeddb_stores,encryption,rustls-tls",
"-p matrix-sdk --no-default-features --features indexeddb_state_store,indexeddb_cryptostore,encryption,rustls-tls",
),
]);