Merge branch 'master' into sas-verification

This commit is contained in:
Damir Jelić 2020-07-22 11:18:26 +02:00
commit e612326714
40 changed files with 3092 additions and 2182 deletions

View file

@ -7,26 +7,46 @@ addons:
jobs:
allow_failures:
- os: windows
- os: linux
name: wasm32-unknown-unknown
- os: osx
name: macOS 10.15
include:
- stage: Lint
- stage: Format
os: linux
before_script:
- rustup component add rustfmt
script:
- cargo fmt --all -- --check
- stage: Clippy
os: linux
before_script:
- rustup component add clippy
script:
- cargo clippy --all-targets --all-features -- -D warnings
- stage: Test
os: linux
dist: bionic
- os: windows
script:
- cd matrix_sdk
- cargo test --no-default-features --features "messages"
- cd ../matrix_sdk_base
- cargo test --no-default-features --features "messages"
- os: osx
- os: linux
name: Minimal build
script:
- cd matrix_sdk
- cargo build --no-default-features
- os: osx
name: macOS 10.15
osx_image: xcode12
- os: linux
name: Coverage
before_script:

View file

@ -21,7 +21,7 @@ http = "0.2.1"
reqwest = "0.10.6"
serde_json = "1.0.56"
thiserror = "1.0.20"
tracing = "0.1.15"
tracing = "0.1.16"
url = "2.1.1"
matrix-sdk-common-macros = { version = "0.1.0", path = "../matrix_sdk_common_macros" }
@ -37,8 +37,8 @@ version = "0.2.4"
default-features = false
features = ["std", "std-future"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.futures-timer]
version = "3.0.2"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
futures-timer = "3.0.2"
[target.'cfg(target_arch = "wasm32")'.dependencies.futures-timer]
version = "3.0.2"

View file

@ -2,7 +2,7 @@ use std::{env, process::exit};
use matrix_sdk::{
self,
events::{room::member::MemberEventContent, StrippedStateEventStub},
events::{room::member::MemberEventContent, StrippedStateEvent},
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
};
use matrix_sdk_common_macros::async_trait;
@ -23,7 +23,7 @@ impl EventEmitter for AutoJoinBot {
async fn on_stripped_state_member(
&self,
room: SyncRoom,
room_member: &StrippedStateEventStub<MemberEventContent>,
room_member: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
if room_member.state_key != self.client.user_id().await.unwrap() {

View file

@ -4,7 +4,7 @@ use matrix_sdk::{
self,
events::{
room::message::{MessageEventContent, TextMessageEventContent},
MessageEventStub,
SyncMessageEvent,
},
Client, ClientConfig, EventEmitter, JsonStore, SyncRoom, SyncSettings,
};
@ -25,9 +25,9 @@ impl CommandBot {
#[async_trait]
impl EventEmitter for CommandBot {
async fn on_room_message(&self, room: SyncRoom, event: &MessageEventStub<MessageEventContent>) {
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
if let SyncRoom::Joined(room) = room {
let msg_body = if let MessageEventStub {
let msg_body = if let SyncMessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
..
} = event

View file

@ -5,7 +5,7 @@ use matrix_sdk::{
self,
events::{
room::message::{MessageEventContent, TextMessageEventContent},
MessageEventStub,
SyncMessageEvent,
},
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
};
@ -15,9 +15,9 @@ struct EventCallback;
#[async_trait]
impl EventEmitter for EventCallback {
async fn on_room_message(&self, room: SyncRoom, event: &MessageEventStub<MessageEventContent>) {
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
if let SyncRoom::Joined(room) = room {
if let MessageEventStub {
if let SyncMessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
sender,
..

View file

@ -2,7 +2,7 @@ use matrix_sdk::{
api::r0::sync::sync_events::Response as SyncResponse,
events::{
room::message::{MessageEventContent, TextMessageEventContent},
AnyMessageEventStub, AnyRoomEventStub, MessageEventStub,
AnySyncMessageEvent, AnySyncRoomEvent, SyncMessageEvent,
},
identifiers::RoomId,
Client, ClientConfig, SyncSettings,
@ -17,9 +17,9 @@ impl WasmBot {
async fn on_room_message(
&self,
room_id: &RoomId,
event: MessageEventStub<MessageEventContent>,
event: SyncMessageEvent<MessageEventContent>,
) {
let msg_body = if let MessageEventStub {
let msg_body = if let SyncMessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
..
} = event
@ -45,7 +45,7 @@ impl WasmBot {
for (room_id, room) in response.rooms.join {
for event in room.timeline.events {
if let Ok(event) = event.deserialize() {
if let AnyRoomEventStub::Message(AnyMessageEventStub::RoomMessage(ev)) = event {
if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(ev)) = event {
self.on_room_message(&room_id, ev).await
}
}

View file

@ -31,7 +31,7 @@ use futures_timer::Delay as sleep;
use std::future::Future;
#[cfg(feature = "encryption")]
use tracing::{debug, warn};
use tracing::{info, instrument, trace};
use tracing::{error, info, instrument, trace};
use http::Method as HttpMethod;
use http::Response as HttpResponse;
@ -105,6 +105,7 @@ pub struct ClientConfig {
user_agent: Option<HeaderValue>,
disable_ssl_verification: bool,
base_config: BaseClientConfig,
timeout: Option<Duration>,
}
// #[cfg_attr(tarpaulin, skip)]
@ -198,11 +199,18 @@ impl ClientConfig {
self.base_config = self.base_config.passphrase(passphrase);
self
}
/// Set a timeout duration for all HTTP requests. The default is no timeout.
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
}
#[derive(Debug, Default, Clone)]
/// Settings for a sync call.
pub struct SyncSettings {
pub(crate) filter: Option<sync_events::Filter>,
pub(crate) timeout: Option<Duration>,
pub(crate) token: Option<String>,
pub(crate) full_state: bool,
@ -235,6 +243,17 @@ impl SyncSettings {
self
}
/// Set the sync filter.
/// It can be either the filter ID, or the definition for the filter.
///
/// # Arguments
///
/// * `filter` - The filter configuration that should be used for the sync call.
pub fn filter(mut self, filter: sync_events::Filter) -> Self {
self.filter = Some(filter);
self
}
/// Should the server return the full state from the start of the timeline.
///
/// This does nothing if no sync token is set.
@ -302,6 +321,11 @@ impl Client {
#[cfg(not(target_arch = "wasm32"))]
let http_client = {
let http_client = match config.timeout {
Some(x) => http_client.timeout(x),
None => http_client,
};
let http_client = if config.disable_ssl_verification {
http_client.danger_accept_invalid_certs(true)
} else {
@ -448,7 +472,7 @@ impl Client {
login_info: login::LoginInfo::Password {
password: password.into(),
},
device_id: device_id.map(|d| d.into()),
device_id: device_id.map(|d| d.into().into_boxed_str()),
initial_device_display_name: initial_device_display_name.map(|d| d.into()),
};
@ -1222,7 +1246,7 @@ impl Client {
#[instrument]
pub async fn sync(&self, sync_settings: SyncSettings) -> Result<sync_events::Response> {
let request = sync_events::Request {
filter: None,
filter: sync_settings.filter,
since: sync_settings.token,
full_state: sync_settings.full_state,
set_presence: sync_events::SetPresence::Online,
@ -1302,6 +1326,7 @@ impl Client {
C: Future<Output = ()>,
{
let mut sync_settings = sync_settings;
let filter = sync_settings.filter.clone();
let mut last_sync_time: Option<Instant> = None;
if sync_settings.token.is_none() {
@ -1311,12 +1336,13 @@ impl Client {
loop {
let response = self.sync(sync_settings.clone()).await;
let response = if let Ok(r) = response {
r
} else {
sleep::new(Duration::from_secs(1)).await;
continue;
let response = match response {
Ok(r) => r,
Err(e) => {
error!("Received an invalid response: {}", e);
sleep::new(Duration::from_secs(1)).await;
continue;
}
};
// TODO send out to-device messages here
@ -1360,6 +1386,9 @@ impl Client {
.await
.expect("No sync token found after initial sync"),
);
if let Some(f) = filter.as_ref() {
sync_settings = sync_settings.filter(f.clone());
}
}
}
@ -1378,7 +1407,7 @@ impl Client {
#[instrument]
async fn claim_one_time_keys(
&self,
one_time_keys: BTreeMap<UserId, BTreeMap<DeviceId, KeyAlgorithm>>,
one_time_keys: BTreeMap<UserId, BTreeMap<Box<DeviceId>, KeyAlgorithm>>,
) -> Result<claim_keys::Response> {
let request = claim_keys::Request {
timeout: None,
@ -1482,7 +1511,7 @@ impl Client {
users_for_query
);
let mut device_keys: BTreeMap<UserId, Vec<DeviceId>> = BTreeMap::new();
let mut device_keys: BTreeMap<UserId, Vec<Box<DeviceId>>> = BTreeMap::new();
for user in users_for_query.drain() {
device_keys.insert(user, Vec::new());

View file

@ -40,6 +40,8 @@ pub use matrix_sdk_base::Error as BaseError;
#[cfg(not(target_arch = "wasm32"))]
pub use matrix_sdk_base::JsonStore;
pub use matrix_sdk_base::{CustomOrRawEvent, EventEmitter, Room, Session, SyncRoom};
#[cfg(feature = "messages")]
pub use matrix_sdk_base::{MessageQueue, MessageWrapper, PossiblyRedactedExt};
pub use matrix_sdk_base::{RoomState, StateStore};
pub use matrix_sdk_common::*;
pub use reqwest::header::InvalidHeaderValue;

View file

@ -152,6 +152,12 @@ impl RoomBuilder {
}
}
impl Default for RoomBuilder {
fn default() -> Self {
Self::new()
}
}
impl Into<create_room::Request> for RoomBuilder {
fn into(mut self) -> create_room::Request {
self.req.creation_content = Some(self.creation_content);
@ -269,7 +275,7 @@ impl Into<get_message_events::Request> for MessagesRequestBuilder {
pub struct RegistrationBuilder {
password: Option<String>,
username: Option<String>,
device_id: Option<DeviceId>,
device_id: Option<Box<DeviceId>>,
initial_device_display_name: Option<String>,
auth: Option<AuthData>,
kind: Option<RegistrationKind>,
@ -303,7 +309,7 @@ impl RegistrationBuilder {
///
/// If this does not correspond to a known client device, a new device will be created.
/// The server will auto-generate a device_id if this is not specified.
pub fn device_id<S: Into<String>>(&mut self, device_id: S) -> &mut Self {
pub fn device_id<S: Into<Box<str>>>(&mut self, device_id: S) -> &mut Self {
self.device_id = Some(device_id.into());
self
}

View file

@ -21,6 +21,7 @@ async-trait = "0.1.36"
serde = "1.0.114"
serde_json = "1.0.56"
zeroize = "1.1.0"
tracing = "0.1.16"
matrix-sdk-common-macros = { version = "0.1.0", path = "../matrix_sdk_common_macros" }
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
@ -45,4 +46,4 @@ mockito = "0.26.0"
tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3.14"
wasm-bindgen-test = "0.3.15"

View file

@ -38,8 +38,8 @@ use crate::session::Session;
use crate::state::{AllRooms, ClientState, StateStore};
use crate::EventEmitter;
use matrix_sdk_common::events::{
AnyBasicEvent, AnyEphemeralRoomEventStub, AnyMessageEventStub, AnyRoomEventStub,
AnyStateEventStub, AnyStrippedStateEventStub, EventJson,
AnyBasicEvent, AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, AnySyncMessageEvent,
AnySyncRoomEvent, AnySyncStateEvent, EventJson,
};
#[cfg(feature = "encryption")]
@ -94,8 +94,8 @@ pub struct AdditionalUnsignedData {
/// [synapse-bug]: <https://github.com/matrix-org/matrix-doc/issues/684#issuecomment-641182668>
/// [discussion]: <https://github.com/matrix-org/matrix-doc/issues/684#issuecomment-641182668>
fn hoist_room_event_prev_content(
event: &EventJson<AnyRoomEventStub>,
) -> Option<EventJson<AnyRoomEventStub>> {
event: &EventJson<AnySyncRoomEvent>,
) -> Option<EventJson<AnySyncRoomEvent>> {
let prev_content = serde_json::from_str::<AdditionalEventData>(event.json().get())
.map(|more_unsigned| more_unsigned.unsigned)
.map(|additional| additional.prev_content)
@ -105,7 +105,7 @@ fn hoist_room_event_prev_content(
let mut ev = event.deserialize().ok()?;
match &mut ev {
AnyRoomEventStub::State(AnyStateEventStub::RoomMember(ref mut member))
AnySyncRoomEvent::State(AnySyncStateEvent::RoomMember(ref mut member))
if member.prev_content.is_none() =>
{
if let Ok(prev) = prev_content.deserialize() {
@ -122,8 +122,8 @@ fn hoist_room_event_prev_content(
///
/// See comment of `hoist_room_event_prev_content`.
fn hoist_state_event_prev_content(
event: &EventJson<AnyStateEventStub>,
) -> Option<EventJson<AnyStateEventStub>> {
event: &EventJson<AnySyncStateEvent>,
) -> Option<EventJson<AnySyncStateEvent>> {
let prev_content = serde_json::from_str::<AdditionalEventData>(event.json().get())
.map(|more_unsigned| more_unsigned.unsigned)
.map(|additional| additional.prev_content)
@ -132,7 +132,7 @@ fn hoist_state_event_prev_content(
let mut ev = event.deserialize().ok()?;
match &mut ev {
AnyStateEventStub::RoomMember(ref mut member) if member.prev_content.is_none() => {
AnySyncStateEvent::RoomMember(ref mut member) if member.prev_content.is_none() => {
member.prev_content = Some(prev_content.deserialize().ok()?);
Some(EventJson::from(ev))
}
@ -141,7 +141,7 @@ fn hoist_state_event_prev_content(
}
fn stripped_deserialize_prev_content(
event: &EventJson<AnyStrippedStateEventStub>,
event: &EventJson<AnyStrippedStateEvent>,
) -> Option<AdditionalUnsignedData> {
serde_json::from_str::<AdditionalEventData>(event.json().get())
.map(|more_unsigned| more_unsigned.unsigned)
@ -488,7 +488,7 @@ impl BaseClient {
*olm = Some(
OlmMachine::new_with_store(
session.user_id.to_owned(),
session.device_id.to_owned(),
session.device_id.as_str().into(),
store,
)
.await
@ -713,14 +713,14 @@ impl BaseClient {
pub async fn receive_joined_timeline_event(
&self,
room_id: &RoomId,
event: &mut EventJson<AnyRoomEventStub>,
event: &mut EventJson<AnySyncRoomEvent>,
) -> Result<bool> {
match event.deserialize() {
#[allow(unused_mut)]
Ok(mut e) => {
#[cfg(feature = "encryption")]
{
if let AnyRoomEventStub::Message(AnyMessageEventStub::RoomEncrypted(
if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomEncrypted(
ref mut encrypted_event,
)) = e
{
@ -742,8 +742,8 @@ impl BaseClient {
let room_lock = self.get_or_create_joined_room(&room_id).await?;
let mut room = room_lock.write().await;
if let AnyRoomEventStub::State(AnyStateEventStub::RoomMember(mem_event)) = &mut e {
let changed = room.handle_membership(mem_event);
if let AnySyncRoomEvent::State(AnySyncStateEvent::RoomMember(mem_event)) = &mut e {
let (changed, _) = room.handle_membership(mem_event, false);
// The memberlist of the room changed, invalidate the group session
// of the room.
@ -774,13 +774,13 @@ impl BaseClient {
pub async fn receive_joined_state_event(
&self,
room_id: &RoomId,
event: &AnyStateEventStub,
event: &AnySyncStateEvent,
) -> Result<bool> {
let room_lock = self.get_or_create_joined_room(room_id).await?;
let mut room = room_lock.write().await;
if let AnyStateEventStub::RoomMember(e) = event {
let changed = room.handle_membership(e);
if let AnySyncStateEvent::RoomMember(e) = event {
let (changed, _) = room.handle_membership(e, true);
// The memberlist of the room changed, invalidate the group session
// of the room.
@ -808,7 +808,7 @@ impl BaseClient {
pub async fn receive_invite_state_event(
&self,
room_id: &RoomId,
event: &AnyStrippedStateEventStub,
event: &AnyStrippedStateEvent,
) -> Result<bool> {
let room_lock = self.get_or_create_invited_room(room_id).await?;
let mut room = room_lock.write().await;
@ -828,7 +828,7 @@ impl BaseClient {
pub async fn receive_left_timeline_event(
&self,
room_id: &RoomId,
event: &EventJson<AnyRoomEventStub>,
event: &EventJson<AnySyncRoomEvent>,
) -> Result<bool> {
match event.deserialize() {
Ok(e) => {
@ -853,7 +853,7 @@ impl BaseClient {
pub async fn receive_left_state_event(
&self,
room_id: &RoomId,
event: &AnyStateEventStub,
event: &AnySyncStateEvent,
) -> Result<bool> {
let room_lock = self.get_or_create_left_room(room_id).await?;
let mut room = room_lock.write().await;
@ -906,11 +906,11 @@ impl BaseClient {
/// * `room_id` - The unique id of the room the event belongs to.
///
/// * `event` - The presence event for a specified room member.
pub async fn receive_ephemeral_event(&self, event: &AnyEphemeralRoomEventStub) -> bool {
pub async fn receive_ephemeral_event(&self, event: &AnySyncEphemeralRoomEvent) -> bool {
match event {
AnyEphemeralRoomEventStub::FullyRead(_) => {}
AnyEphemeralRoomEventStub::Receipt(_) => {}
AnyEphemeralRoomEventStub::Typing(_) => {}
AnySyncEphemeralRoomEvent::FullyRead(_) => {}
AnySyncEphemeralRoomEvent::Receipt(_) => {}
AnySyncEphemeralRoomEvent::Typing(_) => {}
_ => {}
};
false
@ -1197,7 +1197,7 @@ impl BaseClient {
if let Ok(mut e) = event.deserialize() {
// if the event is a m.room.member event the server will sometimes
// send the `prev_content` field as part of the unsigned field.
if let AnyStrippedStateEventStub::RoomMember(_) = &mut e {
if let AnyStrippedStateEvent::RoomMember(_) = &mut e {
if let Some(raw_content) = stripped_deserialize_prev_content(event) {
let prev_content = match raw_content.prev_content {
Some(json) => json.deserialize().ok(),
@ -1280,7 +1280,7 @@ impl BaseClient {
pub async fn get_missing_sessions(
&self,
users: impl Iterator<Item = &UserId>,
) -> Result<BTreeMap<UserId, BTreeMap<DeviceId, KeyAlgorithm>>> {
) -> Result<BTreeMap<UserId, BTreeMap<Box<DeviceId>, KeyAlgorithm>>> {
let mut olm = self.olm.lock().await;
match &mut *olm {
@ -1437,7 +1437,7 @@ impl BaseClient {
pub(crate) async fn emit_timeline_event(
&self,
room_id: &RoomId,
event: &AnyRoomEventStub,
event: &AnySyncRoomEvent,
room_state: RoomStateType,
) {
let lock = self.event_emitter.read().await;
@ -1472,52 +1472,54 @@ impl BaseClient {
};
match event {
AnyRoomEventStub::State(event) => match event {
AnyStateEventStub::RoomMember(e) => event_emitter.on_room_member(room, e).await,
AnyStateEventStub::RoomName(e) => event_emitter.on_room_name(room, e).await,
AnyStateEventStub::RoomCanonicalAlias(e) => {
AnySyncRoomEvent::State(event) => match event {
AnySyncStateEvent::RoomMember(e) => event_emitter.on_room_member(room, e).await,
AnySyncStateEvent::RoomName(e) => event_emitter.on_room_name(room, e).await,
AnySyncStateEvent::RoomCanonicalAlias(e) => {
event_emitter.on_room_canonical_alias(room, e).await
}
AnyStateEventStub::RoomAliases(e) => event_emitter.on_room_aliases(room, e).await,
AnyStateEventStub::RoomAvatar(e) => event_emitter.on_room_avatar(room, e).await,
AnyStateEventStub::RoomPowerLevels(e) => {
AnySyncStateEvent::RoomAliases(e) => event_emitter.on_room_aliases(room, e).await,
AnySyncStateEvent::RoomAvatar(e) => event_emitter.on_room_avatar(room, e).await,
AnySyncStateEvent::RoomPowerLevels(e) => {
event_emitter.on_room_power_levels(room, e).await
}
AnyStateEventStub::RoomTombstone(e) => {
AnySyncStateEvent::RoomTombstone(e) => {
event_emitter.on_room_tombstone(room, e).await
}
AnyStateEventStub::RoomJoinRules(e) => {
AnySyncStateEvent::RoomJoinRules(e) => {
event_emitter.on_room_join_rules(room, e).await
}
AnyStateEventStub::Custom(e) => {
AnySyncStateEvent::Custom(e) => {
event_emitter
.on_unrecognized_event(room, &CustomOrRawEvent::State(e))
.await
}
_ => {}
},
AnyRoomEventStub::Message(event) => match event {
AnyMessageEventStub::RoomMessage(e) => event_emitter.on_room_message(room, e).await,
AnyMessageEventStub::RoomMessageFeedback(e) => {
AnySyncRoomEvent::Message(event) => match event {
AnySyncMessageEvent::RoomMessage(e) => event_emitter.on_room_message(room, e).await,
AnySyncMessageEvent::RoomMessageFeedback(e) => {
event_emitter.on_room_message_feedback(room, e).await
}
AnyMessageEventStub::RoomRedaction(e) => {
AnySyncMessageEvent::RoomRedaction(e) => {
event_emitter.on_room_redaction(room, e).await
}
AnyMessageEventStub::Custom(e) => {
AnySyncMessageEvent::Custom(e) => {
event_emitter
.on_unrecognized_event(room, &CustomOrRawEvent::Message(e))
.await
}
_ => {}
},
AnySyncRoomEvent::RedactedState(_event) => {}
AnySyncRoomEvent::RedactedMessage(_event) => {}
}
}
pub(crate) async fn emit_state_event(
&self,
room_id: &RoomId,
event: &AnyStateEventStub,
event: &AnySyncStateEvent,
room_state: RoomStateType,
) {
let lock = self.event_emitter.read().await;
@ -1552,32 +1554,32 @@ impl BaseClient {
};
match event {
AnyStateEventStub::RoomMember(member) => {
AnySyncStateEvent::RoomMember(member) => {
event_emitter.on_state_member(room, &member).await
}
AnyStateEventStub::RoomName(name) => event_emitter.on_state_name(room, &name).await,
AnyStateEventStub::RoomCanonicalAlias(canonical) => {
AnySyncStateEvent::RoomName(name) => event_emitter.on_state_name(room, &name).await,
AnySyncStateEvent::RoomCanonicalAlias(canonical) => {
event_emitter
.on_state_canonical_alias(room, &canonical)
.await
}
AnyStateEventStub::RoomAliases(aliases) => {
AnySyncStateEvent::RoomAliases(aliases) => {
event_emitter.on_state_aliases(room, &aliases).await
}
AnyStateEventStub::RoomAvatar(avatar) => {
AnySyncStateEvent::RoomAvatar(avatar) => {
event_emitter.on_state_avatar(room, &avatar).await
}
AnyStateEventStub::RoomPowerLevels(power) => {
AnySyncStateEvent::RoomPowerLevels(power) => {
event_emitter.on_state_power_levels(room, &power).await
}
AnyStateEventStub::RoomJoinRules(rules) => {
AnySyncStateEvent::RoomJoinRules(rules) => {
event_emitter.on_state_join_rules(room, &rules).await
}
AnyStateEventStub::RoomTombstone(tomb) => {
AnySyncStateEvent::RoomTombstone(tomb) => {
// TODO make `on_state_tombstone` method
event_emitter.on_room_tombstone(room, &tomb).await
}
AnyStateEventStub::Custom(custom) => {
AnySyncStateEvent::Custom(custom) => {
event_emitter
.on_unrecognized_event(room, &CustomOrRawEvent::State(custom))
.await
@ -1589,7 +1591,7 @@ impl BaseClient {
pub(crate) async fn emit_stripped_state_event(
&self,
room_id: &RoomId,
event: &AnyStrippedStateEventStub,
event: &AnyStrippedStateEvent,
prev_content: Option<MemberEventContent>,
room_state: RoomStateType,
) {
@ -1625,33 +1627,33 @@ impl BaseClient {
};
match event {
AnyStrippedStateEventStub::RoomMember(member) => {
AnyStrippedStateEvent::RoomMember(member) => {
event_emitter
.on_stripped_state_member(room, &member, prev_content)
.await
}
AnyStrippedStateEventStub::RoomName(name) => {
AnyStrippedStateEvent::RoomName(name) => {
event_emitter.on_stripped_state_name(room, &name).await
}
AnyStrippedStateEventStub::RoomCanonicalAlias(canonical) => {
AnyStrippedStateEvent::RoomCanonicalAlias(canonical) => {
event_emitter
.on_stripped_state_canonical_alias(room, &canonical)
.await
}
AnyStrippedStateEventStub::RoomAliases(aliases) => {
AnyStrippedStateEvent::RoomAliases(aliases) => {
event_emitter
.on_stripped_state_aliases(room, &aliases)
.await
}
AnyStrippedStateEventStub::RoomAvatar(avatar) => {
AnyStrippedStateEvent::RoomAvatar(avatar) => {
event_emitter.on_stripped_state_avatar(room, &avatar).await
}
AnyStrippedStateEventStub::RoomPowerLevels(power) => {
AnyStrippedStateEvent::RoomPowerLevels(power) => {
event_emitter
.on_stripped_state_power_levels(room, &power)
.await
}
AnyStrippedStateEventStub::RoomJoinRules(rules) => {
AnyStrippedStateEvent::RoomJoinRules(rules) => {
event_emitter
.on_stripped_state_join_rules(room, &rules)
.await
@ -1716,7 +1718,7 @@ impl BaseClient {
pub(crate) async fn emit_ephemeral_event(
&self,
room_id: &RoomId,
event: &AnyEphemeralRoomEventStub,
event: &AnySyncEphemeralRoomEvent,
room_state: RoomStateType,
) {
let lock = self.event_emitter.read().await;
@ -1751,13 +1753,13 @@ impl BaseClient {
};
match event {
AnyEphemeralRoomEventStub::FullyRead(full_read) => {
AnySyncEphemeralRoomEvent::FullyRead(full_read) => {
event_emitter.on_non_room_fully_read(room, &full_read).await
}
AnyEphemeralRoomEventStub::Typing(typing) => {
AnySyncEphemeralRoomEvent::Typing(typing) => {
event_emitter.on_non_room_typing(room, &typing).await
}
AnyEphemeralRoomEventStub::Receipt(receipt) => {
AnySyncEphemeralRoomEvent::Receipt(receipt) => {
event_emitter.on_non_room_receipt(room, &receipt).await
}
_ => {}
@ -1837,17 +1839,19 @@ impl BaseClient {
#[cfg(test)]
mod test {
use crate::identifiers::{RoomId, UserId};
use crate::{BaseClient, BaseClientConfig, Session};
use matrix_sdk_common::events::{AnyRoomEventStub, EventJson};
#[cfg(feature = "messages")]
use crate::{
events::{AnySyncRoomEvent, EventJson},
identifiers::EventId,
BaseClientConfig, JsonStore,
};
use crate::{BaseClient, Session};
use matrix_sdk_common_macros::async_trait;
use matrix_sdk_test::{async_test, test_json, EventBuilder, EventsJson};
use serde_json::json;
use std::convert::TryFrom;
use tempfile::tempdir;
#[cfg(feature = "messages")]
use crate::JsonStore;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
@ -2006,7 +2010,7 @@ mod test {
use crate::{EventEmitter, SyncRoom};
use matrix_sdk_common::events::{
room::member::{MemberEventContent, MembershipChange},
StateEventStub,
SyncStateEvent,
};
use matrix_sdk_common::locks::RwLock;
use std::sync::{
@ -2020,7 +2024,7 @@ mod test {
async fn on_room_member(
&self,
room: SyncRoom,
event: &StateEventStub<MemberEventContent>,
event: &SyncStateEvent<MemberEventContent>,
) {
if let SyncRoom::Joined(_) = room {
if let MembershipChange::Joined = event.membership_change() {
@ -2157,8 +2161,8 @@ mod test {
let member = room.joined_members.get(&user_id).unwrap();
assert_eq!(*member.display_name.as_ref().unwrap(), "changed");
// The second part tests that the event is emitted correctly. If `prev_content` was
// missing, this bool is reset to false.
// The second part tests that the event is emitted correctly. If `prev_content` were
// missing, this bool would had been flipped.
assert!(passed.load(Ordering::SeqCst))
}
@ -2393,7 +2397,7 @@ mod test {
"type": "m.room.redaction",
"redacts": "$152037280074GZeOm:localhost"
});
let mut event: EventJson<AnyRoomEventStub> = serde_json::from_value(json).unwrap();
let mut event: EventJson<AnySyncRoomEvent> = serde_json::from_value(json).unwrap();
client
.receive_joined_timeline_event(&room_id, &mut event)
.await
@ -2402,16 +2406,22 @@ mod test {
// check that the message has actually been redacted
for room in client.joined_rooms().read().await.values() {
let queue = &room.read().await.messages;
if let crate::events::AnyMessageEventContent::RoomRedaction(content) =
&queue.msgs[0].content
if let crate::events::AnyPossiblyRedactedSyncMessageEvent::Redacted(
crate::events::AnyRedactedSyncMessageEvent::RoomMessage(event),
) = &queue.msgs[0].deref()
{
assert_eq!(content.reason, Some("😀".to_string()));
// this is the id from the message event in the sync response
assert_eq!(
event.event_id,
EventId::try_from("$152037280074GZeOm:localhost").unwrap()
)
} else {
panic!("[pre store sync] message event in message queue should be redacted")
panic!("message event in message queue should be redacted")
}
}
// `receive_joined_timeline_event` does not save the state to the store so we must
// `receive_joined_timeline_event` does not save the state to the store
// so we must do it ourselves
client.store_room_state(&room_id).await.unwrap();
// we load state from the store only
@ -2424,10 +2434,15 @@ mod test {
// properly
for room in client.joined_rooms().read().await.values() {
let queue = &room.read().await.messages;
if let crate::events::AnyMessageEventContent::RoomRedaction(content) =
&queue.msgs[0].content
if let crate::events::AnyPossiblyRedactedSyncMessageEvent::Redacted(
crate::events::AnyRedactedSyncMessageEvent::RoomMessage(event),
) = &queue.msgs[0].deref()
{
assert_eq!(content.reason, Some("😀".to_string()));
// this is the id from the message event in the sync response
assert_eq!(
event.event_id,
EventId::try_from("$152037280074GZeOm:localhost").unwrap()
)
} else {
panic!("[post store sync] message event in message queue should be redacted")
}

View file

@ -33,11 +33,11 @@ use crate::events::{
message::{feedback::FeedbackEventContent, MessageEventContent as MsgEventContent},
name::NameEventContent,
power_levels::PowerLevelsEventContent,
redaction::RedactionEventStub,
redaction::SyncRedactionEvent,
tombstone::TombstoneEventContent,
},
typing::TypingEventContent,
BasicEvent, EphemeralRoomEvent, MessageEventStub, StateEventStub, StrippedStateEventStub,
BasicEvent, EphemeralRoomEvent, StrippedStateEvent, SyncMessageEvent, SyncStateEvent,
};
use crate::{Room, RoomState};
use matrix_sdk_common_macros::async_trait;
@ -55,11 +55,11 @@ pub enum CustomOrRawEvent<'c> {
/// A custom basic event.
EphemeralRoom(&'c EphemeralRoomEvent<CustomEventContent>),
/// A custom room event.
Message(&'c MessageEventStub<CustomEventContent>),
Message(&'c SyncMessageEvent<CustomEventContent>),
/// A custom state event.
State(&'c StateEventStub<CustomEventContent>),
State(&'c SyncStateEvent<CustomEventContent>),
/// A custom stripped state event.
StrippedState(&'c StrippedStateEventStub<CustomEventContent>),
StrippedState(&'c StrippedStateEvent<CustomEventContent>),
}
/// This trait allows any type implementing `EventEmitter` to specify event callbacks for each event.
@ -74,7 +74,7 @@ pub enum CustomOrRawEvent<'c> {
/// # self,
/// # events::{
/// # room::message::{MessageEventContent, TextMessageEventContent},
/// # MessageEventStub
/// # SyncMessageEvent
/// # },
/// # EventEmitter, SyncRoom
/// # };
@ -85,9 +85,9 @@ pub enum CustomOrRawEvent<'c> {
///
/// #[async_trait]
/// impl EventEmitter for EventCallback {
/// async fn on_room_message(&self, room: SyncRoom, event: &MessageEventStub<MessageEventContent>) {
/// async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
/// if let SyncRoom::Joined(room) = room {
/// if let MessageEventStub {
/// if let SyncMessageEvent {
/// content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
/// sender,
/// ..
@ -112,114 +112,109 @@ pub enum CustomOrRawEvent<'c> {
pub trait EventEmitter: Send + Sync {
// ROOM EVENTS from `IncomingTimeline`
/// Fires when `Client` receives a `RoomEvent::RoomMember` event.
async fn on_room_member(&self, _: SyncRoom, _: &StateEventStub<MemberEventContent>) {}
async fn on_room_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomName` event.
async fn on_room_name(&self, _: SyncRoom, _: &StateEventStub<NameEventContent>) {}
async fn on_room_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomCanonicalAlias` event.
async fn on_room_canonical_alias(
&self,
_: SyncRoom,
_: &StateEventStub<CanonicalAliasEventContent>,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
}
/// Fires when `Client` receives a `RoomEvent::RoomAliases` event.
async fn on_room_aliases(&self, _: SyncRoom, _: &StateEventStub<AliasesEventContent>) {}
async fn on_room_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomAvatar` event.
async fn on_room_avatar(&self, _: SyncRoom, _: &StateEventStub<AvatarEventContent>) {}
async fn on_room_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomMessage` event.
async fn on_room_message(&self, _: SyncRoom, _: &MessageEventStub<MsgEventContent>) {}
async fn on_room_message(&self, _: SyncRoom, _: &SyncMessageEvent<MsgEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomMessageFeedback` event.
async fn on_room_message_feedback(
&self,
_: SyncRoom,
_: &MessageEventStub<FeedbackEventContent>,
_: &SyncMessageEvent<FeedbackEventContent>,
) {
}
/// Fires when `Client` receives a `RoomEvent::RoomRedaction` event.
async fn on_room_redaction(&self, _: SyncRoom, _: &RedactionEventStub) {}
async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {}
/// Fires when `Client` receives a `RoomEvent::RoomPowerLevels` event.
async fn on_room_power_levels(&self, _: SyncRoom, _: &StateEventStub<PowerLevelsEventContent>) {
async fn on_room_power_levels(&self, _: SyncRoom, _: &SyncStateEvent<PowerLevelsEventContent>) {
}
/// Fires when `Client` receives a `RoomEvent::Tombstone` event.
async fn on_room_join_rules(&self, _: SyncRoom, _: &StateEventStub<JoinRulesEventContent>) {}
async fn on_room_join_rules(&self, _: SyncRoom, _: &SyncStateEvent<JoinRulesEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::Tombstone` event.
async fn on_room_tombstone(&self, _: SyncRoom, _: &StateEventStub<TombstoneEventContent>) {}
async fn on_room_tombstone(&self, _: SyncRoom, _: &SyncStateEvent<TombstoneEventContent>) {}
// `RoomEvent`s from `IncomingState`
/// Fires when `Client` receives a `StateEvent::RoomMember` event.
async fn on_state_member(&self, _: SyncRoom, _: &StateEventStub<MemberEventContent>) {}
async fn on_state_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomName` event.
async fn on_state_name(&self, _: SyncRoom, _: &StateEventStub<NameEventContent>) {}
async fn on_state_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomCanonicalAlias` event.
async fn on_state_canonical_alias(
&self,
_: SyncRoom,
_: &StateEventStub<CanonicalAliasEventContent>,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
}
/// Fires when `Client` receives a `StateEvent::RoomAliases` event.
async fn on_state_aliases(&self, _: SyncRoom, _: &StateEventStub<AliasesEventContent>) {}
async fn on_state_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomAvatar` event.
async fn on_state_avatar(&self, _: SyncRoom, _: &StateEventStub<AvatarEventContent>) {}
async fn on_state_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomPowerLevels` event.
async fn on_state_power_levels(
&self,
_: SyncRoom,
_: &StateEventStub<PowerLevelsEventContent>,
_: &SyncStateEvent<PowerLevelsEventContent>,
) {
}
/// Fires when `Client` receives a `StateEvent::RoomJoinRules` event.
async fn on_state_join_rules(&self, _: SyncRoom, _: &StateEventStub<JoinRulesEventContent>) {}
async fn on_state_join_rules(&self, _: SyncRoom, _: &SyncStateEvent<JoinRulesEventContent>) {}
// `AnyStrippedStateEvent`s
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomMember` event.
async fn on_stripped_state_member(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<MemberEventContent>,
_: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomName` event.
async fn on_stripped_state_name(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<NameEventContent>,
) {
}
async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedStateEvent<NameEventContent>) {}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
async fn on_stripped_state_canonical_alias(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<CanonicalAliasEventContent>,
_: &StrippedStateEvent<CanonicalAliasEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAliases` event.
async fn on_stripped_state_aliases(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<AliasesEventContent>,
_: &StrippedStateEvent<AliasesEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAvatar` event.
async fn on_stripped_state_avatar(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<AvatarEventContent>,
_: &StrippedStateEvent<AvatarEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
async fn on_stripped_state_power_levels(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<PowerLevelsEventContent>,
_: &StrippedStateEvent<PowerLevelsEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
async fn on_stripped_state_join_rules(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<JoinRulesEventContent>,
_: &StrippedStateEvent<JoinRulesEventContent>,
) {
}
@ -276,79 +271,79 @@ mod test {
#[async_trait]
impl EventEmitter for EvEmitterTest {
async fn on_room_member(&self, _: SyncRoom, _: &StateEventStub<MemberEventContent>) {
async fn on_room_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {
self.0.lock().await.push("member".to_string())
}
async fn on_room_name(&self, _: SyncRoom, _: &StateEventStub<NameEventContent>) {
async fn on_room_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {
self.0.lock().await.push("name".to_string())
}
async fn on_room_canonical_alias(
&self,
_: SyncRoom,
_: &StateEventStub<CanonicalAliasEventContent>,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
self.0.lock().await.push("canonical".to_string())
}
async fn on_room_aliases(&self, _: SyncRoom, _: &StateEventStub<AliasesEventContent>) {
async fn on_room_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {
self.0.lock().await.push("aliases".to_string())
}
async fn on_room_avatar(&self, _: SyncRoom, _: &StateEventStub<AvatarEventContent>) {
async fn on_room_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {
self.0.lock().await.push("avatar".to_string())
}
async fn on_room_message(&self, _: SyncRoom, _: &MessageEventStub<MsgEventContent>) {
async fn on_room_message(&self, _: SyncRoom, _: &SyncMessageEvent<MsgEventContent>) {
self.0.lock().await.push("message".to_string())
}
async fn on_room_message_feedback(
&self,
_: SyncRoom,
_: &MessageEventStub<FeedbackEventContent>,
_: &SyncMessageEvent<FeedbackEventContent>,
) {
self.0.lock().await.push("feedback".to_string())
}
async fn on_room_redaction(&self, _: SyncRoom, _: &RedactionEventStub) {
async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {
self.0.lock().await.push("redaction".to_string())
}
async fn on_room_power_levels(
&self,
_: SyncRoom,
_: &StateEventStub<PowerLevelsEventContent>,
_: &SyncStateEvent<PowerLevelsEventContent>,
) {
self.0.lock().await.push("power".to_string())
}
async fn on_room_tombstone(&self, _: SyncRoom, _: &StateEventStub<TombstoneEventContent>) {
async fn on_room_tombstone(&self, _: SyncRoom, _: &SyncStateEvent<TombstoneEventContent>) {
self.0.lock().await.push("tombstone".to_string())
}
async fn on_state_member(&self, _: SyncRoom, _: &StateEventStub<MemberEventContent>) {
async fn on_state_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {
self.0.lock().await.push("state member".to_string())
}
async fn on_state_name(&self, _: SyncRoom, _: &StateEventStub<NameEventContent>) {
async fn on_state_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {
self.0.lock().await.push("state name".to_string())
}
async fn on_state_canonical_alias(
&self,
_: SyncRoom,
_: &StateEventStub<CanonicalAliasEventContent>,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
self.0.lock().await.push("state canonical".to_string())
}
async fn on_state_aliases(&self, _: SyncRoom, _: &StateEventStub<AliasesEventContent>) {
async fn on_state_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {
self.0.lock().await.push("state aliases".to_string())
}
async fn on_state_avatar(&self, _: SyncRoom, _: &StateEventStub<AvatarEventContent>) {
async fn on_state_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {
self.0.lock().await.push("state avatar".to_string())
}
async fn on_state_power_levels(
&self,
_: SyncRoom,
_: &StateEventStub<PowerLevelsEventContent>,
_: &SyncStateEvent<PowerLevelsEventContent>,
) {
self.0.lock().await.push("state power".to_string())
}
async fn on_state_join_rules(
&self,
_: SyncRoom,
_: &StateEventStub<JoinRulesEventContent>,
_: &SyncStateEvent<JoinRulesEventContent>,
) {
self.0.lock().await.push("state rules".to_string())
}
@ -358,7 +353,7 @@ mod test {
async fn on_stripped_state_member(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<MemberEventContent>,
_: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
self.0
@ -370,7 +365,7 @@ mod test {
async fn on_stripped_state_name(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<NameEventContent>,
_: &StrippedStateEvent<NameEventContent>,
) {
self.0.lock().await.push("stripped state name".to_string())
}
@ -378,7 +373,7 @@ mod test {
async fn on_stripped_state_canonical_alias(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<CanonicalAliasEventContent>,
_: &StrippedStateEvent<CanonicalAliasEventContent>,
) {
self.0
.lock()
@ -389,7 +384,7 @@ mod test {
async fn on_stripped_state_aliases(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<AliasesEventContent>,
_: &StrippedStateEvent<AliasesEventContent>,
) {
self.0
.lock()
@ -400,7 +395,7 @@ mod test {
async fn on_stripped_state_avatar(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<AvatarEventContent>,
_: &StrippedStateEvent<AvatarEventContent>,
) {
self.0
.lock()
@ -411,7 +406,7 @@ mod test {
async fn on_stripped_state_power_levels(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<PowerLevelsEventContent>,
_: &StrippedStateEvent<PowerLevelsEventContent>,
) {
self.0.lock().await.push("stripped state power".to_string())
}
@ -419,7 +414,7 @@ mod test {
async fn on_stripped_state_join_rules(
&self,
_: SyncRoom,
_: &StrippedStateEventStub<JoinRulesEventContent>,
_: &StrippedStateEvent<JoinRulesEventContent>,
) {
self.0.lock().await.push("stripped state rules".to_string())
}
@ -581,7 +576,7 @@ mod test {
"unrecognized event",
"redaction",
"unrecognized event",
"unrecognized event",
// "unrecognized event", this is actually a redacted "m.room.messages" event
"receipt event",
"typing event"
],

View file

@ -47,11 +47,16 @@ mod state;
pub use client::{BaseClient, BaseClientConfig, RoomState, RoomStateType};
pub use event_emitter::{CustomOrRawEvent, EventEmitter, SyncRoom};
pub use models::Room;
pub use state::{AllRooms, ClientState};
#[cfg(feature = "encryption")]
pub use matrix_sdk_crypto::{Device, TrustState};
pub use models::Room;
pub use state::AllRooms;
pub use state::ClientState;
#[cfg(feature = "messages")]
#[cfg_attr(docsrs, doc(cfg(feature = "messages")))]
pub use models::{MessageQueue, MessageWrapper, PossiblyRedactedExt};
#[cfg(not(target_arch = "wasm32"))]
pub use state::JsonStore;
pub use state::StateStore;

View file

@ -3,17 +3,50 @@
//! The `Room` struct optionally holds a `MessageQueue` if the "messages"
//! feature is enabled.
use std::cmp::Ordering;
use std::ops::{Deref, DerefMut};
use std::vec::IntoIter;
use crate::events::{AnyMessageEventContent, AnyMessageEventStub, MessageEventStub};
use std::{
cmp::Ordering,
ops::{Deref, DerefMut},
time::SystemTime,
vec::IntoIter,
};
use matrix_sdk_common::identifiers::EventId;
use serde::{de, ser, Serialize};
use crate::events::AnyPossiblyRedactedSyncMessageEvent;
/// Exposes some of the field access methods found in the event held by
/// `AnyPossiblyRedacted*` enums.
///
/// This is just an extension trait to aid the ease of use of certain event enums.
pub trait PossiblyRedactedExt {
/// Access the redacted or full events `event_id` field.
fn event_id(&self) -> &EventId;
/// Access the redacted or full events `origin_server_ts` field.
fn origin_server_ts(&self) -> &SystemTime;
}
impl PossiblyRedactedExt for AnyPossiblyRedactedSyncMessageEvent {
/// Access the underlying events `event_id`.
fn event_id(&self) -> &EventId {
match self {
Self::Regular(e) => e.event_id(),
Self::Redacted(e) => e.event_id(),
}
}
/// Access the underlying events `origin_server_ts`.
fn origin_server_ts(&self) -> &SystemTime {
match self {
Self::Regular(e) => e.origin_server_ts(),
Self::Redacted(e) => e.origin_server_ts(),
}
}
}
const MESSAGE_QUEUE_CAP: usize = 35;
pub type SyncMessageEvent = MessageEventStub<AnyMessageEventContent>;
pub type SyncMessageEvent = AnyPossiblyRedactedSyncMessageEvent;
/// A queue that holds the 35 most recent messages received from the server.
#[derive(Clone, Debug, Default)]
@ -29,18 +62,6 @@ pub struct MessageQueue {
#[derive(Clone, Debug, Serialize)]
pub struct MessageWrapper(pub SyncMessageEvent);
impl MessageWrapper {
pub fn clone_into_any_content(event: &AnyMessageEventStub) -> SyncMessageEvent {
MessageEventStub {
content: event.content(),
sender: event.sender().clone(),
origin_server_ts: *event.origin_server_ts(),
event_id: event.event_id().clone(),
unsigned: event.unsigned().clone(),
}
}
}
impl Deref for MessageWrapper {
type Target = SyncMessageEvent;
@ -57,7 +78,7 @@ impl DerefMut for MessageWrapper {
impl PartialEq for MessageWrapper {
fn eq(&self, other: &MessageWrapper) -> bool {
self.0.event_id == other.0.event_id
self.0.event_id() == other.0.event_id()
}
}
@ -65,7 +86,7 @@ impl Eq for MessageWrapper {}
impl PartialOrd for MessageWrapper {
fn partial_cmp(&self, other: &MessageWrapper) -> Option<Ordering> {
Some(self.0.origin_server_ts.cmp(&other.0.origin_server_ts))
Some(self.0.origin_server_ts().cmp(&other.0.origin_server_ts()))
}
}
@ -82,7 +103,7 @@ impl PartialEq for MessageQueue {
.msgs
.iter()
.zip(other.msgs.iter())
.all(|(msg_a, msg_b)| msg_a.event_id == msg_b.event_id)
.all(|(msg_a, msg_b)| msg_a.event_id() == msg_b.event_id())
}
}
@ -100,7 +121,7 @@ impl MessageQueue {
pub fn push(&mut self, msg: SyncMessageEvent) -> bool {
// only push new messages into the queue
if let Some(latest) = self.msgs.last() {
if msg.origin_server_ts < latest.origin_server_ts && self.msgs.len() >= 10 {
if msg.origin_server_ts() < latest.origin_server_ts() && self.msgs.len() >= 10 {
return false;
}
}
@ -120,10 +141,12 @@ impl MessageQueue {
true
}
/// Iterate over the messages in the queue.
pub fn iter(&self) -> impl Iterator<Item = &MessageWrapper> {
self.msgs.iter()
}
/// Iterate over each message mutably.
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut MessageWrapper> {
self.msgs.iter_mut()
}
@ -183,17 +206,18 @@ pub(crate) mod ser_deser {
#[cfg(test)]
mod test {
use super::*;
use std::collections::HashMap;
use std::convert::TryFrom;
use matrix_sdk_common::{
events::{AnyPossiblyRedactedSyncMessageEvent, AnySyncMessageEvent},
identifiers::{RoomId, UserId},
};
use matrix_sdk_test::test_json;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
use matrix_sdk_test::test_json;
use crate::identifiers::{RoomId, UserId};
use super::*;
use crate::Room;
#[test]
@ -204,7 +228,9 @@ mod test {
let mut room = Room::new(&id, &user);
let json: &serde_json::Value = &test_json::MESSAGE_TEXT;
let msg = serde_json::from_value::<SyncMessageEvent>(json.clone()).unwrap();
let msg = AnyPossiblyRedactedSyncMessageEvent::Regular(
serde_json::from_value::<AnySyncMessageEvent>(json.clone()).unwrap(),
);
let mut msgs = MessageQueue::new();
msgs.push(msg.clone());
@ -216,7 +242,6 @@ mod test {
serde_json::json!({
"!roomid:example.com": {
"room_id": "!roomid:example.com",
"disambiguated_display_names": {},
"room_name": {
"name": null,
"canonical_alias": null,
@ -250,7 +275,9 @@ mod test {
let mut room = Room::new(&id, &user);
let json: &serde_json::Value = &test_json::MESSAGE_TEXT;
let msg = serde_json::from_value::<SyncMessageEvent>(json.clone()).unwrap();
let msg = AnyPossiblyRedactedSyncMessageEvent::Regular(
serde_json::from_value::<AnySyncMessageEvent>(json.clone()).unwrap(),
);
let mut msgs = MessageQueue::new();
msgs.push(msg.clone());
@ -262,7 +289,6 @@ mod test {
let json = serde_json::json!({
"!roomid:example.com": {
"room_id": "!roomid:example.com",
"disambiguated_display_names": {},
"room_name": {
"name": null,
"canonical_alias": null,

View file

@ -4,5 +4,8 @@ mod message;
mod room;
mod room_member;
#[cfg(feature = "messages")]
#[cfg_attr(docsrs, doc(cfg(feature = "messages")))]
pub use message::{MessageQueue, MessageWrapper, PossiblyRedactedExt};
pub use room::{Room, RoomName};
pub use room_member::RoomMember;

File diff suppressed because it is too large Load diff

View file

@ -15,16 +15,17 @@
use std::convert::TryFrom;
use crate::events::presence::{PresenceEvent, PresenceEventContent, PresenceState};
use crate::events::room::{
member::{MemberEventContent, MembershipChange, MembershipState},
power_levels::PowerLevelsEventContent,
use matrix_sdk_common::{
events::{
presence::{PresenceEvent, PresenceState},
room::member::MemberEventContent,
SyncStateEvent,
},
identifiers::{RoomId, UserId},
js_int::{Int, UInt},
};
use crate::events::StateEventStub;
use crate::identifiers::{RoomId, UserId};
use crate::js_int::{int, Int, UInt};
use serde::{Deserialize, Serialize};
// Notes: if Alice invites Bob into a room we will get an event with the sender as Alice and the state key as Bob.
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -34,6 +35,9 @@ pub struct RoomMember {
pub user_id: UserId,
/// The human readable name of the user.
pub display_name: Option<String>,
/// Whether the member's display name is ambiguous due to being shared with
/// other members.
pub display_name_ambiguous: bool,
/// The matrix url of the users avatar.
pub avatar_url: Option<String>,
/// The time, in ms, since the user interacted with the server.
@ -52,10 +56,18 @@ pub struct RoomMember {
pub power_level: Option<Int>,
/// The normalized power level of this `RoomMember` (0-100).
pub power_level_norm: Option<Int>,
/// The `MembershipState` of this `RoomMember`.
pub membership: MembershipState,
/// The human readable name of this room member.
pub name: String,
// FIXME: The docstring below is currently a lie since we only store the initial event that
// creates the member (the one we pass to RoomMember::new).
//
// The intent of this field is to keep the last (or last few?) state events related to the room
// member cached so we can quickly go back to the previous one in case some of them get
// redacted. Keeping all state for each room member is probably too much.
//
// Needs design.
/// The events that created the state of this room member.
pub events: Vec<SyncStateEvent<MemberEventContent>>,
/// The `PresenceEvent`s connected to this user.
pub presence_events: Vec<PresenceEvent>,
}
@ -67,18 +79,20 @@ impl PartialEq for RoomMember {
&& self.user_id == other.user_id
&& self.name == other.name
&& self.display_name == other.display_name
&& self.display_name_ambiguous == other.display_name_ambiguous
&& self.avatar_url == other.avatar_url
&& self.last_active_ago == other.last_active_ago
}
}
impl RoomMember {
pub fn new(event: &StateEventStub<MemberEventContent>, room_id: &RoomId) -> Self {
pub fn new(event: &SyncStateEvent<MemberEventContent>, room_id: &RoomId) -> Self {
Self {
name: event.state_key.clone(),
room_id: room_id.clone(),
user_id: UserId::try_from(event.state_key.as_str()).unwrap(),
display_name: event.content.displayname.clone(),
display_name_ambiguous: false,
avatar_url: event.content.avatar_url.clone(),
presence: None,
status_msg: None,
@ -87,12 +101,13 @@ impl RoomMember {
typing: None,
power_level: None,
power_level_norm: None,
membership: event.content.membership,
presence_events: vec![],
presence_events: Vec::default(),
events: vec![event.clone()],
}
}
/// Returns the most ergonomic name available for the member.
/// Returns the most ergonomic (but potentially ambiguous/non-unique) name
/// available for the member.
///
/// This is the member's display name if it is set, otherwise their MXID.
pub fn name(&self) -> String {
@ -101,10 +116,11 @@ impl RoomMember {
.unwrap_or_else(|| format!("{}", self.user_id))
}
/// Returns a name for the member which is guaranteed to be unique.
/// Returns a name for the member which is guaranteed to be unique, but not
/// necessarily the most ergonomic.
///
/// This is either of the format "DISPLAY_NAME (MXID)" if the display name is set for the
/// member, or simply "MXID" if not.
/// This is either a name in the format "DISPLAY_NAME (MXID)" if the
/// member's display name is set, or simply "MXID" if not.
pub fn unique_name(&self) -> String {
self.display_name
.clone()
@ -112,100 +128,28 @@ impl RoomMember {
.unwrap_or_else(|| format!("{}", self.user_id))
}
/// Handle profile updates.
pub(crate) fn update_profile(&mut self, event: &StateEventStub<MemberEventContent>) -> bool {
use MembershipChange::*;
match event.membership_change() {
// we assume that the profile has changed
ProfileChanged { .. } => {
self.display_name = event.content.displayname.clone();
self.avatar_url = event.content.avatar_url.clone();
true
}
// We're only interested in profile changes here.
_ => false,
}
}
pub fn update_power(
&mut self,
event: &StateEventStub<PowerLevelsEventContent>,
max_power: Int,
) -> bool {
let changed;
if let Some(user_power) = event.content.users.get(&self.user_id) {
changed = self.power_level != Some(*user_power);
self.power_level = Some(*user_power);
/// Get the disambiguated display name for the member which is as ergonomic
/// as possible while still guaranteeing it is unique.
///
/// If the member's display name is currently ambiguous (i.e. shared by
/// other room members), this method will return the same result as
/// `RoomMember::unique_name`. Otherwise, this method will return the same
/// result as `RoomMember::name`.
///
/// This is usually the name you want when showing room messages from the
/// member or when showing the member in the member list.
///
/// **Warning**: When displaying a room member's display name, clients
/// *must* use a disambiguated name, so they *must not* use
/// `RoomMember::display_name` directly. Clients *should* use this method to
/// obtain the name, but an acceptable alternative is to use
/// `RoomMember::unique_name` in certain situations.
pub fn disambiguated_name(&self) -> String {
if self.display_name_ambiguous {
self.unique_name()
} else {
changed = self.power_level != Some(event.content.users_default);
self.power_level = Some(event.content.users_default);
self.name()
}
if max_power > int!(0) {
self.power_level_norm = Some((self.power_level.unwrap() * int!(100)) / max_power);
}
changed
}
/// If the current `PresenceEvent` updated the state of this `User`.
///
/// Returns true if the specific users presence has changed, false otherwise.
///
/// # Arguments
///
/// * `presence` - The presence event for a this room member.
pub fn did_update_presence(&self, presence: &PresenceEvent) -> bool {
let PresenceEvent {
content:
PresenceEventContent {
avatar_url,
currently_active,
displayname,
last_active_ago,
presence,
status_msg,
},
..
} = presence;
self.display_name == *displayname
&& self.avatar_url == *avatar_url
&& self.presence.as_ref() == Some(presence)
&& self.status_msg == *status_msg
&& self.last_active_ago == *last_active_ago
&& self.currently_active == *currently_active
}
/// Updates the `User`s presence.
///
/// This should only be used if `did_update_presence` was true.
///
/// # Arguments
///
/// * `presence` - The presence event for a this room member.
pub fn update_presence(&mut self, presence_ev: &PresenceEvent) {
let PresenceEvent {
content:
PresenceEventContent {
avatar_url,
currently_active,
displayname,
last_active_ago,
presence,
status_msg,
},
..
} = presence_ev;
self.presence_events.push(presence_ev.clone());
self.avatar_url = avatar_url.clone();
self.currently_active = *currently_active;
self.display_name = displayname.clone();
self.last_active_ago = *last_active_ago;
self.presence = Some(*presence);
self.status_msg = status_msg.clone();
}
}
@ -234,7 +178,9 @@ mod test {
client
}
fn get_room_id() -> RoomId {
// TODO: Move this to EventBuilder since it's a magic room ID used in EventBuilder's example
// events.
fn test_room_id() -> RoomId {
RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap()
}
@ -242,11 +188,11 @@ mod test {
async fn room_member_events() {
let client = get_client().await;
let room_id = get_room_id();
let room_id = test_room_id();
let mut response = EventBuilder::default()
.add_state_event(EventsJson::Member)
.add_state_event(EventsJson::PowerLevels)
.add_room_event(EventsJson::Member)
.add_room_event(EventsJson::PowerLevels)
.build_sync_response();
client.receive_sync_response(&mut response).await.unwrap();
@ -261,15 +207,65 @@ mod test {
assert_eq!(member.power_level, Some(int!(100)));
}
#[async_test]
async fn room_member_display_name_change() {
let client = get_client().await;
let room_id = test_room_id();
let mut builder = EventBuilder::default();
let mut initial_response = builder
.add_room_event(EventsJson::Member)
.build_sync_response();
let mut name_change_response = builder
.add_room_event(EventsJson::MemberNameChange)
.build_sync_response();
client
.receive_sync_response(&mut initial_response)
.await
.unwrap();
let room = client.get_joined_room(&room_id).await.unwrap();
// Initially, the display name is "example".
{
let room = room.read().await;
let member = room
.joined_members
.get(&UserId::try_from("@example:localhost").unwrap())
.unwrap();
assert_eq!(member.display_name.as_ref().unwrap(), "example");
}
client
.receive_sync_response(&mut name_change_response)
.await
.unwrap();
// Afterwards, the display name is "changed".
{
let room = room.read().await;
let member = room
.joined_members
.get(&UserId::try_from("@example:localhost").unwrap())
.unwrap();
assert_eq!(member.display_name.as_ref().unwrap(), "changed");
}
}
#[async_test]
async fn member_presence_events() {
let client = get_client().await;
let room_id = get_room_id();
let room_id = test_room_id();
let mut response = EventBuilder::default()
.add_state_event(EventsJson::Member)
.add_state_event(EventsJson::PowerLevels)
.add_room_event(EventsJson::Member)
.add_room_event(EventsJson::PowerLevels)
.add_presence_event(EventsJson::Presence)
.build_sync_response();

View file

@ -39,6 +39,36 @@ impl JsonStore {
user_path_set: AtomicBool::new(false),
})
}
/// Build a path for a file where the Room state to be stored in.
async fn build_room_path(&self, room_state: &str, room_id: &RoomId) -> PathBuf {
let mut path = self.path.read().await.clone();
path.push("rooms");
path.push(room_state);
path.push(JsonStore::sanitize_room_id(room_id));
path.set_extension("json");
path
}
/// Build a path for the file where the Client state to be stored in.
async fn build_client_path(&self) -> PathBuf {
let mut path = self.path.read().await.clone();
path.push("client");
path.set_extension("json");
path
}
/// Replace common characters that can't be used in a file name with an
/// underscore.
fn sanitize_room_id(room_id: &RoomId) -> String {
room_id.as_str().replace(
&['.', ':', '<', '>', '"', '/', '\\', '|', '?', '*'][..],
"_",
)
}
}
impl fmt::Debug for JsonStore {
@ -57,8 +87,7 @@ impl StateStore for JsonStore {
self.path.write().await.push(sess.user_id.localpart())
}
let mut path = self.path.read().await.clone();
path.push("client.json");
let path = self.build_client_path().await;
let json = async_fs::read_to_string(path)
.await
@ -114,8 +143,7 @@ impl StateStore for JsonStore {
}
async fn store_client_state(&self, state: ClientState) -> Result<()> {
let mut path = self.path.read().await.clone();
path.push("client.json");
let path = self.build_client_path().await;
if !path.exists() {
let mut dir = path.clone();
@ -146,9 +174,7 @@ impl StateStore for JsonStore {
self.path.write().await.push(room.own_user_id.localpart())
}
let mut path = self.path.read().await.clone();
path.push("rooms");
path.push(&format!("{}/{}.json", room_state, room.room_id));
let path = self.build_room_path(room_state, &room.room_id).await;
if !path.exists() {
let mut dir = path.clone();
@ -178,15 +204,13 @@ impl StateStore for JsonStore {
return Err(Error::StateStore("path for JsonStore not set".into()));
}
let mut to_del = self.path.read().await.clone();
to_del.push("rooms");
to_del.push(&format!("{}/{}.json", room_state, room_id));
let path = self.build_room_path(room_state, room_id).await;
if !to_del.exists() {
return Err(Error::StateStore(format!("file {:?} not found", to_del)));
if !path.exists() {
return Err(Error::StateStore(format!("file {:?} not found", path)));
}
tokio::fs::remove_file(to_del).await.map_err(Error::from)
tokio::fs::remove_file(path).await.map_err(Error::from)
}
}

View file

@ -159,7 +159,6 @@ mod test {
"creator": null,
"joined_members": {},
"invited_members": {},
"disambiguated_display_names": {},
"typing_users": [],
"power_levels": null,
"encrypted": null,
@ -176,7 +175,6 @@ mod test {
serde_json::json!({
"!roomid:example.com": {
"room_id": "!roomid:example.com",
"disambiguated_display_names": {},
"room_name": {
"name": null,
"canonical_alias": null,

View file

@ -11,13 +11,13 @@ repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.1.0"
[dependencies]
instant = { version = "0.1.4", features = ["wasm-bindgen", "now"] }
instant = { version = "0.1.6", features = ["wasm-bindgen", "now"] }
js_int = "0.1.8"
[dependencies.ruma]
path = "/home/poljar/werk/priv/ruma/ruma"
git = "https://github.com/ruma/ruma"
features = ["client-api"]
rev = "c19bcaab"
rev = "848b22568106d05c5444f3fe46070d5aa16e422b"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
uuid = { version = "0.8.1", features = ["v4"] }

View file

@ -14,5 +14,5 @@ version = "0.1.0"
proc-macro = true
[dependencies]
syn = "1.0.33"
syn = "1.0.34"
quote = "1.0.7"

View file

@ -29,7 +29,7 @@ url = "2.1.1"
# Misc dependencies
thiserror = "1.0.20"
tracing = "0.1.15"
tracing = "0.1.16"
atomic = "0.4.6"
dashmap = "3.11.7"

View file

@ -33,9 +33,10 @@ use crate::verify_json;
#[derive(Debug, Clone)]
pub struct Device {
user_id: Arc<UserId>,
device_id: Arc<DeviceId>,
device_id: Arc<Box<DeviceId>>,
algorithms: Arc<Vec<Algorithm>>,
keys: Arc<BTreeMap<AlgorithmAndDeviceId, String>>,
signatures: Arc<BTreeMap<UserId, BTreeMap<AlgorithmAndDeviceId, String>>>,
display_name: Arc<Option<String>>,
deleted: Arc<AtomicBool>,
trust_state: Arc<Atomic<TrustState>>,
@ -70,17 +71,19 @@ impl Device {
/// Create a new Device.
pub fn new(
user_id: UserId,
device_id: DeviceId,
device_id: Box<DeviceId>,
display_name: Option<String>,
trust_state: TrustState,
algorithms: Vec<Algorithm>,
keys: BTreeMap<AlgorithmAndDeviceId, String>,
signatures: BTreeMap<UserId, BTreeMap<AlgorithmAndDeviceId, String>>,
) -> Self {
Device {
user_id: Arc::new(user_id),
device_id: Arc::new(device_id),
display_name: Arc::new(display_name),
trust_state: Arc::new(Atomic::new(trust_state)),
signatures: Arc::new(signatures),
algorithms: Arc::new(algorithms),
keys: Arc::new(keys),
deleted: Arc::new(AtomicBool::new(false)),
@ -104,8 +107,10 @@ impl Device {
/// Get the key of the given key algorithm belonging to this device.
pub fn get_key(&self, algorithm: KeyAlgorithm) -> Option<&String> {
self.keys
.get(&AlgorithmAndDeviceId(algorithm, self.device_id.to_string()))
self.keys.get(&AlgorithmAndDeviceId(
algorithm,
self.device_id.as_ref().clone(),
))
}
/// Get a map containing all the device keys.
@ -113,6 +118,11 @@ impl Device {
&self.keys
}
/// Get a map containing all the device signatures.
pub fn signatures(&self) -> &BTreeMap<UserId, BTreeMap<AlgorithmAndDeviceId, String>> {
&self.signatures
}
/// Get the trust state of the device.
pub fn trust_state(&self) -> TrustState {
self.trust_state.load(Ordering::Relaxed)
@ -142,6 +152,7 @@ impl Device {
self.algorithms = Arc::new(device_keys.algorithms.clone());
self.keys = Arc::new(device_keys.keys.clone());
self.signatures = Arc::new(device_keys.signatures.clone());
self.display_name = display_name;
Ok(())
@ -173,37 +184,11 @@ impl Device {
pub(crate) fn mark_as_deleted(&self) {
self.deleted.store(true, Ordering::Relaxed);
}
}
#[cfg(test)]
impl From<&OlmMachine> for Device {
fn from(machine: &OlmMachine) -> Self {
Device {
user_id: Arc::new(machine.user_id().clone()),
device_id: Arc::new(machine.device_id().clone()),
algorithms: Arc::new(vec![
Algorithm::MegolmV1AesSha2,
Algorithm::OlmV1Curve25519AesSha2,
]),
keys: Arc::new(
machine
.identity_keys()
.iter()
.map(|(key, value)| {
(
AlgorithmAndDeviceId(
KeyAlgorithm::try_from(key.as_ref()).unwrap(),
machine.device_id().clone(),
),
value.to_owned(),
)
})
.collect(),
),
display_name: Arc::new(None),
deleted: Arc::new(AtomicBool::new(false)),
trust_state: Arc::new(Atomic::new(TrustState::Unset)),
}
#[cfg(test)]
pub async fn from_machine(machine: &OlmMachine) -> Device {
let device_keys = machine.account.device_keys().await;
Device::try_from(&device_keys).unwrap()
}
}
@ -215,6 +200,7 @@ impl TryFrom<&DeviceKeys> for Device {
user_id: Arc::new(device_keys.user_id.clone()),
device_id: Arc::new(device_keys.device_id.clone()),
algorithms: Arc::new(device_keys.algorithms.clone()),
signatures: Arc::new(device_keys.signatures.clone()),
keys: Arc::new(device_keys.keys.clone()),
display_name: Arc::new(
device_keys

View file

@ -13,6 +13,7 @@
// limitations under the License.
use cjson::Error as CjsonError;
use matrix_sdk_common::identifiers::{DeviceId, UserId};
use olm_rs::errors::{OlmGroupSessionError, OlmSessionError};
use serde_json::Error as SerdeError;
use thiserror::Error;
@ -49,6 +50,14 @@ pub enum OlmError {
/// The session with a device has become corrupted.
#[error("decryption failed likely because a Olm session was wedged")]
SessionWedged,
/// Encryption failed because the device does not have a valid Olm session
/// with us.
#[error(
"encryption failed because the device does not \
have a valid Olm session with us"
)]
MissingSession,
}
/// Error representing a failure during a group encryption operation.
@ -93,6 +102,9 @@ pub enum EventError {
#[error("the Encrypted message is missing the signing key of the sender")]
MissingSigningKey,
#[error("the Encrypted message is missing the sender key")]
MissingSenderKey,
#[error("the Encrypted message is missing the field {0}")]
MissingField(String),
@ -121,6 +133,29 @@ pub enum SignatureError {
VerificationError,
}
#[derive(Error, Debug)]
pub(crate) enum SessionCreationError {
#[error(
"Failed to create a new Olm session for {0} {1}, the requested \
one-time key isn't a signed curve key"
)]
OneTimeKeyNotSigned(UserId, Box<DeviceId>),
#[error(
"Tried to create a new Olm session for {0} {1}, but the signed \
one-time key is missing"
)]
OneTimeKeyMissing(UserId, Box<DeviceId>),
#[error("Failed to verify the one-time key signatures for {0} {1}: {2:?}")]
InvalidSignature(UserId, Box<DeviceId>, SignatureError),
#[error(
"Tried to create an Olm session for {0} {1}, but the device is missing \
a curve25519 key"
)]
DeviceMissingCurveKey(UserId, Box<DeviceId>),
#[error("Error creating new Olm session for {0} {1}: {2:?}")]
OlmError(UserId, Box<DeviceId>, OlmSessionError),
}
impl From<CjsonError> for SignatureError {
fn from(error: CjsonError) -> Self {
Self::CanonicalJsonError(error)

View file

@ -38,7 +38,7 @@ pub use device::{Device, TrustState};
pub use error::{MegolmError, OlmError};
pub use machine::{OlmMachine, OneTimeKeys};
pub use memory_stores::{DeviceStore, GroupSessionStore, SessionStore, UserDevices};
pub use olm::{Account, InboundGroupSession, OutboundGroupSession, Session};
pub use olm::{Account, IdentityKeys, InboundGroupSession, OutboundGroupSession, Session};
#[cfg(feature = "sqlite-cryptostore")]
pub use store::sqlite::SqliteStore;
pub use store::{CryptoStore, CryptoStoreError};
@ -83,7 +83,7 @@ pub(crate) fn verify_json(
json_object.insert("unsigned".to_string(), u);
}
let key_id = AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, key_id.to_string());
let key_id = AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, key_id.into());
let signatures = signatures.ok_or(SignatureError::NoSignatureFound)?;
let signature_object = signatures

View file

@ -23,7 +23,6 @@ use std::result::Result as StdResult;
use super::error::{EventError, MegolmError, MegolmResult, OlmError, OlmResult};
use super::olm::{
Account, GroupSessionKey, IdentityKeys, InboundGroupSession, OlmMessage, OutboundGroupSession,
Session,
};
use super::store::memorystore::MemoryStore;
#[cfg(feature = "sqlite-cryptostore")]
@ -32,13 +31,10 @@ use super::{device::Device, store::Result as StoreResult, CryptoStore};
use matrix_sdk_common::api;
use matrix_sdk_common::events::{
forwarded_room_key::ForwardedRoomKeyEventContent,
room::encrypted::{CiphertextInfo, EncryptedEventContent, OlmV1Curve25519AesSha2Content},
room::message::MessageEventContent,
room_key::RoomKeyEventContent,
room_key_request::RoomKeyRequestEventContent,
Algorithm, AnyRoomEventStub, AnyToDeviceEvent, EventJson, EventType, MessageEventStub,
ToDeviceEvent,
forwarded_room_key::ForwardedRoomKeyEventContent, room::encrypted::EncryptedEventContent,
room::message::MessageEventContent, room_key::RoomKeyEventContent,
room_key_request::RoomKeyRequestEventContent, Algorithm, AnySyncRoomEvent, AnyToDeviceEvent,
EventJson, EventType, SyncMessageEvent, ToDeviceEvent,
};
use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId};
use matrix_sdk_common::uuid::Uuid;
@ -50,7 +46,7 @@ use api::r0::{
to_device::{send_event_to_device::Request as ToDeviceRequest, DeviceIdOrAllDevices},
};
use serde_json::{json, Value};
use serde_json::Value;
use tracing::{debug, error, info, instrument, trace, warn};
/// A map from the algorithm and device id to a one-time key.
@ -64,9 +60,9 @@ pub struct OlmMachine {
/// The unique user id that owns this account.
user_id: UserId,
/// The unique device id of the device that holds this account.
device_id: DeviceId,
device_id: Box<DeviceId>,
/// Our underlying Olm Account holding our identity keys.
account: Account,
pub(crate) account: Account,
/// Store for the encryption keys.
/// Persists all the encryption keys so a client can resume the session
/// without the need to create new keys.
@ -98,10 +94,11 @@ impl OlmMachine {
/// * `user_id` - The unique id of the user that owns this machine.
///
/// * `device_id` - The unique id of the device that owns this machine.
#[allow(clippy::ptr_arg)]
pub fn new(user_id: &UserId, device_id: &DeviceId) -> Self {
OlmMachine {
user_id: user_id.clone(),
device_id: device_id.to_owned(),
device_id: device_id.into(),
account: Account::new(user_id, &device_id),
store: Box::new(MemoryStore::new()),
outbound_group_sessions: HashMap::new(),
@ -127,7 +124,7 @@ impl OlmMachine {
/// the encryption keys.
pub async fn new_with_store(
user_id: UserId,
device_id: String,
device_id: Box<DeviceId>,
mut store: Box<dyn CryptoStore>,
) -> StoreResult<Self> {
let account = match store.load_account().await? {
@ -163,14 +160,14 @@ impl OlmMachine {
/// * `device_id` - The unique id of the device that owns this machine.
pub async fn new_with_default_store<P: AsRef<Path>>(
user_id: &UserId,
device_id: &str,
device_id: &DeviceId,
path: P,
passphrase: &str,
) -> StoreResult<Self> {
let store =
SqliteStore::open_with_passphrase(&user_id, device_id, path, passphrase).await?;
OlmMachine::new_with_store(user_id.to_owned(), device_id.to_owned(), Box::new(store)).await
OlmMachine::new_with_store(user_id.to_owned(), device_id.into(), Box::new(store)).await
}
/// The unique user id that owns this identity.
@ -254,7 +251,7 @@ impl OlmMachine {
pub async fn get_missing_sessions(
&mut self,
users: impl Iterator<Item = &UserId>,
) -> OlmResult<BTreeMap<UserId, BTreeMap<DeviceId, KeyAlgorithm>>> {
) -> OlmResult<BTreeMap<UserId, BTreeMap<Box<DeviceId>, KeyAlgorithm>>> {
let mut missing = BTreeMap::new();
for user_id in users {
@ -281,10 +278,8 @@ impl OlmMachine {
}
let user_map = missing.get_mut(user_id).unwrap();
let _ = user_map.insert(
device.device_id().to_owned(),
KeyAlgorithm::SignedCurve25519,
);
let _ =
user_map.insert(device.device_id().into(), KeyAlgorithm::SignedCurve25519);
}
}
}
@ -306,76 +301,35 @@ impl OlmMachine {
for (user_id, user_devices) in &response.one_time_keys {
for (device_id, key_map) in user_devices {
let device = if let Some(d) = self
.store
.get_device(&user_id, device_id)
.await
.expect("Can't get devices")
{
d
} else {
warn!(
"Tried to create an Olm session for {} {}, but the device is unknown",
user_id, device_id
);
continue;
};
// TODO move this logic into the account, pass the device to the
// account when creating an outbound session.
let one_time_key = if let Some(k) = key_map.values().next() {
match k {
OneTimeKey::SignedKey(k) => k,
OneTimeKey::Key(_) => {
let device: Device = match self.store.get_device(&user_id, device_id).await {
Ok(d) => {
if let Some(d) = d {
d
} else {
warn!(
"Tried to create an Olm session for {} {}, but
the requested key isn't a signed curve key",
"Tried to create an Olm session for {} {}, but \
the device is unknown",
user_id, device_id
);
continue;
}
}
} else {
warn!(
"Tried to create an Olm session for {} {}, but the
signed one-time key is missing",
user_id, device_id
);
continue;
};
if let Err(e) = device.verify_one_time_key(&one_time_key) {
warn!(
"Failed to verify the one-time key signatures for {} {}: {:?}",
user_id, device_id, e
);
continue;
}
let curve_key = if let Some(k) = device.get_key(KeyAlgorithm::Curve25519) {
k
} else {
warn!(
"Tried to create an Olm session for {} {}, but the
device is missing the curve key",
user_id, device_id
);
continue;
Err(e) => {
warn!(
"Tried to create an Olm session for {} {}, but \
can't fetch the device from the store {:?}",
user_id, device_id, e
);
continue;
}
};
info!("Creating outbound Session for {} {}", user_id, device_id);
let session = match self
.account
.create_outbound_session(curve_key, &one_time_key)
.await
{
let session = match self.account.create_outbound_session(device, &key_map).await {
Ok(s) => s,
Err(e) => {
warn!(
"Error creating new Olm session for {} {}: {}",
user_id, device_id, e
);
warn!("{:?}", e);
continue;
}
};
@ -396,7 +350,7 @@ impl OlmMachine {
async fn handle_devices_from_key_query(
&mut self,
device_keys_map: &BTreeMap<UserId, BTreeMap<DeviceId, DeviceKeys>>,
device_keys_map: &BTreeMap<UserId, BTreeMap<Box<DeviceId>, DeviceKeys>>,
) -> StoreResult<Vec<Device>> {
let mut changed_devices = Vec::new();
@ -446,7 +400,8 @@ impl OlmMachine {
changed_devices.push(device);
}
let current_devices: HashSet<&DeviceId> = device_map.keys().collect();
let current_devices: HashSet<&DeviceId> =
device_map.keys().map(|id| id.as_ref()).collect();
let stored_devices = self.store.get_user_devices(&user_id).await.unwrap();
let stored_devices_set: HashSet<&DeviceId> = stored_devices.keys().collect();
@ -840,66 +795,51 @@ impl OlmMachine {
Ok(session.encrypt(content).await)
}
/// Encrypt some JSON content using the given Olm session.
/// Encrypt the given event for the given Device
///
/// # Arguments
///
/// * `reciepient_device` - The device that the event should be encrypted
/// for.
///
/// * `event_type` - The type of the event.
///
/// * `content` - The content of the event that should be encrypted.
async fn olm_encrypt(
&mut self,
mut session: Session,
recipient_device: &Device,
event_type: EventType,
content: Value,
) -> OlmResult<EncryptedEventContent> {
let identity_keys = self.account.identity_keys();
// TODO most of this could go into the session, the session already
// stores the curve key of the device, if we also store the ed25519 key
// with the session we'll only need to pass in the account to the
// session and all of this can live in the session.
let recipient_signing_key = recipient_device
.get_key(KeyAlgorithm::Ed25519)
.ok_or(EventError::MissingSigningKey)?;
let recipient_sender_key = recipient_device
.get_key(KeyAlgorithm::Curve25519)
.ok_or(EventError::MissingSigningKey)?;
let payload = json!({
"sender": self.user_id,
"sender_device": self.device_id,
"keys": {
"ed25519": identity_keys.ed25519(),
},
"recipient": recipient_device.user_id(),
"recipient_keys": {
"ed25519": recipient_signing_key,
},
"type": event_type,
"content": content,
});
let plaintext = cjson::to_string(&payload)
.unwrap_or_else(|_| panic!(format!("Can't serialize {} to canonical JSON", payload)));
let ciphertext = session.encrypt(&plaintext).await.to_tuple();
let message_type: usize = ciphertext.0.into();
let ciphertext = CiphertextInfo {
body: ciphertext.1,
message_type: (message_type as u32).into(),
let sender_key = if let Some(k) = recipient_device.get_key(KeyAlgorithm::Curve25519) {
k
} else {
warn!(
"Trying to encrypt a Megolm session for user {} on device {}, \
but the device doesn't have a curve25519 key",
recipient_device.user_id(),
recipient_device.device_id()
);
return Err(EventError::MissingSenderKey.into());
};
let mut content = BTreeMap::new();
content.insert(recipient_sender_key.to_owned(), ciphertext);
let mut session = if let Some(s) = self.store.get_sessions(sender_key).await? {
let session = &s.lock().await[0];
session.clone()
} else {
warn!(
"Trying to encrypt a Megolm session for user {} on device {}, \
but no Olm session is found",
recipient_device.user_id(),
recipient_device.device_id()
);
return Err(OlmError::MissingSession);
};
let message = session.encrypt(recipient_device, event_type, content).await;
self.store.save_sessions(&[session]).await?;
Ok(EncryptedEventContent::OlmV1Curve25519AesSha2(
OlmV1Curve25519AesSha2Content {
sender_key: identity_keys.curve25519().to_owned(),
ciphertext: content,
},
))
message
}
/// Should the client share a group session for the given room.
@ -946,102 +886,67 @@ impl OlmMachine {
I: IntoIterator<Item = &'a UserId>,
{
self.create_outbound_group_session(room_id).await?;
let megolm_session = self.outbound_group_sessions.get(room_id).unwrap();
let session = self.outbound_group_sessions.get(room_id).unwrap();
if megolm_session.shared() {
if session.shared() {
panic!("Session is already shared");
}
let session_id = megolm_session.session_id().to_owned();
// TODO don't mark the session as shared automatically only, when all
// the requests are done, failure to send these requests will likely end
// up in wedged sessions. We'll need to store the requests and let the
// caller mark them as sent using an UUID.
megolm_session.mark_as_shared();
session.mark_as_shared();
// TODO the key content creation can go into the OutboundGroupSession
// struct.
let key_content = json!({
"algorithm": Algorithm::MegolmV1AesSha2,
"room_id": room_id,
"session_id": session_id.clone(),
"session_key": megolm_session.session_key().await,
"chain_index": megolm_session.message_index().await,
});
let mut user_map = Vec::new();
let mut devices = Vec::new();
for user_id in users {
for device in self.store.get_user_devices(user_id).await?.devices() {
let sender_key = if let Some(k) = device.get_key(KeyAlgorithm::Curve25519) {
k
} else {
warn!(
"The device {} of user {} doesn't have a curve 25519 key",
user_id,
device.device_id()
);
// TODO mark the user for a key query.
continue;
};
// TODO abort if the device isn't verified
let sessions = self.store.get_sessions(sender_key).await?;
if let Some(s) = sessions {
let session = &s.lock().await[0];
// TODO once the session has the all the device info, we
// won't need the device anymore to encrypt stuff with the
// session.
user_map.push((session.clone(), device.clone()));
} else {
warn!(
"Trying to encrypt a Megolm session for user
{} on device {}, but no Olm session is found",
user_id,
device.device_id()
);
}
devices.push(device.clone());
}
}
let mut message_vec = Vec::new();
let mut requests = Vec::new();
let key_content = session.as_json().await;
for user_map_chunk in user_map.chunks(OlmMachine::MAX_TO_DEVICE_MESSAGES) {
for device_map_chunk in devices.chunks(OlmMachine::MAX_TO_DEVICE_MESSAGES) {
let mut messages = BTreeMap::new();
for (session, device) in user_map_chunk {
for device in device_map_chunk {
let encrypted = self
.olm_encrypt(&device, EventType::RoomKey, key_content.clone())
.await;
let encrypted = match encrypted {
Ok(c) => c,
Err(OlmError::MissingSession)
| Err(OlmError::EventError(EventError::MissingSenderKey)) => {
continue;
}
Err(e) => return Err(e),
};
if !messages.contains_key(device.user_id()) {
messages.insert(device.user_id().clone(), BTreeMap::new());
};
let user_messages = messages.get_mut(device.user_id()).unwrap();
let encrypted_content = self
.olm_encrypt(
session.clone(),
&device,
EventType::RoomKey,
key_content.clone(),
)
.await?;
user_messages.insert(
DeviceIdOrAllDevices::DeviceId(device.device_id().clone()),
serde_json::value::to_raw_value(&encrypted_content)?,
DeviceIdOrAllDevices::DeviceId(device.device_id().into()),
serde_json::value::to_raw_value(&encrypted)?,
);
}
message_vec.push(ToDeviceRequest {
requests.push(ToDeviceRequest {
event_type: EventType::RoomEncrypted,
txn_id: Uuid::new_v4().to_string(),
messages,
});
}
Ok(message_vec)
Ok(requests)
}
fn add_forwarded_room_key(
@ -1150,8 +1055,6 @@ impl OlmMachine {
}
};
// TODO make sure private keys are cleared from the event
// before we replace the result.
*event_result = decrypted_event;
}
AnyToDeviceEvent::RoomKeyRequest(e) => self.handle_room_key_request(e),
@ -1177,9 +1080,9 @@ impl OlmMachine {
/// * `room_id` - The ID of the room where the event was sent to.
pub async fn decrypt_room_event(
&mut self,
event: &MessageEventStub<EncryptedEventContent>,
event: &SyncMessageEvent<EncryptedEventContent>,
room_id: &RoomId,
) -> MegolmResult<EventJson<AnyRoomEventStub>> {
) -> MegolmResult<EventJson<AnySyncRoomEvent>> {
let content = match &event.content {
EncryptedEventContent::MegolmV1AesSha2(c) => c,
_ => return Err(EventError::UnsupportedAlgorithm.into()),
@ -1286,8 +1189,8 @@ mod test {
encrypted::EncryptedEventContent,
message::{MessageEventContent, TextMessageEventContent},
},
AnyMessageEventStub, AnyRoomEventStub, AnyToDeviceEvent, EventJson, EventType,
MessageEventStub, ToDeviceEvent, UnsignedData,
AnySyncMessageEvent, AnySyncRoomEvent, AnyToDeviceEvent, EventJson, EventType,
SyncMessageEvent, ToDeviceEvent, Unsigned,
};
use matrix_sdk_common::identifiers::{DeviceId, EventId, RoomId, UserId};
use matrix_sdk_test::test_json;
@ -1296,8 +1199,8 @@ mod test {
UserId::try_from("@alice:example.org").unwrap()
}
fn alice_device_id() -> DeviceId {
"JLAFKJWSCS".to_string()
fn alice_device_id() -> Box<DeviceId> {
"JLAFKJWSCS".into()
}
fn user_id() -> UserId {
@ -1375,8 +1278,8 @@ mod test {
let alice_device = alice_device_id();
let alice = OlmMachine::new(&alice_id, &alice_device);
let alice_deivce = Device::from(&alice);
let bob_device = Device::from(&bob);
let alice_deivce = Device::from_machine(&alice).await;
let bob_device = Device::from_machine(&bob).await;
alice.store.save_devices(&[bob_device]).await.unwrap();
bob.store.save_devices(&[alice_deivce]).await.unwrap();
@ -1409,16 +1312,6 @@ mod test {
async fn get_machine_pair_with_setup_sessions() -> (OlmMachine, OlmMachine) {
let (mut alice, mut bob) = get_machine_pair_with_session().await;
let session = alice
.store
.get_sessions(bob.account.identity_keys().curve25519())
.await
.unwrap()
.unwrap()
.lock()
.await[0]
.clone();
let bob_device = alice
.store
.get_device(&bob.user_id, &bob.device_id)
@ -1429,7 +1322,7 @@ mod test {
let event = ToDeviceEvent {
sender: alice.user_id.clone(),
content: alice
.olm_encrypt(session, &bob_device, EventType::Dummy, json!({}))
.olm_encrypt(&bob_device, EventType::Dummy, json!({}))
.await
.unwrap(),
};
@ -1698,16 +1591,6 @@ mod test {
async fn test_olm_encryption() {
let (mut alice, mut bob) = get_machine_pair_with_session().await;
let session = alice
.store
.get_sessions(bob.account.identity_keys().curve25519())
.await
.unwrap()
.unwrap()
.lock()
.await[0]
.clone();
let bob_device = alice
.store
.get_device(&bob.user_id, &bob.device_id)
@ -1718,7 +1601,7 @@ mod test {
let event = ToDeviceEvent {
sender: alice.user_id.clone(),
content: alice
.olm_encrypt(session, &bob_device, EventType::Dummy, json!({}))
.olm_encrypt(&bob_device, EventType::Dummy, json!({}))
.await
.unwrap(),
};
@ -1804,12 +1687,12 @@ mod test {
let encrypted_content = alice.encrypt(&room_id, content.clone()).await.unwrap();
let event = MessageEventStub {
let event = SyncMessageEvent {
event_id: EventId::try_from("$xxxxx:example.org").unwrap(),
origin_server_ts: SystemTime::now(),
sender: alice.user_id().clone(),
content: encrypted_content,
unsigned: UnsignedData::default(),
unsigned: Unsigned::default(),
};
let decrypted_event = bob
@ -1820,7 +1703,7 @@ mod test {
.unwrap();
match decrypted_event {
AnyRoomEventStub::Message(AnyMessageEventStub::RoomMessage(MessageEventStub {
AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(SyncMessageEvent {
sender,
content,
..

View file

@ -129,24 +129,24 @@ impl GroupSessionStore {
/// In-memory store holding the devices of users.
#[derive(Clone, Debug, Default)]
pub struct DeviceStore {
entries: Arc<DashMap<UserId, DashMap<String, Device>>>,
entries: Arc<DashMap<UserId, DashMap<Box<DeviceId>, Device>>>,
}
/// A read only view over all devices belonging to a user.
#[derive(Debug)]
pub struct UserDevices {
entries: ReadOnlyView<DeviceId, Device>,
entries: ReadOnlyView<Box<DeviceId>, Device>,
}
impl UserDevices {
/// Get the specific device with the given device id.
pub fn get(&self, device_id: &str) -> Option<Device> {
pub fn get(&self, device_id: &DeviceId) -> Option<Device> {
self.entries.get(device_id).cloned()
}
/// Iterator over all the device ids of the user devices.
pub fn keys(&self) -> impl Iterator<Item = &DeviceId> {
self.entries.keys()
self.entries.keys().map(|id| id.as_ref())
}
/// Iterator over all the devices of the user devices.
@ -175,12 +175,12 @@ impl DeviceStore {
let device_map = self.entries.get_mut(&user_id).unwrap();
device_map
.insert(device.device_id().to_owned(), device)
.insert(device.device_id().into(), device)
.is_none()
}
/// Get the device with the given device_id and belonging to the given user.
pub fn get(&self, user_id: &UserId, device_id: &str) -> Option<Device> {
pub fn get(&self, user_id: &UserId, device_id: &DeviceId) -> Option<Device> {
self.entries
.get(user_id)
.and_then(|m| m.get(device_id).map(|d| d.value().clone()))
@ -189,7 +189,7 @@ impl DeviceStore {
/// Remove the device with the given device_id and belonging to the given user.
///
/// Returns the device if it was removed, None if it wasn't in the store.
pub fn remove(&self, user_id: &UserId, device_id: &str) -> Option<Device> {
pub fn remove(&self, user_id: &UserId, device_id: &DeviceId) -> Option<Device> {
self.entries
.get(user_id)
.and_then(|m| m.remove(device_id))
@ -292,8 +292,8 @@ mod test {
let user_devices = store.user_devices(device.user_id());
assert_eq!(user_devices.keys().nth(0).unwrap(), device.device_id());
assert_eq!(user_devices.devices().nth(0).unwrap(), &device);
assert_eq!(user_devices.keys().next().unwrap(), device.device_id());
assert_eq!(user_devices.devices().next().unwrap(), &device);
let loaded_device = user_devices.get(device.device_id()).unwrap();

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,550 @@
// Copyright 2020 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.
use matrix_sdk_common::instant::Instant;
use std::convert::TryFrom;
use std::convert::TryInto;
use std::fmt;
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::sync::Arc;
use matrix_sdk_common::locks::Mutex;
use serde_json::{json, Value};
use std::collections::BTreeMap;
pub use olm_rs::account::IdentityKeys;
use olm_rs::account::{OlmAccount, OneTimeKeys};
use olm_rs::errors::{OlmAccountError, OlmSessionError};
use olm_rs::PicklingMode;
use crate::device::Device;
use crate::error::SessionCreationError;
pub use olm_rs::{
session::{OlmMessage, PreKeyMessage},
utility::OlmUtility,
};
use matrix_sdk_common::{
api::r0::keys::{AlgorithmAndDeviceId, DeviceKeys, KeyAlgorithm, OneTimeKey, SignedKey},
events::Algorithm,
identifiers::{DeviceId, RoomId, UserId},
};
use super::{InboundGroupSession, OutboundGroupSession, Session};
/// Account holding identity keys for which sessions can be created.
///
/// An account is the central identity for encrypted communication between two
/// devices.
#[derive(Clone)]
pub struct Account {
pub(crate) user_id: Arc<UserId>,
pub(crate) device_id: Arc<Box<DeviceId>>,
inner: Arc<Mutex<OlmAccount>>,
pub(crate) identity_keys: Arc<IdentityKeys>,
shared: Arc<AtomicBool>,
/// The number of signed one-time keys we have uploaded to the server. If
/// this is None, no action will be taken. After a sync request the client
/// needs to set this for us, depending on the count we will suggest the
/// client to upload new keys.
uploaded_signed_key_count: Arc<AtomicI64>,
}
// #[cfg_attr(tarpaulin, skip)]
impl fmt::Debug for Account {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Account")
.field("identity_keys", self.identity_keys())
.field("shared", &self.shared())
.finish()
}
}
impl Account {
const ALGORITHMS: &'static [&'static Algorithm] = &[
&Algorithm::OlmV1Curve25519AesSha2,
&Algorithm::MegolmV1AesSha2,
];
/// Create a fresh new account, this will generate the identity key-pair.
#[allow(clippy::ptr_arg)]
pub fn new(user_id: &UserId, device_id: &DeviceId) -> Self {
let account = OlmAccount::new();
let identity_keys = account.parsed_identity_keys();
Account {
user_id: Arc::new(user_id.to_owned()),
device_id: Arc::new(device_id.into()),
inner: Arc::new(Mutex::new(account)),
identity_keys: Arc::new(identity_keys),
shared: Arc::new(AtomicBool::new(false)),
uploaded_signed_key_count: Arc::new(AtomicI64::new(0)),
}
}
/// Get the public parts of the identity keys for the account.
pub fn identity_keys(&self) -> &IdentityKeys {
&self.identity_keys
}
/// Update the uploaded key count.
///
/// # Arguments
///
/// * `new_count` - The new count that was reported by the server.
pub(crate) fn update_uploaded_key_count(&self, new_count: u64) {
let key_count = i64::try_from(new_count).unwrap_or(i64::MAX);
self.uploaded_signed_key_count
.store(key_count, Ordering::Relaxed);
}
/// Get the currently known uploaded key count.
pub fn uploaded_key_count(&self) -> i64 {
self.uploaded_signed_key_count.load(Ordering::Relaxed)
}
/// Has the account been shared with the server.
pub fn shared(&self) -> bool {
self.shared.load(Ordering::Relaxed)
}
/// Mark the account as shared.
///
/// Messages shouldn't be encrypted with the session before it has been
/// shared.
pub(crate) fn mark_as_shared(&self) {
self.shared.store(true, Ordering::Relaxed);
}
/// Get the one-time keys of the account.
///
/// This can be empty, keys need to be generated first.
pub(crate) async fn one_time_keys(&self) -> OneTimeKeys {
self.inner.lock().await.parsed_one_time_keys()
}
/// Generate count number of one-time keys.
pub(crate) async fn generate_one_time_keys_helper(&self, count: usize) {
self.inner.lock().await.generate_one_time_keys(count);
}
/// Get the maximum number of one-time keys the account can hold.
pub(crate) async fn max_one_time_keys(&self) -> usize {
self.inner.lock().await.max_number_of_one_time_keys()
}
/// Get a tuple of device and one-time keys that need to be uploaded.
///
/// Returns an empty error if no keys need to be uploaded.
pub(crate) async fn generate_one_time_keys(&self) -> Result<u64, ()> {
let count = self.uploaded_key_count() as u64;
let max_keys = self.max_one_time_keys().await;
let max_on_server = (max_keys as u64) / 2;
if count >= (max_on_server) {
return Err(());
}
let key_count = (max_on_server) - count;
let key_count: usize = key_count.try_into().unwrap_or(max_keys);
self.generate_one_time_keys_helper(key_count).await;
Ok(key_count as u64)
}
/// Should account or one-time keys be uploaded to the server.
pub(crate) async fn should_upload_keys(&self) -> bool {
if !self.shared() {
return true;
}
let count = self.uploaded_key_count() as u64;
// If we have a known key count, check that we have more than
// max_one_time_Keys() / 2, otherwise tell the client to upload more.
let max_keys = self.max_one_time_keys().await as u64;
// If there are more keys already uploaded than max_key / 2
// bail out returning false, this also avoids overflow.
if count > (max_keys / 2) {
return false;
}
let key_count = (max_keys / 2) - count;
key_count > 0
}
/// Get a tuple of device and one-time keys that need to be uploaded.
///
/// Returns an empty error if no keys need to be uploaded.
pub(crate) async fn keys_for_upload(
&self,
) -> Result<
(
Option<DeviceKeys>,
Option<BTreeMap<AlgorithmAndDeviceId, OneTimeKey>>,
),
(),
> {
if !self.should_upload_keys().await {
return Err(());
}
let device_keys = if !self.shared() {
Some(self.device_keys().await)
} else {
None
};
let one_time_keys = self.signed_one_time_keys().await.ok();
Ok((device_keys, one_time_keys))
}
/// Mark the current set of one-time keys as being published.
pub(crate) async fn mark_keys_as_published(&self) {
self.inner.lock().await.mark_keys_as_published();
}
/// Sign the given string using the accounts signing key.
///
/// Returns the signature as a base64 encoded string.
pub async fn sign(&self, string: &str) -> String {
self.inner.lock().await.sign(string)
}
/// Store the account as a base64 encoded string.
///
/// # Arguments
///
/// * `pickle_mode` - The mode that was used to pickle the account, either an
/// unencrypted mode or an encrypted using passphrase.
pub async fn pickle(&self, pickle_mode: PicklingMode) -> String {
self.inner.lock().await.pickle(pickle_mode)
}
/// Restore an account from a previously pickled string.
///
/// # Arguments
///
/// * `pickle` - The pickled string of the account.
///
/// * `pickle_mode` - The mode that was used to pickle the account, either an
/// unencrypted mode or an encrypted using passphrase.
///
/// * `shared` - Boolean determining if the account was uploaded to the
/// server.
#[allow(clippy::ptr_arg)]
pub fn from_pickle(
pickle: String,
pickle_mode: PicklingMode,
shared: bool,
uploaded_signed_key_count: i64,
user_id: &UserId,
device_id: &DeviceId,
) -> Result<Self, OlmAccountError> {
let account = OlmAccount::unpickle(pickle, pickle_mode)?;
let identity_keys = account.parsed_identity_keys();
Ok(Account {
user_id: Arc::new(user_id.to_owned()),
device_id: Arc::new(device_id.into()),
inner: Arc::new(Mutex::new(account)),
identity_keys: Arc::new(identity_keys),
shared: Arc::new(AtomicBool::from(shared)),
uploaded_signed_key_count: Arc::new(AtomicI64::new(uploaded_signed_key_count)),
})
}
/// Sign the device keys of the account and return them so they can be
/// uploaded.
pub(crate) async fn device_keys(&self) -> DeviceKeys {
let identity_keys = self.identity_keys();
let mut keys = BTreeMap::new();
keys.insert(
AlgorithmAndDeviceId(KeyAlgorithm::Curve25519, (*self.device_id).clone()),
identity_keys.curve25519().to_owned(),
);
keys.insert(
AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, (*self.device_id).clone()),
identity_keys.ed25519().to_owned(),
);
let device_keys = json!({
"user_id": (*self.user_id).clone(),
"device_id": (*self.device_id).clone(),
"algorithms": Account::ALGORITHMS,
"keys": keys,
});
let mut signatures = BTreeMap::new();
let mut signature = BTreeMap::new();
signature.insert(
AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, (*self.device_id).clone()),
self.sign_json(&device_keys).await,
);
signatures.insert((*self.user_id).clone(), signature);
DeviceKeys {
user_id: (*self.user_id).clone(),
device_id: (*self.device_id).clone(),
algorithms: vec![
Algorithm::OlmV1Curve25519AesSha2,
Algorithm::MegolmV1AesSha2,
],
keys,
signatures,
unsigned: None,
}
}
/// Convert a JSON value to the canonical representation and sign the JSON
/// string.
///
/// # Arguments
///
/// * `json` - The value that should be converted into a canonical JSON
/// string.
///
/// # Panic
///
/// Panics if the json value can't be serialized.
pub async fn sign_json(&self, json: &Value) -> String {
let canonical_json = cjson::to_string(json)
.unwrap_or_else(|_| panic!(format!("Can't serialize {} to canonical JSON", json)));
self.sign(&canonical_json).await
}
/// Generate, sign and prepare one-time keys to be uploaded.
///
/// If no one-time keys need to be uploaded returns an empty error.
pub(crate) async fn signed_one_time_keys(
&self,
) -> Result<BTreeMap<AlgorithmAndDeviceId, OneTimeKey>, ()> {
let _ = self.generate_one_time_keys().await?;
let one_time_keys = self.one_time_keys().await;
let mut one_time_key_map = BTreeMap::new();
for (key_id, key) in one_time_keys.curve25519().iter() {
let key_json = json!({
"key": key,
});
let signature = self.sign_json(&key_json).await;
let mut signature_map = BTreeMap::new();
signature_map.insert(
AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, (*self.device_id).clone()),
signature,
);
let mut signatures = BTreeMap::new();
signatures.insert((*self.user_id).clone(), signature_map);
let signed_key = SignedKey {
key: key.to_owned(),
signatures,
};
one_time_key_map.insert(
AlgorithmAndDeviceId(KeyAlgorithm::SignedCurve25519, key_id.as_str().into()),
OneTimeKey::SignedKey(signed_key),
);
}
Ok(one_time_key_map)
}
/// Create a new session with another account given a one-time key.
///
/// Returns the newly created session or a `OlmSessionError` if creating a
/// session failed.
///
/// # Arguments
/// * `their_identity_key` - The other account's identity/curve25519 key.
///
/// * `their_one_time_key` - A signed one-time key that the other account
/// created and shared with us.
pub(crate) async fn create_outbound_session_helper(
&self,
their_identity_key: &str,
their_one_time_key: &SignedKey,
) -> Result<Session, OlmSessionError> {
let session = self
.inner
.lock()
.await
.create_outbound_session(their_identity_key, &their_one_time_key.key)?;
let now = Instant::now();
let session_id = session.session_id();
Ok(Session {
user_id: self.user_id.clone(),
device_id: self.device_id.clone(),
our_identity_keys: self.identity_keys.clone(),
inner: Arc::new(Mutex::new(session)),
session_id: Arc::new(session_id),
sender_key: Arc::new(their_identity_key.to_owned()),
creation_time: Arc::new(now),
last_use_time: Arc::new(now),
})
}
/// Create a new session with another account given a one-time key and a
/// device.
///
/// Returns the newly created session or a `OlmSessionError` if creating a
/// session failed.
///
/// # Arguments
/// * `device` - The other account's device.
///
/// * `key_map` - A map from the algorithm and device id to the one-time
/// key that the other account created and shared with us.
pub(crate) async fn create_outbound_session(
&self,
device: Device,
key_map: &BTreeMap<AlgorithmAndDeviceId, OneTimeKey>,
) -> Result<Session, SessionCreationError> {
let one_time_key = key_map.values().next().ok_or_else(|| {
SessionCreationError::OneTimeKeyMissing(
device.user_id().to_owned(),
device.device_id().into(),
)
})?;
let one_time_key = match one_time_key {
OneTimeKey::SignedKey(k) => k,
OneTimeKey::Key(_) => {
return Err(SessionCreationError::OneTimeKeyNotSigned(
device.user_id().to_owned(),
device.device_id().into(),
));
}
};
device.verify_one_time_key(&one_time_key).map_err(|e| {
SessionCreationError::InvalidSignature(
device.user_id().to_owned(),
device.device_id().into(),
e,
)
})?;
let curve_key = device.get_key(KeyAlgorithm::Curve25519).ok_or_else(|| {
SessionCreationError::DeviceMissingCurveKey(
device.user_id().to_owned(),
device.device_id().into(),
)
})?;
self.create_outbound_session_helper(curve_key, &one_time_key)
.await
.map_err(|e| {
SessionCreationError::OlmError(
device.user_id().to_owned(),
device.device_id().into(),
e,
)
})
}
/// Create a new session with another account given a pre-key Olm message.
///
/// Returns the newly created session or a `OlmSessionError` if creating a
/// session failed.
///
/// # Arguments
/// * `their_identity_key` - The other account's identitiy/curve25519 key.
///
/// * `message` - A pre-key Olm message that was sent to us by the other
/// account.
pub(crate) async fn create_inbound_session(
&self,
their_identity_key: &str,
message: PreKeyMessage,
) -> Result<Session, OlmSessionError> {
let session = self
.inner
.lock()
.await
.create_inbound_session_from(their_identity_key, message)?;
self.inner
.lock()
.await
.remove_one_time_keys(&session)
.expect(
"Session was successfully created but the account doesn't hold a matching one-time key",
);
let now = Instant::now();
let session_id = session.session_id();
Ok(Session {
user_id: self.user_id.clone(),
device_id: self.device_id.clone(),
our_identity_keys: self.identity_keys.clone(),
inner: Arc::new(Mutex::new(session)),
session_id: Arc::new(session_id),
sender_key: Arc::new(their_identity_key.to_owned()),
creation_time: Arc::new(now),
last_use_time: Arc::new(now),
})
}
/// Create a group session pair.
///
/// This session pair can be used to encrypt and decrypt messages meant for
/// a large group of participants.
///
/// The outbound session is used to encrypt messages while the inbound one
/// is used to decrypt messages encrypted by the outbound one.
///
/// # Arguments
///
/// * `room_id` - The ID of the room where the group session will be used.
pub(crate) async fn create_group_session_pair(
&self,
room_id: &RoomId,
) -> (OutboundGroupSession, InboundGroupSession) {
let outbound =
OutboundGroupSession::new(self.device_id.clone(), self.identity_keys.clone(), room_id);
let identity_keys = self.identity_keys();
let sender_key = identity_keys.curve25519();
let signing_key = identity_keys.ed25519();
let inbound = InboundGroupSession::new(
sender_key,
signing_key,
&room_id,
outbound.session_key().await,
)
.expect("Can't create inbound group session from a newly created outbound group session");
(outbound, inbound)
}
}
impl PartialEq for Account {
fn eq(&self, other: &Self) -> bool {
self.identity_keys() == other.identity_keys() && self.shared() == other.shared()
}
}

View file

@ -0,0 +1,412 @@
// Copyright 2020 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.
use matrix_sdk_common::instant::Instant;
use std::convert::TryInto;
use std::fmt;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use matrix_sdk_common::locks::Mutex;
use serde::Serialize;
use serde_json::{json, Value};
use zeroize::Zeroize;
pub use olm_rs::account::IdentityKeys;
use olm_rs::errors::OlmGroupSessionError;
use olm_rs::inbound_group_session::OlmInboundGroupSession;
use olm_rs::outbound_group_session::OlmOutboundGroupSession;
use olm_rs::PicklingMode;
use crate::error::{EventError, MegolmResult};
pub use olm_rs::{
session::{OlmMessage, PreKeyMessage},
utility::OlmUtility,
};
use matrix_sdk_common::{
events::{
room::{
encrypted::{EncryptedEventContent, MegolmV1AesSha2Content},
message::MessageEventContent,
},
Algorithm, AnySyncRoomEvent, EventJson, EventType, SyncMessageEvent,
},
identifiers::{DeviceId, RoomId},
};
/// The private session key of a group session.
/// Can be used to create a new inbound group session.
#[derive(Clone, Debug, Serialize, Zeroize)]
#[zeroize(drop)]
pub struct GroupSessionKey(pub String);
/// Inbound group session.
///
/// Inbound group sessions are used to exchange room messages between a group of
/// participants. Inbound group sessions are used to decrypt the room messages.
#[derive(Clone)]
pub struct InboundGroupSession {
inner: Arc<Mutex<OlmInboundGroupSession>>,
session_id: Arc<String>,
pub(crate) sender_key: Arc<String>,
pub(crate) signing_key: Arc<String>,
pub(crate) room_id: Arc<RoomId>,
forwarding_chains: Arc<Mutex<Option<Vec<String>>>>,
}
impl InboundGroupSession {
/// Create a new inbound group session for the given room.
///
/// These sessions are used to decrypt room messages.
///
/// # Arguments
///
/// * `sender_key` - The public curve25519 key of the account that
/// sent us the session
///
/// * `signing_key` - The public ed25519 key of the account that
/// sent us the session.
///
/// * `room_id` - The id of the room that the session is used in.
///
/// * `session_key` - The private session key that is used to decrypt
/// messages.
pub fn new(
sender_key: &str,
signing_key: &str,
room_id: &RoomId,
session_key: GroupSessionKey,
) -> Result<Self, OlmGroupSessionError> {
let session = OlmInboundGroupSession::new(&session_key.0)?;
let session_id = session.session_id();
Ok(InboundGroupSession {
inner: Arc::new(Mutex::new(session)),
session_id: Arc::new(session_id),
sender_key: Arc::new(sender_key.to_owned()),
signing_key: Arc::new(signing_key.to_owned()),
room_id: Arc::new(room_id.clone()),
forwarding_chains: Arc::new(Mutex::new(None)),
})
}
/// Store the group session as a base64 encoded string.
///
/// # Arguments
///
/// * `pickle_mode` - The mode that was used to pickle the group session,
/// either an unencrypted mode or an encrypted using passphrase.
pub async fn pickle(&self, pickle_mode: PicklingMode) -> String {
self.inner.lock().await.pickle(pickle_mode)
}
/// Restore a Session from a previously pickled string.
///
/// Returns the restored group session or a `OlmGroupSessionError` if there
/// was an error.
///
/// # Arguments
///
/// * `pickle` - The pickled string of the group session session.
///
/// * `pickle_mode` - The mode that was used to pickle the session, either
/// an unencrypted mode or an encrypted using passphrase.
///
/// * `sender_key` - The public curve25519 key of the account that
/// sent us the session
///
/// * `signing_key` - The public ed25519 key of the account that
/// sent us the session.
///
/// * `room_id` - The id of the room that the session is used in.
pub fn from_pickle(
pickle: String,
pickle_mode: PicklingMode,
sender_key: String,
signing_key: String,
room_id: RoomId,
) -> Result<Self, OlmGroupSessionError> {
let session = OlmInboundGroupSession::unpickle(pickle, pickle_mode)?;
let session_id = session.session_id();
Ok(InboundGroupSession {
inner: Arc::new(Mutex::new(session)),
session_id: Arc::new(session_id),
sender_key: Arc::new(sender_key),
signing_key: Arc::new(signing_key),
room_id: Arc::new(room_id),
forwarding_chains: Arc::new(Mutex::new(None)),
})
}
/// Returns the unique identifier for this session.
pub fn session_id(&self) -> &str {
&self.session_id
}
/// Get the first message index we know how to decrypt.
pub async fn first_known_index(&self) -> u32 {
self.inner.lock().await.first_known_index()
}
/// Decrypt the given ciphertext.
///
/// Returns the decrypted plaintext or an `OlmGroupSessionError` if
/// decryption failed.
///
/// # Arguments
///
/// * `message` - The message that should be decrypted.
pub async fn decrypt_helper(
&self,
message: String,
) -> Result<(String, u32), OlmGroupSessionError> {
self.inner.lock().await.decrypt(message)
}
/// Decrypt an event from a room timeline.
///
/// # Arguments
///
/// * `event` - The event that should be decrypted.
pub async fn decrypt(
&self,
event: &SyncMessageEvent<EncryptedEventContent>,
) -> MegolmResult<(EventJson<AnySyncRoomEvent>, u32)> {
let content = match &event.content {
EncryptedEventContent::MegolmV1AesSha2(c) => c,
_ => return Err(EventError::UnsupportedAlgorithm.into()),
};
let (plaintext, message_index) = self.decrypt_helper(content.ciphertext.clone()).await?;
let mut decrypted_value = serde_json::from_str::<Value>(&plaintext)?;
let decrypted_object = decrypted_value
.as_object_mut()
.ok_or(EventError::NotAnObject)?;
// TODO better number conversion here.
let server_ts = event
.origin_server_ts
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let server_ts: i64 = server_ts.try_into().unwrap_or_default();
decrypted_object.insert("sender".to_owned(), event.sender.to_string().into());
decrypted_object.insert("event_id".to_owned(), event.event_id.to_string().into());
decrypted_object.insert("origin_server_ts".to_owned(), server_ts.into());
decrypted_object.insert(
"unsigned".to_owned(),
serde_json::to_value(&event.unsigned).unwrap_or_default(),
);
Ok((
serde_json::from_value::<EventJson<AnySyncRoomEvent>>(decrypted_value)?,
message_index,
))
}
}
// #[cfg_attr(tarpaulin, skip)]
impl fmt::Debug for InboundGroupSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InboundGroupSession")
.field("session_id", &self.session_id())
.finish()
}
}
impl PartialEq for InboundGroupSession {
fn eq(&self, other: &Self) -> bool {
self.session_id() == other.session_id()
}
}
/// Outbound group session.
///
/// Outbound group sessions are used to exchange room messages between a group
/// of participants. Outbound group sessions are used to encrypt the room
/// messages.
#[derive(Clone)]
pub struct OutboundGroupSession {
inner: Arc<Mutex<OlmOutboundGroupSession>>,
device_id: Arc<Box<DeviceId>>,
account_identity_keys: Arc<IdentityKeys>,
session_id: Arc<String>,
room_id: Arc<RoomId>,
creation_time: Arc<Instant>,
message_count: Arc<AtomicUsize>,
shared: Arc<AtomicBool>,
}
impl OutboundGroupSession {
/// Create a new outbound group session for the given room.
///
/// Outbound group sessions are used to encrypt room messages.
///
/// # Arguments
///
/// * `device_id` - The id of the device that created this session.
///
/// * `identity_keys` - The identity keys of the account that created this
/// session.
///
/// * `room_id` - The id of the room that the session is used in.
pub fn new(
device_id: Arc<Box<DeviceId>>,
identity_keys: Arc<IdentityKeys>,
room_id: &RoomId,
) -> Self {
let session = OlmOutboundGroupSession::new();
let session_id = session.session_id();
OutboundGroupSession {
inner: Arc::new(Mutex::new(session)),
room_id: Arc::new(room_id.to_owned()),
device_id,
account_identity_keys: identity_keys,
session_id: Arc::new(session_id),
creation_time: Arc::new(Instant::now()),
message_count: Arc::new(AtomicUsize::new(0)),
shared: Arc::new(AtomicBool::new(false)),
}
}
/// Encrypt the given plaintext using this session.
///
/// Returns the encrypted ciphertext.
///
/// # Arguments
///
/// * `plaintext` - The plaintext that should be encrypted.
pub(crate) async fn encrypt_helper(&self, plaintext: String) -> String {
let session = self.inner.lock().await;
session.encrypt(plaintext)
}
/// Encrypt a room message for the given room.
///
/// Beware that a group session needs to be shared before this method can be
/// called using the `share_group_session()` method.
///
/// Since group sessions can expire or become invalid if the room membership
/// changes client authors should check with the
/// `should_share_group_session()` method if a new group session needs to
/// be shared.
///
/// # Arguments
///
/// * `content` - The plaintext content of the message that should be
/// encrypted.
///
/// # Panics
///
/// Panics if the content can't be serialized.
pub async fn encrypt(&self, content: MessageEventContent) -> EncryptedEventContent {
let json_content = json!({
"content": content,
"room_id": &*self.room_id,
"type": EventType::RoomMessage,
});
let plaintext = cjson::to_string(&json_content).unwrap_or_else(|_| {
panic!(format!(
"Can't serialize {} to canonical JSON",
json_content
))
});
let ciphertext = self.encrypt_helper(plaintext).await;
EncryptedEventContent::MegolmV1AesSha2(MegolmV1AesSha2Content::new(
matrix_sdk_common::events::room::encrypted::MegolmV1AesSha2ContentInit {
ciphertext,
sender_key: self.account_identity_keys.curve25519().to_owned(),
session_id: self.session_id().to_owned(),
device_id: (&*self.device_id).to_owned(),
},
))
}
/// Check if the session has expired and if it should be rotated.
///
/// A session will expire after some time or if enough messages have been
/// encrypted using it.
pub fn expired(&self) -> bool {
// TODO implement this.
false
}
/// Mark the session as shared.
///
/// Messages shouldn't be encrypted with the session before it has been
/// shared.
pub fn mark_as_shared(&self) {
self.shared.store(true, Ordering::Relaxed);
}
/// Check if the session has been marked as shared.
pub fn shared(&self) -> bool {
self.shared.load(Ordering::Relaxed)
}
/// Get the session key of this session.
///
/// A session key can be used to to create an `InboundGroupSession`.
pub async fn session_key(&self) -> GroupSessionKey {
let session = self.inner.lock().await;
GroupSessionKey(session.session_key())
}
/// Returns the unique identifier for this session.
pub fn session_id(&self) -> &str {
&self.session_id
}
/// Get the current message index for this session.
///
/// Each message is sent with an increasing index. This returns the
/// message index that will be used for the next encrypted message.
pub async fn message_index(&self) -> u32 {
let session = self.inner.lock().await;
session.session_message_index()
}
/// Get the outbound group session key as a json value that can be sent as a
/// m.room_key.
pub async fn as_json(&self) -> Value {
json!({
"algorithm": Algorithm::MegolmV1AesSha2,
"room_id": &*self.room_id,
"session_id": &*self.session_id,
"session_key": self.session_key().await,
"chain_index": self.message_index().await,
})
}
}
// #[cfg_attr(tarpaulin, skip)]
impl std::fmt::Debug for OutboundGroupSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OutboundGroupSession")
.field("session_id", &self.session_id)
.field("room_id", &self.room_id)
.field("creation_time", &self.creation_time)
.field("message_count", &self.message_count)
.finish()
}
}

View file

@ -0,0 +1,208 @@
// Copyright 2020 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.
mod account;
mod group_sessions;
mod session;
pub use account::{Account, IdentityKeys};
pub use group_sessions::{GroupSessionKey, InboundGroupSession, OutboundGroupSession};
pub use session::{OlmMessage, Session};
#[cfg(test)]
pub(crate) mod test {
use crate::olm::{Account, InboundGroupSession, Session};
use matrix_sdk_common::api::r0::keys::SignedKey;
use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId};
use olm_rs::session::OlmMessage;
use std::collections::BTreeMap;
use std::convert::TryFrom;
fn alice_id() -> UserId {
UserId::try_from("@alice:example.org").unwrap()
}
fn alice_device_id() -> Box<DeviceId> {
"ALICEDEVICE".into()
}
fn bob_id() -> UserId {
UserId::try_from("@bob:example.org").unwrap()
}
fn bob_device_id() -> Box<DeviceId> {
"BOBDEVICE".into()
}
pub(crate) async fn get_account_and_session() -> (Account, Session) {
let alice = Account::new(&alice_id(), &alice_device_id());
let bob = Account::new(&bob_id(), &bob_device_id());
bob.generate_one_time_keys_helper(1).await;
let one_time_key = bob
.one_time_keys()
.await
.curve25519()
.iter()
.next()
.unwrap()
.1
.to_owned();
let one_time_key = SignedKey {
key: one_time_key,
signatures: BTreeMap::new(),
};
let sender_key = bob.identity_keys().curve25519().to_owned();
let session = alice
.create_outbound_session_helper(&sender_key, &one_time_key)
.await
.unwrap();
(alice, session)
}
#[test]
fn account_creation() {
let account = Account::new(&alice_id(), &alice_device_id());
let identyty_keys = account.identity_keys();
assert!(!account.shared());
assert!(!identyty_keys.ed25519().is_empty());
assert_ne!(identyty_keys.values().len(), 0);
assert_ne!(identyty_keys.keys().len(), 0);
assert_ne!(identyty_keys.iter().len(), 0);
assert!(identyty_keys.contains_key("ed25519"));
assert_eq!(
identyty_keys.ed25519(),
identyty_keys.get("ed25519").unwrap()
);
assert!(!identyty_keys.curve25519().is_empty());
account.mark_as_shared();
assert!(account.shared());
}
#[tokio::test]
async fn one_time_keys_creation() {
let account = Account::new(&alice_id(), &alice_device_id());
let one_time_keys = account.one_time_keys().await;
assert!(one_time_keys.curve25519().is_empty());
assert_ne!(account.max_one_time_keys().await, 0);
account.generate_one_time_keys_helper(10).await;
let one_time_keys = account.one_time_keys().await;
assert!(!one_time_keys.curve25519().is_empty());
assert_ne!(one_time_keys.values().len(), 0);
assert_ne!(one_time_keys.keys().len(), 0);
assert_ne!(one_time_keys.iter().len(), 0);
assert!(one_time_keys.contains_key("curve25519"));
assert_eq!(one_time_keys.curve25519().keys().len(), 10);
assert_eq!(
one_time_keys.curve25519(),
one_time_keys.get("curve25519").unwrap()
);
account.mark_keys_as_published().await;
let one_time_keys = account.one_time_keys().await;
assert!(one_time_keys.curve25519().is_empty());
}
#[tokio::test]
async fn session_creation() {
let alice = Account::new(&alice_id(), &alice_device_id());
let bob = Account::new(&bob_id(), &bob_device_id());
let alice_keys = alice.identity_keys();
alice.generate_one_time_keys_helper(1).await;
let one_time_keys = alice.one_time_keys().await;
alice.mark_keys_as_published().await;
let one_time_key = one_time_keys
.curve25519()
.iter()
.next()
.unwrap()
.1
.to_owned();
let one_time_key = SignedKey {
key: one_time_key,
signatures: BTreeMap::new(),
};
let mut bob_session = bob
.create_outbound_session_helper(alice_keys.curve25519(), &one_time_key)
.await
.unwrap();
let plaintext = "Hello world";
let message = bob_session.encrypt_helper(plaintext).await;
let prekey_message = match message.clone() {
OlmMessage::PreKey(m) => m,
OlmMessage::Message(_) => panic!("Incorrect message type"),
};
let bob_keys = bob.identity_keys();
let mut alice_session = alice
.create_inbound_session(bob_keys.curve25519(), prekey_message.clone())
.await
.unwrap();
assert!(alice_session
.matches(bob_keys.curve25519(), prekey_message)
.await
.unwrap());
assert_eq!(bob_session.session_id(), alice_session.session_id());
let decyrpted = alice_session.decrypt(message).await.unwrap();
assert_eq!(plaintext, decyrpted);
}
#[tokio::test]
async fn group_session_creation() {
let alice = Account::new(&alice_id(), &alice_device_id());
let room_id = RoomId::try_from("!test:localhost").unwrap();
let (outbound, _) = alice.create_group_session_pair(&room_id).await;
assert_eq!(0, outbound.message_index().await);
assert!(!outbound.shared());
outbound.mark_as_shared();
assert!(outbound.shared());
let inbound = InboundGroupSession::new(
"test_key",
"test_key",
&room_id,
outbound.session_key().await,
)
.unwrap();
assert_eq!(0, inbound.first_known_index().await);
assert_eq!(outbound.session_id(), inbound.session_id());
let plaintext = "This is a secret to everybody".to_owned();
let ciphertext = outbound.encrypt_helper(plaintext.clone()).await;
assert_eq!(
plaintext,
inbound.decrypt_helper(ciphertext).await.unwrap().0
);
}
}

View file

@ -0,0 +1,246 @@
// Copyright 2020 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.
use std::collections::BTreeMap;
use std::fmt;
use std::sync::Arc;
use olm_rs::errors::OlmSessionError;
use olm_rs::session::OlmSession;
use olm_rs::PicklingMode;
use serde_json::{json, Value};
pub use olm_rs::{
session::{OlmMessage, PreKeyMessage},
utility::OlmUtility,
};
use super::IdentityKeys;
use crate::error::{EventError, OlmResult};
use crate::Device;
use matrix_sdk_common::{
api::r0::keys::KeyAlgorithm,
events::{
room::encrypted::{CiphertextInfo, EncryptedEventContent, OlmV1Curve25519AesSha2Content},
EventType,
},
identifiers::{DeviceId, UserId},
instant::Instant,
locks::Mutex,
};
/// Cryptographic session that enables secure communication between two
/// `Account`s
#[derive(Clone)]
pub struct Session {
pub(crate) user_id: Arc<UserId>,
pub(crate) device_id: Arc<Box<DeviceId>>,
pub(crate) our_identity_keys: Arc<IdentityKeys>,
pub(crate) inner: Arc<Mutex<OlmSession>>,
pub(crate) session_id: Arc<String>,
pub(crate) sender_key: Arc<String>,
pub(crate) creation_time: Arc<Instant>,
pub(crate) last_use_time: Arc<Instant>,
}
// #[cfg_attr(tarpaulin, skip)]
impl fmt::Debug for Session {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Session")
.field("session_id", &self.session_id())
.field("sender_key", &self.sender_key)
.finish()
}
}
impl Session {
/// Decrypt the given Olm message.
///
/// Returns the decrypted plaintext or an `OlmSessionError` if decryption
/// failed.
///
/// # Arguments
///
/// * `message` - The Olm message that should be decrypted.
pub async fn decrypt(&mut self, message: OlmMessage) -> Result<String, OlmSessionError> {
let plaintext = self.inner.lock().await.decrypt(message)?;
self.last_use_time = Arc::new(Instant::now());
Ok(plaintext)
}
/// Encrypt the given plaintext as a OlmMessage.
///
/// Returns the encrypted Olm message.
///
/// # Arguments
///
/// * `plaintext` - The plaintext that should be encrypted.
pub(crate) async fn encrypt_helper(&mut self, plaintext: &str) -> OlmMessage {
let message = self.inner.lock().await.encrypt(plaintext);
self.last_use_time = Arc::new(Instant::now());
message
}
/// Encrypt the given event content content as an m.room.encrypted event
/// content.
///
/// # Arguments
///
/// * `recipient_device` - The device for which this message is going to be
/// encrypted, this needs to be the device that was used to create this
/// session with.
///
/// * `event_type` - The type of the event.
///
/// * `content` - The content of the event.
pub async fn encrypt(
&mut self,
recipient_device: &Device,
event_type: EventType,
content: Value,
) -> OlmResult<EncryptedEventContent> {
let recipient_signing_key = recipient_device
.get_key(KeyAlgorithm::Ed25519)
.ok_or(EventError::MissingSigningKey)?;
let payload = json!({
"sender": self.user_id.as_str(),
"sender_device": self.device_id.as_ref(),
"keys": {
"ed25519": self.our_identity_keys.ed25519(),
},
"recipient": recipient_device.user_id(),
"recipient_keys": {
"ed25519": recipient_signing_key,
},
"type": event_type,
"content": content,
});
let plaintext = cjson::to_string(&payload)
.unwrap_or_else(|_| panic!(format!("Can't serialize {} to canonical JSON", payload)));
let ciphertext = self.encrypt_helper(&plaintext).await.to_tuple();
let message_type = ciphertext.0;
let ciphertext = CiphertextInfo::new(ciphertext.1, (message_type as u32).into());
let mut content = BTreeMap::new();
content.insert((&*self.sender_key).to_owned(), ciphertext);
Ok(EncryptedEventContent::OlmV1Curve25519AesSha2(
OlmV1Curve25519AesSha2Content::new(
content,
self.our_identity_keys.curve25519().to_string(),
),
))
}
/// Check if a pre-key Olm message was encrypted for this session.
///
/// Returns true if it matches, false if not and a OlmSessionError if there
/// was an error checking if it matches.
///
/// # Arguments
///
/// * `their_identity_key` - The identity/curve25519 key of the account
/// that encrypted this Olm message.
///
/// * `message` - The pre-key Olm message that should be checked.
pub async fn matches(
&self,
their_identity_key: &str,
message: PreKeyMessage,
) -> Result<bool, OlmSessionError> {
self.inner
.lock()
.await
.matches_inbound_session_from(their_identity_key, message)
}
/// Returns the unique identifier for this session.
pub fn session_id(&self) -> &str {
&self.session_id
}
/// Store the session as a base64 encoded string.
///
/// # Arguments
///
/// * `pickle_mode` - The mode that was used to pickle the session, either
/// an unencrypted mode or an encrypted using passphrase.
pub async fn pickle(&self, pickle_mode: PicklingMode) -> String {
self.inner.lock().await.pickle(pickle_mode)
}
/// Restore a Session from a previously pickled string.
///
/// Returns the restored Olm Session or a `OlmSessionError` if there was an
/// error.
///
/// # Arguments
///
/// * `user_id` - Our own user id that the session belongs to.
///
/// * `device_id` - Our own device id that the session belongs to.
///
/// * `our_idenity_keys` - An clone of the Arc to our own identity keys.
///
/// * `pickle` - The pickled string of the session.
///
/// * `pickle_mode` - The mode that was used to pickle the session, either
/// an unencrypted mode or an encrypted using passphrase.
///
/// * `sender_key` - The public curve25519 key of the account that
/// established the session with us.
///
/// * `creation_time` - The timestamp that marks when the session was
/// created.
///
/// * `last_use_time` - The timestamp that marks when the session was
/// last used to encrypt or decrypt an Olm message.
#[allow(clippy::too_many_arguments)]
pub fn from_pickle(
user_id: Arc<UserId>,
device_id: Arc<Box<DeviceId>>,
our_identity_keys: Arc<IdentityKeys>,
pickle: String,
pickle_mode: PicklingMode,
sender_key: String,
creation_time: Instant,
last_use_time: Instant,
) -> Result<Self, OlmSessionError> {
let session = OlmSession::unpickle(pickle, pickle_mode)?;
let session_id = session.session_id();
Ok(Session {
user_id,
device_id,
our_identity_keys,
inner: Arc::new(Mutex::new(session)),
session_id: Arc::new(session_id),
sender_key: Arc::new(sender_key),
creation_time: Arc::new(creation_time),
last_use_time: Arc::new(last_use_time),
})
}
}
impl PartialEq for Session {
fn eq(&self, other: &Self) -> bool {
self.session_id() == other.session_id()
}
}

View file

@ -200,8 +200,8 @@ mod test {
let user_devices = store.get_user_devices(device.user_id()).await.unwrap();
assert_eq!(user_devices.keys().nth(0).unwrap(), device.device_id());
assert_eq!(user_devices.devices().nth(0).unwrap(), &device);
assert_eq!(user_devices.keys().next().unwrap(), device.device_id());
assert_eq!(user_devices.devices().next().unwrap(), &device);
let loaded_device = user_devices.get(device.device_id()).unwrap();

View file

@ -87,7 +87,7 @@ pub enum CryptoStoreError {
pub type Result<T> = std::result::Result<T, CryptoStoreError>;
#[async_trait]
#[warn(clippy::type_complexity)]
#[allow(clippy::type_complexity)]
#[cfg_attr(not(target_arch = "wasm32"), send_sync)]
/// Trait abstracting a store that the `OlmMachine` uses to store cryptographic
/// keys.

View file

@ -26,9 +26,10 @@ use olm_rs::PicklingMode;
use sqlx::{query, query_as, sqlite::SqliteQueryAs, Connect, Executor, SqliteConnection};
use zeroize::Zeroizing;
use super::{Account, CryptoStore, CryptoStoreError, InboundGroupSession, Result, Session};
use super::{CryptoStore, CryptoStoreError, Result};
use crate::device::{Device, TrustState};
use crate::memory_stores::{DeviceStore, GroupSessionStore, SessionStore, UserDevices};
use crate::{Account, IdentityKeys, InboundGroupSession, Session};
use matrix_sdk_common::api::r0::keys::{AlgorithmAndDeviceId, KeyAlgorithm};
use matrix_sdk_common::events::Algorithm;
use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId};
@ -36,8 +37,8 @@ use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId};
/// SQLite based implementation of a `CryptoStore`.
pub struct SqliteStore {
user_id: Arc<UserId>,
device_id: Arc<String>,
account_id: Option<i64>,
device_id: Arc<Box<DeviceId>>,
account_info: Option<AccountInfo>,
path: PathBuf,
sessions: SessionStore,
@ -50,6 +51,11 @@ pub struct SqliteStore {
pickle_passphrase: Option<Zeroizing<String>>,
}
struct AccountInfo {
account_id: i64,
identity_keys: Arc<IdentityKeys>,
}
static DATABASE_NAME: &str = "matrix-sdk-crypto.db";
impl SqliteStore {
@ -66,7 +72,7 @@ impl SqliteStore {
/// * `path` - The path where the database file should reside in.
pub async fn open<P: AsRef<Path>>(
user_id: &UserId,
device_id: &str,
device_id: &DeviceId,
path: P,
) -> Result<SqliteStore> {
SqliteStore::open_helper(user_id, device_id, path, None).await
@ -88,7 +94,7 @@ impl SqliteStore {
/// the encryption keys.
pub async fn open_with_passphrase<P: AsRef<Path>>(
user_id: &UserId,
device_id: &str,
device_id: &DeviceId,
path: P,
passphrase: &str,
) -> Result<SqliteStore> {
@ -109,7 +115,7 @@ impl SqliteStore {
async fn open_helper<P: AsRef<Path>>(
user_id: &UserId,
device_id: &str,
device_id: &DeviceId,
path: P,
passphrase: Option<Zeroizing<String>>,
) -> Result<SqliteStore> {
@ -118,8 +124,8 @@ impl SqliteStore {
let connection = SqliteConnection::connect(url.as_ref()).await?;
let store = SqliteStore {
user_id: Arc::new(user_id.to_owned()),
device_id: Arc::new(device_id.to_owned()),
account_id: None,
device_id: Arc::new(device_id.into()),
account_info: None,
sessions: SessionStore::new(),
inbound_group_sessions: GroupSessionStore::new(),
devices: DeviceStore::new(),
@ -133,6 +139,10 @@ impl SqliteStore {
Ok(store)
}
fn account_id(&self) -> Option<i64> {
self.account_info.as_ref().map(|i| i.account_id)
}
async fn create_tables(&self) -> Result<()> {
let mut connection = self.connection.lock().await;
connection
@ -262,6 +272,25 @@ impl SqliteStore {
)
.await?;
connection
.execute(
r#"
CREATE TABLE IF NOT EXISTS device_signatures (
"id" INTEGER NOT NULL PRIMARY KEY,
"device_id" INTEGER NOT NULL,
"user_id" TEXT NOT NULL,
"key_algorithm" TEXT NOT NULL,
"signature" TEXT NOT NULL,
FOREIGN KEY ("device_id") REFERENCES "devices" ("id")
ON DELETE CASCADE
UNIQUE(device_id, user_id, key_algorithm)
);
CREATE INDEX IF NOT EXISTS "device_keys_device_id" ON "device_keys" ("device_id");
"#,
)
.await?;
Ok(())
}
@ -288,14 +317,17 @@ impl SqliteStore {
}
async fn load_sessions_for(&mut self, sender_key: &str) -> Result<Vec<Session>> {
let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?;
let account_info = self
.account_info
.as_ref()
.ok_or(CryptoStoreError::AccountUnset)?;
let mut connection = self.connection.lock().await;
let rows: Vec<(String, String, String, String)> = query_as(
"SELECT pickle, sender_key, creation_time, last_use_time
FROM sessions WHERE account_id = ? and sender_key = ?",
)
.bind(account_id)
.bind(account_info.account_id)
.bind(sender_key)
.fetch_all(&mut *connection)
.await?;
@ -315,6 +347,9 @@ impl SqliteStore {
.ok_or(CryptoStoreError::SessionTimestampError)?;
Ok(Session::from_pickle(
self.user_id.clone(),
self.device_id.clone(),
account_info.identity_keys.clone(),
pickle.to_string(),
self.get_pickle_mode(),
sender_key.to_string(),
@ -326,7 +361,7 @@ impl SqliteStore {
}
async fn load_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>> {
let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?;
let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?;
let mut connection = self.connection.lock().await;
let rows: Vec<(String, String, String, String)> = query_as(
@ -357,7 +392,7 @@ impl SqliteStore {
}
async fn save_tracked_user(&self, user: &UserId, dirty: bool) -> Result<()> {
let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?;
let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?;
let mut connection = self.connection.lock().await;
query(
@ -378,7 +413,7 @@ impl SqliteStore {
}
async fn load_tracked_users(&self) -> Result<(HashSet<UserId>, HashSet<UserId>)> {
let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?;
let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?;
let mut connection = self.connection.lock().await;
let rows: Vec<(String, bool)> = query_as(
@ -410,7 +445,7 @@ impl SqliteStore {
}
async fn load_devices(&self) -> Result<DeviceStore> {
let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?;
let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?;
let mut connection = self.connection.lock().await;
let rows: Vec<(i64, String, String, Option<String>, i64)> = query_as(
@ -456,31 +491,64 @@ impl SqliteStore {
.fetch_all(&mut *connection)
.await?;
let mut keys = BTreeMap::new();
let keys: BTreeMap<AlgorithmAndDeviceId, String> = key_rows
.into_iter()
.filter_map(|row| {
let algorithm = KeyAlgorithm::try_from(&*row.0).ok()?;
let key = row.1;
for row in key_rows {
let algorithm: &str = &row.0;
let algorithm = if let Ok(a) = KeyAlgorithm::try_from(algorithm) {
a
Some((
AlgorithmAndDeviceId(algorithm, device_id.as_str().into()),
key,
))
})
.collect();
let signature_rows: Vec<(String, String, String)> = query_as(
"SELECT user_id, key_algorithm, signature
FROM device_signatures WHERE device_id = ?",
)
.bind(device_row_id)
.fetch_all(&mut *connection)
.await?;
let mut signatures: BTreeMap<UserId, BTreeMap<AlgorithmAndDeviceId, String>> =
BTreeMap::new();
for row in signature_rows {
let user_id = if let Ok(u) = UserId::try_from(&*row.0) {
u
} else {
continue;
};
let key = &row.1;
let key_algorithm = if let Ok(k) = KeyAlgorithm::try_from(&*row.1) {
k
} else {
continue;
};
keys.insert(
AlgorithmAndDeviceId(algorithm, device_id.clone()),
key.to_owned(),
let signature = row.2;
if !signatures.contains_key(&user_id) {
let _ = signatures.insert(user_id.clone(), BTreeMap::new());
}
let user_map = signatures.get_mut(&user_id).unwrap();
user_map.insert(
AlgorithmAndDeviceId(key_algorithm, device_id.as_str().into()),
signature.to_owned(),
);
}
let device = Device::new(
user_id,
device_id.to_owned(),
device_id.as_str().into(),
display_name.clone(),
trust_state,
algorithms,
keys,
signatures,
);
store.add(device);
@ -490,7 +558,7 @@ impl SqliteStore {
}
async fn save_device_helper(&self, device: Device) -> Result<()> {
let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?;
let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?;
let mut connection = self.connection.lock().await;
@ -550,6 +618,23 @@ impl SqliteStore {
.await?;
}
for (user_id, signature_map) in device.signatures() {
for (key_id, signature) in signature_map {
query(
"INSERT OR IGNORE INTO device_signatures (
device_id, user_id, key_algorithm, signature
) VALUES (?1, ?2, ?3, ?4)
",
)
.bind(device_row_id)
.bind(user_id.as_str())
.bind(key_id.0.to_string())
.bind(signature)
.execute(&mut *connection)
.await?;
}
}
Ok(())
}
@ -573,20 +658,26 @@ impl CryptoStore for SqliteStore {
WHERE user_id = ? and device_id = ?",
)
.bind(self.user_id.as_str())
.bind(&*self.device_id)
.bind((&*self.device_id).as_ref())
.fetch_optional(&mut *connection)
.await?;
let result = if let Some((id, pickle, shared, uploaded_key_count)) = row {
self.account_id = Some(id);
Some(Account::from_pickle(
let account = Account::from_pickle(
pickle,
self.get_pickle_mode(),
shared,
uploaded_key_count,
&self.user_id,
&self.device_id,
)?)
)?;
self.account_info = Some(AccountInfo {
account_id: id,
identity_keys: account.identity_keys.clone(),
});
Some(account)
} else {
return Ok(None);
};
@ -640,19 +731,22 @@ impl CryptoStore for SqliteStore {
.fetch_one(&mut *connection)
.await?;
self.account_id = Some(account_id.0);
self.account_info = Some(AccountInfo {
account_id: account_id.0,
identity_keys: account.identity_keys.clone(),
});
Ok(())
}
async fn save_sessions(&mut self, sessions: &[Session]) -> Result<()> {
let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?;
// TODO turn this into a transaction
for session in sessions {
self.lazy_load_sessions(&session.sender_key).await?;
self.sessions.add(session.clone()).await;
let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?;
let session_id = session.session_id();
let creation_time = serde_json::to_string(&session.creation_time.elapsed())?;
let last_use_time = serde_json::to_string(&session.last_use_time.elapsed())?;
@ -683,7 +777,7 @@ impl CryptoStore for SqliteStore {
}
async fn save_inbound_group_session(&mut self, session: InboundGroupSession) -> Result<bool> {
let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?;
let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?;
let pickle = session.pickle(self.get_pickle_mode()).await;
let mut connection = self.connection.lock().await;
let session_id = session.session_id();
@ -753,7 +847,7 @@ impl CryptoStore for SqliteStore {
}
async fn delete_device(&self, device: Device) -> Result<()> {
let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?;
let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?;
let mut connection = self.connection.lock().await;
query(
@ -804,7 +898,7 @@ mod test {
use super::{Account, CryptoStore, InboundGroupSession, RoomId, Session, SqliteStore, TryFrom};
static USER_ID: &str = "@example:localhost";
static DEVICE_ID: &str = "DEVICEID";
static DEVICE_ID: &DeviceId = "DEVICEID";
async fn get_store(passphrase: Option<&str>) -> (SqliteStore, tempfile::TempDir) {
let tmpdir = tempdir().unwrap();
@ -840,16 +934,16 @@ mod test {
UserId::try_from("@alice:example.org").unwrap()
}
fn alice_device_id() -> DeviceId {
"ALICEDEVICE".to_string()
fn alice_device_id() -> Box<DeviceId> {
"ALICEDEVICE".into()
}
fn bob_id() -> UserId {
UserId::try_from("@bob:example.org").unwrap()
}
fn bob_device_id() -> DeviceId {
"BOBDEVICE".to_string()
fn bob_device_id() -> Box<DeviceId> {
"BOBDEVICE".into()
}
fn get_account() -> Account {
@ -866,7 +960,7 @@ mod test {
.await
.curve25519()
.iter()
.nth(0)
.next()
.unwrap()
.1
.to_owned();
@ -876,7 +970,7 @@ mod test {
};
let sender_key = bob.identity_keys().curve25519().to_owned();
let session = alice
.create_outbound_session(&sender_key, &one_time_key)
.create_outbound_session_helper(&sender_key, &one_time_key)
.await
.unwrap();
@ -1165,8 +1259,8 @@ mod test {
assert_eq!(device.keys(), loaded_device.keys());
let user_devices = store.get_user_devices(device.user_id()).await.unwrap();
assert_eq!(user_devices.keys().nth(0).unwrap(), device.device_id());
assert_eq!(user_devices.devices().nth(0).unwrap(), &device);
assert_eq!(user_devices.keys().next().unwrap(), device.device_id());
assert_eq!(user_devices.devices().next().unwrap(), &device);
}
#[tokio::test]

View file

@ -1,8 +1,8 @@
use crate::Device;
use matrix_sdk_common::events::key::verification::{
start::{StartEvent, StartEventContent},
accept::AcceptEvent,
start::{StartEvent, StartEventContent},
HashAlgorithm, KeyAgreementProtocol, MessageAuthenticationCode, ShortAuthenticationString,
VerificationMethod,
};
@ -11,7 +11,7 @@ use matrix_sdk_common::uuid::Uuid;
struct SasIds {
own_user_id: UserId,
own_device_id: DeviceId,
own_device_id: Box<DeviceId>,
other_device: Device,
}
@ -27,7 +27,7 @@ struct AcceptedProtocols {
key_agreement_protocol: KeyAgreementProtocol,
hash: HashAlgorithm,
message_auth_code: MessageAuthenticationCode,
short_auth_string: Vec<ShortAuthenticationString>
short_auth_string: Vec<ShortAuthenticationString>,
}
struct Sas<S> {
@ -38,11 +38,11 @@ struct Sas<S> {
}
impl Sas<Created> {
fn new(own_user_id: UserId, own_device_id: DeviceId, other_device: Device) -> Sas<Created> {
fn new(own_user_id: UserId, own_device_id: &DeviceId, other_device: Device) -> Sas<Created> {
Sas {
ids: SasIds {
own_user_id,
own_device_id,
own_device_id: own_device_id.into(),
other_device,
},
verification_flow_id: Uuid::new_v4(),
@ -71,12 +71,12 @@ impl Sas<Created> {
state: Accepted {
commitment: content.commitment.clone(),
accepted_protocols: AcceptedProtocols {
method: content.method,
hash: content.hash,
key_agreement_protocol: content.key_agreement_protocol,
message_auth_code: content.message_authentication_code,
short_auth_string: content.short_authentication_string.clone(),
}
method: content.method,
hash: content.hash,
key_agreement_protocol: content.key_agreement_protocol,
message_auth_code: content.message_authentication_code,
short_auth_string: content.short_authentication_string.clone(),
},
},
}
}
@ -89,7 +89,7 @@ struct Started {}
impl Sas<Started> {
fn from_start_event(
own_user_id: UserId,
own_device_id: DeviceId,
own_device_id: &DeviceId,
other_device: Device,
event: &StartEvent,
) -> Sas<Started> {
@ -102,7 +102,7 @@ impl Sas<Started> {
Sas {
ids: SasIds {
own_user_id,
own_device_id,
own_device_id: own_device_id.into(),
other_device,
},
verification_flow_id: Uuid::new_v4(),

View file

@ -16,4 +16,4 @@ http = "0.2.1"
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
matrix-sdk-test-macros = { version = "0.1.0", path = "../matrix_sdk_test_macros" }
lazy_static = "1.4.0"
serde = "1.0.111"
serde = "1.0.114"

View file

@ -6,8 +6,8 @@ use http::Response;
use matrix_sdk_common::api::r0::sync::sync_events::Response as SyncResponse;
use matrix_sdk_common::events::{
presence::PresenceEvent, AnyBasicEvent, AnyEphemeralRoomEventStub, AnyRoomEventStub,
AnyStateEventStub,
presence::PresenceEvent, AnyBasicEvent, AnySyncEphemeralRoomEvent, AnySyncRoomEvent,
AnySyncStateEvent,
};
use matrix_sdk_common::identifiers::RoomId;
use serde_json::Value as JsonValue;
@ -26,6 +26,7 @@ pub enum EventsJson {
HistoryVisibility,
JoinRules,
Member,
MemberNameChange,
MessageEmote,
MessageNotice,
MessageText,
@ -42,21 +43,54 @@ pub enum EventsJson {
Typing,
}
/// Easily create events to stream into either a Client or a `Room` for testing.
/// The `EventBuilder` struct can be used to easily generate valid sync responses for testing.
/// These can be then fed into either `Client` or `Room`.
///
/// It supports generated a number of canned events, such as a member entering a room, his power
/// level and display name changing and similar. It also supports insertion of custom events in the
/// form of `EventsJson` values.
///
/// **Important** You *must* use the *same* builder when sending multiple sync responses to
/// a single client. Otherwise, the subsequent responses will be *ignored* by the client because
/// the `next_batch` sync token will not be rotated properly.
///
/// # Example usage
///
/// ```rust
/// use matrix_sdk_test::{EventBuilder, EventsJson};
///
/// let mut builder = EventBuilder::new();
///
/// // response1 now contains events that add an example member to the room and change their power
/// // level
/// let response1 = builder
/// .add_room_event(EventsJson::Member)
/// .add_room_event(EventsJson::PowerLevels)
/// .build_sync_response();
///
/// // response2 is now empty (nothing changed)
/// let response2 = builder.build_sync_response();
///
/// // response3 contains a display name change for member example
/// let response3 = builder
/// .add_room_event(EventsJson::MemberNameChange)
/// .build_sync_response();
/// ```
#[derive(Default)]
pub struct EventBuilder {
/// The events that determine the state of a `Room`.
joined_room_events: HashMap<RoomId, Vec<AnyRoomEventStub>>,
joined_room_events: HashMap<RoomId, Vec<AnySyncRoomEvent>>,
/// The events that determine the state of a `Room`.
invited_room_events: HashMap<RoomId, Vec<AnyStateEventStub>>,
invited_room_events: HashMap<RoomId, Vec<AnySyncStateEvent>>,
/// The events that determine the state of a `Room`.
left_room_events: HashMap<RoomId, Vec<AnyRoomEventStub>>,
left_room_events: HashMap<RoomId, Vec<AnySyncRoomEvent>>,
/// The presence events that determine the presence state of a `RoomMember`.
presence_events: Vec<PresenceEvent>,
/// The state events that determine the state of a `Room`.
state_events: Vec<AnyStateEventStub>,
state_events: Vec<AnySyncStateEvent>,
/// The ephemeral room events that determine the state of a `Room`.
ephemeral: Vec<AnyEphemeralRoomEventStub>,
ephemeral: Vec<AnySyncEphemeralRoomEvent>,
/// The account data events that determine the state of a `Room`.
account_data: Vec<AnyBasicEvent>,
/// Internal counter to enable the `prev_batch` and `next_batch` of each sync response to vary.
@ -76,7 +110,7 @@ impl EventBuilder {
_ => panic!("unknown ephemeral event {:?}", json),
};
let event = serde_json::from_value::<AnyEphemeralRoomEventStub>(val.clone()).unwrap();
let event = serde_json::from_value::<AnySyncEphemeralRoomEvent>(val.clone()).unwrap();
self.ephemeral.push(event);
self
}
@ -97,11 +131,12 @@ impl EventBuilder {
pub fn add_room_event(&mut self, json: EventsJson) -> &mut Self {
let val: &JsonValue = match json {
EventsJson::Member => &test_json::MEMBER,
EventsJson::MemberNameChange => &test_json::MEMBER_NAME_CHANGE,
EventsJson::PowerLevels => &test_json::POWER_LEVELS,
_ => panic!("unknown room event json {:?}", json),
};
let event = serde_json::from_value::<AnyRoomEventStub>(val.clone()).unwrap();
let event = serde_json::from_value::<AnySyncRoomEvent>(val.clone()).unwrap();
self.add_joined_event(
&RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap(),
@ -115,12 +150,12 @@ impl EventBuilder {
room_id: &RoomId,
event: serde_json::Value,
) -> &mut Self {
let event = serde_json::from_value::<AnyRoomEventStub>(event).unwrap();
let event = serde_json::from_value::<AnySyncRoomEvent>(event).unwrap();
self.add_joined_event(room_id, event);
self
}
fn add_joined_event(&mut self, room_id: &RoomId, event: AnyRoomEventStub) {
fn add_joined_event(&mut self, room_id: &RoomId, event: AnySyncRoomEvent) {
self.joined_room_events
.entry(room_id.clone())
.or_insert_with(Vec::new)
@ -132,7 +167,7 @@ impl EventBuilder {
room_id: &RoomId,
event: serde_json::Value,
) -> &mut Self {
let event = serde_json::from_value::<AnyStateEventStub>(event).unwrap();
let event = serde_json::from_value::<AnySyncStateEvent>(event).unwrap();
self.invited_room_events
.entry(room_id.clone())
.or_insert_with(Vec::new)
@ -145,7 +180,7 @@ impl EventBuilder {
room_id: &RoomId,
event: serde_json::Value,
) -> &mut Self {
let event = serde_json::from_value::<AnyRoomEventStub>(event).unwrap();
let event = serde_json::from_value::<AnySyncRoomEvent>(event).unwrap();
self.left_room_events
.entry(room_id.clone())
.or_insert_with(Vec::new)
@ -164,7 +199,7 @@ impl EventBuilder {
_ => panic!("unknown state event {:?}", json),
};
let event = serde_json::from_value::<AnyStateEventStub>(val.clone()).unwrap();
let event = serde_json::from_value::<AnySyncStateEvent>(val.clone()).unwrap();
self.state_events.push(event);
self
}
@ -181,7 +216,8 @@ impl EventBuilder {
self
}
/// Consumes `ResponseBuilder` and returns `SyncResponse`.
/// Builds a `SyncResponse` containing the events we queued so far. The next response returned
/// by `build_sync_response` will then be empty if no further events were queued.
pub fn build_sync_response(&mut self) -> SyncResponse {
let main_room_id = RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap();
@ -293,12 +329,26 @@ impl EventBuilder {
let response = Response::builder()
.body(serde_json::to_vec(&body).unwrap())
.unwrap();
// Clear state so that the next sync response will be empty if nothing was added.
self.clear();
SyncResponse::try_from(response).unwrap()
}
fn generate_sync_token(&self) -> String {
format!("t392-516_47314_0_7_1_1_1_11444_{}", self.batch_counter)
}
pub fn clear(&mut self) {
self.account_data.clear();
self.ephemeral.clear();
self.invited_room_events.clear();
self.joined_room_events.clear();
self.left_room_events.clear();
self.presence_events.clear();
self.state_events.clear();
}
}
/// Embedded sync reponse files

View file

@ -208,6 +208,7 @@ lazy_static! {
});
}
// TODO: Move `prev_content` into `unsigned` once ruma supports it
lazy_static! {
pub static ref MEMBER: JsonValue = json!({
"content": {
@ -221,14 +222,40 @@ lazy_static! {
"sender": "@example:localhost",
"state_key": "@example:localhost",
"type": "m.room.member",
"prev_content": {
"avatar_url": null,
"displayname": "example",
"membership": "invite"
},
"unsigned": {
"age": 297036,
"replaces_state": "$151800111315tsynI:localhost",
"prev_content": {
"avatar_url": null,
"displayname": "example",
"membership": "invite"
}
"replaces_state": "$151800111315tsynI:localhost"
}
});
}
// TODO: Move `prev_content` into `unsigned` once ruma supports it
lazy_static! {
pub static ref MEMBER_NAME_CHANGE: JsonValue = json!({
"content": {
"avatar_url": null,
"displayname": "changed",
"membership": "join"
},
"event_id": "$151800234427abgho:localhost",
"membership": "join",
"origin_server_ts": 151800152,
"sender": "@example:localhost",
"state_key": "@example:localhost",
"type": "m.room.member",
"prev_content": {
"avatar_url": null,
"displayname": "example",
"membership": "join"
},
"unsigned": {
"age": 297032,
"replaces_state": "$151800140517rfvjc:localhost"
}
});
}
@ -552,6 +579,7 @@ lazy_static! {
});
}
// TODO: Move `prev_content` into `unsigned` once ruma supports it
lazy_static! {
pub static ref TOPIC: JsonValue = json!({
"content": {
@ -562,11 +590,11 @@ lazy_static! {
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.topic",
"prev_content": {
"topic": "test"
},
"unsigned": {
"age": 1392989,
"prev_content": {
"topic": "test"
},
"prev_sender": "@example:localhost",
"replaces_state": "$151957069225EVYKm:localhost"
}

View file

@ -9,8 +9,8 @@ pub mod sync;
pub use events::{
ALIAS, ALIASES, EVENT_ID, KEYS_QUERY, KEYS_UPLOAD, LOGIN, LOGIN_RESPONSE_ERR, LOGOUT, MEMBER,
MESSAGE_EDIT, MESSAGE_TEXT, NAME, POWER_LEVELS, PRESENCE, PUBLIC_ROOMS, REACTION, REDACTED,
REDACTED_INVALID, REDACTED_STATE, REDACTION, REGISTRATION_RESPONSE_ERR, ROOM_ID, ROOM_MESSAGES,
TYPING,
MEMBER_NAME_CHANGE, MESSAGE_EDIT, MESSAGE_TEXT, NAME, POWER_LEVELS, PRESENCE, PUBLIC_ROOMS,
REACTION, REDACTED, REDACTED_INVALID, REDACTED_STATE, REDACTION, REGISTRATION_RESPONSE_ERR,
ROOM_ID, ROOM_MESSAGES, TYPING,
};
pub use sync::{DEFAULT_SYNC_SUMMARY, INVITE_SYNC, LEAVE_SYNC, LEAVE_SYNC_EVENT, MORE_SYNC, SYNC};