Merge remote-tracking branch 'origin/main' into gnunicorn/issue833

This commit is contained in:
Benjamin Kampmann
2022-08-01 13:28:28 +02:00
124 changed files with 3092 additions and 1291 deletions
+2
View File
@@ -113,6 +113,8 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18.0
- name: Install NPM dependencies
working-directory: ${{ env.MATRIX_SDK_CRYPTO_JS_PATH }}
+2 -1
View File
@@ -1,7 +1,8 @@
edition = "2018"
max_width = 100
comment_width = 80
wrap_comments = true
imports_granularity = "Crate"
use_small_heuristics = "Max"
group_imports = "StdExternalCrate"
format_code_in_doc_comments = true
doc_comment_code_block_width = 80
+6
View File
@@ -17,6 +17,12 @@ resolver = "2"
[profile.release]
lto = true
[profile.dev]
# Copied from rust-analyzer. Saves a lot of disk space and hopefully
# compilation time / mem usage too, at the expense of potentially having to
# change this setting here when you want to use a debugger.
debug = 0
[profile.dev.package]
# Optimize quote even in debug mode. Speeds up proc-macros enough to account
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
+4 -4
View File
@@ -63,7 +63,7 @@ pub fn keys_query(c: &mut Criterion) {
let mut group = c.benchmark_group("Keys querying");
group.throughput(Throughput::Elements(count as u64));
let name = format!("{} device and cross signing keys", count);
let name = format!("{count} device and cross signing keys");
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
b.to_async(&runtime)
@@ -96,7 +96,7 @@ pub fn keys_claiming(c: &mut Criterion) {
let mut group = c.benchmark_group("Olm session creation");
group.throughput(Throughput::Elements(count as u64));
let name = format!("{} one-time keys", count);
let name = format!("{count} one-time keys");
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
b.iter_batched(
@@ -158,7 +158,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
let mut group = c.benchmark_group("Room key sharing");
group.throughput(Throughput::Elements(count as u64));
let name = format!("{} devices", count);
let name = format!("{count} devices");
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
b.to_async(&runtime).iter(|| async {
@@ -225,7 +225,7 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
let mut group = c.benchmark_group("Devices missing sessions collecting");
group.throughput(Throughput::Elements(count as u64));
let name = format!("{} devices", count);
let name = format!("{count} devices");
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
+6
View File
@@ -17,16 +17,22 @@ REL_TYPE_DIR="release"
# Build static libs for all the different architectures
# iOS
echo -e "Building for iOS [1/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-ios"
# MacOS
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-darwin"
echo -e "\nBuilding for macOS (Intel) [3/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "x86_64-apple-darwin"
# iOS Simulator
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-ios-sim"
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "x86_64-apple-ios"
echo -e "\nCreating XCFramework"
# Lipo together the libraries for the same platform
# MacOS
+2 -2
View File
@@ -2,7 +2,7 @@
name = "matrix-sdk-crypto-ffi"
version = "0.1.0"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
edition = "2018"
edition = "2021"
rust-version = "1.60"
description = "Uniffi based bindings for the Rust SDK crypto crate"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
@@ -57,7 +57,7 @@ features = ["rt-multi-thread"]
[dependencies.vodozemac]
git = "https://github.com/matrix-org/vodozemac/"
rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd"
rev = "18bcbc3359298894415931547ea41abb75af2d4a"
[build-dependencies]
uniffi_build = { version = "0.18.0", features = ["builtin-bindgen"] }
+2 -1
View File
@@ -14,7 +14,7 @@ mod responses;
mod users;
mod verification;
use std::{borrow::Borrow, collections::HashMap, convert::TryFrom, str::FromStr, sync::Arc};
use std::{borrow::Borrow, collections::HashMap, str::FromStr, sync::Arc};
pub use backup_recovery_key::{
BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
@@ -262,6 +262,7 @@ pub fn migrate(
imported: session.imported,
backed_up: session.backed_up,
history_visibility: None,
algorithm: ruma::EventEncryptionAlgorithm::MegolmV1AesSha2,
};
let session = matrix_sdk_crypto::olm::InboundGroupSession::from_pickle(pickle)?;
@@ -1,6 +1,5 @@
use std::{
collections::{BTreeMap, HashMap},
convert::TryInto,
io::Cursor,
ops::Deref,
sync::Arc,
@@ -32,10 +31,8 @@ use ruma::{
},
IncomingResponse,
},
events::{
key::verification::VerificationMethod, room::encrypted::OriginalSyncRoomEncryptedEvent,
AnySyncMessageLikeEvent,
},
events::{key::verification::VerificationMethod, AnySyncMessageLikeEvent},
serde::Raw,
DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
};
use serde::{Deserialize, Serialize};
@@ -602,7 +599,7 @@ impl OlmMachine {
content: &'a RawValue,
}
let event: OriginalSyncRoomEncryptedEvent = serde_json::from_str(event)?;
let event: Raw<_> = serde_json::from_str(event)?;
let room_id = RoomId::parse(room_id)?;
let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(&event, &room_id))?;
@@ -640,7 +637,7 @@ impl OlmMachine {
event: &str,
room_id: &str,
) -> Result<KeyRequestPair, DecryptionError> {
let event: OriginalSyncRoomEncryptedEvent = serde_json::from_str(event)?;
let event: Raw<_> = serde_json::from_str(event)?;
let room_id = RoomId::parse(room_id)?;
let (cancel, request) =
+8 -4
View File
@@ -1,15 +1,15 @@
[package]
authors = ["Ivan Enderlin <ivane@element.io>"]
name = "matrix-sdk-crypto-js"
description = "Matrix encryption library, for JavaScript"
authors = ["Ivan Enderlin <ivane@element.io>"]
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
license = "Apache-2.0"
name = "matrix-sdk-crypto-js"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = "1.60"
version = "0.5.0"
version = "0.1.0-alpha.0"
[package.metadata.docs.rs]
features = ["docsrs"]
@@ -31,7 +31,6 @@ tracing = []
matrix-sdk-common = { version = "0.5.0", path = "../../crates/matrix-sdk-common" }
matrix-sdk-crypto = { version = "0.5.0", path = "../../crates/matrix-sdk-crypto" }
ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] }
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd", features = ["js"] }
wasm-bindgen = "0.2.80"
wasm-bindgen-futures = "0.4.30"
js-sys = "0.3.49"
@@ -41,3 +40,8 @@ http = "0.2.6"
anyhow = "1.0.58"
tracing = { version = "0.1.35", default-features = false, features = ["attributes"] }
tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] }
[dependencies.vodozemac]
git = "https://github.com/matrix-org/vodozemac/"
rev = "18bcbc3359298894415931547ea41abb75af2d4a"
features = ["js"]
+61
View File
@@ -0,0 +1,61 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Matrix SDK Crypto JavaScript Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | filter(attribute="scope", value="crypto-js") | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/issues/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^test", group = "Testing"},
{ message = "^doc", group = "Documentation"},
{ message = "^refactor", group = "Refactoring"},
{ message = "^ci", group = "Continuous Integration"},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = ""
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@matrix-org/matrix-sdk-crypto-js",
"version": "0.5.0",
"version": "0.1.0-alpha.0",
"homepage": "https://github.com/matrix-org/matrix-rust-sdk",
"description": "Matrix encryption library, for JavaScript",
"license": "Apache-2.0",
@@ -35,7 +35,7 @@
"node": ">= 10"
},
"scripts": {
"build": "cross-env RUSTFLAGS='-C opt-level=z' wasm-pack build --release --target nodejs --out-name matrix_sdk_crypto --out-dir ./pkg",
"build": "cross-env RUSTFLAGS='-C opt-level=z' wasm-pack build --release --target nodejs --scope matrix-org --out-dir ./pkg",
"test": "jest --verbose",
"doc": "typedoc --tsconfig ."
}
+1 -2
View File
@@ -62,8 +62,7 @@ where
Ok(unsafe { T::ref_from_abi(pointer) })
} else {
Err(JsError::new(&format!(
"Expect an `{}` instance, received `{}` instead",
classname, constructor_name,
"Expect an `{classname}` instance, received `{constructor_name}` instead",
)))
}
}
+2 -5
View File
@@ -3,10 +3,7 @@
use std::collections::BTreeMap;
use js_sys::{Array, Map, Promise, Set};
use ruma::{
events::room::encrypted::OriginalSyncRoomEncryptedEvent, DeviceKeyAlgorithm,
OwnedTransactionId, UInt,
};
use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt};
use serde_json::Value as JsonValue;
use wasm_bindgen::prelude::*;
@@ -274,7 +271,7 @@ impl OlmMachine {
event: &str,
room_id: &identifiers::RoomId,
) -> Result<Promise, JsError> {
let event: OriginalSyncRoomEncryptedEvent = serde_json::from_str(event)?;
let event: Raw<_> = serde_json::from_str(event)?;
let room_id = room_id.inner.clone();
let me = self.inner.clone();
+3 -3
View File
@@ -207,7 +207,7 @@ mod inner {
let origin = metadata
.file()
.and_then(|file| metadata.line().map(|ln| format!("{}:{}", file, ln)))
.and_then(|file| metadata.line().map(|ln| format!("{file}:{ln}")))
.unwrap_or_default();
let message = format!("{level} {origin}{recorder}");
@@ -240,11 +240,11 @@ mod inner {
self.string.push('\n');
}
let _ = write!(self.string, "{:?}", value);
let _ = write!(self.string, "{value:?}");
}
field_name => {
let _ = write!(self.string, "\n{} = {:?}", field_name, value);
let _ = write!(self.string, "\n{field_name} = {value:?}");
}
}
}
@@ -1,4 +1,4 @@
const { EncryptionAlgorithm, EncryptionSettings, HistoryVisibility, VerificationState } = require('../pkg/matrix_sdk_crypto');
const { EncryptionAlgorithm, EncryptionSettings, HistoryVisibility, VerificationState } = require('../pkg/matrix_sdk_crypto_js');
describe('EncryptionAlgorithm', () => {
test('has the correct variant values', () => {
@@ -1,4 +1,4 @@
const { HistoryVisibility } = require('../pkg/matrix_sdk_crypto');
const { HistoryVisibility } = require('../pkg/matrix_sdk_crypto_js');
describe('HistoryVisibility', () => {
test('has the correct variant values', () => {
@@ -1,4 +1,4 @@
const { UserId, DeviceId, RoomId, ServerName } = require('../pkg/matrix_sdk_crypto');
const { UserId, DeviceId, RoomId, ServerName } = require('../pkg/matrix_sdk_crypto_js');
describe(UserId.name, () => {
test('cannot be invalid', () => {
@@ -1,4 +1,4 @@
const { OlmMachine, UserId, DeviceId, RoomId, DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, VerificationState } = require('../pkg/matrix_sdk_crypto');
const { OlmMachine, UserId, DeviceId, RoomId, DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, VerificationState } = require('../pkg/matrix_sdk_crypto_js');
describe(OlmMachine.name, () => {
test('can be instantiated with the async initializer', async () => {
@@ -1,4 +1,4 @@
const { RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, ToDeviceRequest, SignatureUploadRequest, RoomMessageRequest, KeysBackupRequest } = require('../pkg/matrix_sdk_crypto');
const { RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, ToDeviceRequest, SignatureUploadRequest, RoomMessageRequest, KeysBackupRequest } = require('../pkg/matrix_sdk_crypto_js');
describe('RequestType', () => {
test('has the correct variant values', () => {
@@ -1,4 +1,4 @@
const { DeviceLists, UserId } = require('../pkg/matrix_sdk_crypto');
const { DeviceLists, UserId } = require('../pkg/matrix_sdk_crypto_js');
describe(DeviceLists.name, () => {
test('can be empty', () => {
@@ -1,4 +1,4 @@
const { Tracing, LoggerLevel, OlmMachine, UserId, DeviceId } = require('../pkg/matrix_sdk_crypto');
const { Tracing, LoggerLevel, OlmMachine, UserId, DeviceId } = require('../pkg/matrix_sdk_crypto_js');
describe('LoggerLevel', () => {
test('has the correct variant values', () => {
+1 -1
View File
@@ -3,7 +3,7 @@
"strict": true
},
"typedocOptions": {
"entryPoints": ["pkg/matrix_sdk_crypto.d.ts"],
"entryPoints": ["pkg/matrix_sdk_crypto_js.d.ts"],
"out": "docs",
"readme": "README.md",
}
+5 -1
View File
@@ -29,7 +29,6 @@ matrix-sdk-crypto = { version = "0.5.0", path = "../../crates/matrix-sdk-crypto"
matrix-sdk-common = { version = "0.5.0", path = "../../crates/matrix-sdk-common" }
matrix-sdk-sled = { version = "0.1.0", path = "../../crates/matrix-sdk-sled", default-features = false, features = ["crypto-store"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] }
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd" }
napi = { git = "https://github.com/Hywan/napi-rs", branch = "fix-napi-strict-on-t-and-ref-t", default-features = false, features = ["napi6", "tokio_rt"] }
napi-derive = { git = "https://github.com/Hywan/napi-rs", branch = "fix-napi-strict-on-t-and-ref-t" }
serde_json = "1.0.79"
@@ -37,5 +36,10 @@ http = "0.2.6"
zeroize = "1.3.0"
tracing-subscriber = { version = "0.3", default-features = false, features = ["tracing-log", "time", "smallvec", "fmt", "env-filter"], optional = true }
[dependencies.vodozemac]
git = "https://github.com/matrix-org/vodozemac/"
rev = "18bcbc3359298894415931547ea41abb75af2d4a"
features = ["js"]
[build-dependencies]
napi-build = "2.0.0"
+4 -2
View File
@@ -40,9 +40,11 @@ commit_preprocessors = [
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^test", group = "Testing"},
{ message = "^doc", group = "Documentation"},
{ message = "^refactor", group = "Refactoring"},
{ message = "^ci", group = "Continuous Integration"},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
@@ -7,11 +7,8 @@ use std::{
use napi::bindgen_prelude::Either7;
use napi_derive::*;
use ruma::{
events::room::encrypted::OriginalSyncRoomEncryptedEvent, DeviceKeyAlgorithm,
OwnedTransactionId, UInt,
};
use serde_json::Value as JsonValue;
use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt};
use serde_json::{value::RawValue, Value as JsonValue};
use zeroize::Zeroize;
use crate::{
@@ -389,8 +386,7 @@ impl OlmMachine {
event: String,
room_id: &identifiers::RoomId,
) -> napi::Result<responses::DecryptedRoomEvent> {
let event: OriginalSyncRoomEncryptedEvent =
serde_json::from_str(event.as_str()).map_err(into_err)?;
let event = Raw::from_json(RawValue::from_string(event).map_err(into_err)?);
let room_id = room_id.inner.clone();
let room_event = self.inner.decrypt_room_event(&event, &room_id).await.map_err(into_err)?;
+14 -2
View File
@@ -42,7 +42,7 @@ interface Client {
string homeserver();
void start_sync();
void start_sync(u16? timeline_limit);
[Throws=ClientError]
string restore_token();
@@ -78,6 +78,12 @@ callback interface RoomDelegate {
void did_receive_message(AnyMessage message);
};
enum Membership {
"Invited",
"Joined",
"Left",
};
interface Room {
void set_delegate(RoomDelegate? delegate);
@@ -85,7 +91,9 @@ interface Room {
string? name();
string? topic();
string? avatar_url();
Membership membership();
boolean is_direct();
boolean is_public();
boolean is_space();
@@ -159,6 +167,7 @@ interface MediaSource {
[Error]
enum AuthenticationError {
"ClientMissing",
"SessionMissing",
"Generic",
};
@@ -178,6 +187,9 @@ interface AuthenticationService {
[Throws=AuthenticationError]
Client login(string username, string password);
[Throws=AuthenticationError]
Client restore_with_access_token(string token, string device_id);
};
interface SessionVerificationEmoji {
@@ -1,6 +1,10 @@
use std::sync::Arc;
use futures_util::future::join3;
use matrix_sdk::{
ruma::{OwnedDeviceId, UserId},
Session,
};
use parking_lot::RwLock;
use super::{client::Client, client_builder::ClientBuilder, RUNTIME};
@@ -15,6 +19,8 @@ pub struct AuthenticationService {
pub enum AuthenticationError {
#[error("A successful call to use_server must be made first.")]
ClientMissing,
#[error("Login was successful but is missing a valid Session to configure the file store.")]
SessionMissing,
#[error("An error occurred: {message}")]
Generic { message: String },
}
@@ -66,14 +72,12 @@ impl AuthenticationService {
/// Updates the service to authenticate with the homeserver for the
/// specified address.
pub fn configure_homeserver(&self, server_name: String) -> Result<(), AuthenticationError> {
// Construct a username as the builder currently requires one.
let username = format!("@auth:{}", server_name);
let mut builder =
Arc::new(ClientBuilder::new()).base_path(self.base_path.clone()).username(username);
let mut builder = Arc::new(ClientBuilder::new()).base_path(self.base_path.clone());
if server_name.starts_with("http://") || server_name.starts_with("https://") {
builder = builder.homeserver_url(server_name)
} else {
builder = builder.server_name(server_name);
}
let client = builder.build().map_err(AuthenticationError::from)?;
@@ -96,18 +100,74 @@ impl AuthenticationService {
) -> Result<Arc<Client>, AuthenticationError> {
match self.client.read().as_ref() {
Some(client) => {
let homeserver_url = client.homeserver();
// Login and ask the server for the full user ID as this could be different from
// the username that was entered.
client.login(username, password).map_err(AuthenticationError::from)?;
let whoami = client.whoami()?;
// Create a new client to setup the store path for the username
// Create a new client to setup the store path now the user ID is known.
let homeserver_url = client.homeserver();
let session = client.session().ok_or(AuthenticationError::SessionMissing)?;
let client = Arc::new(ClientBuilder::new())
.base_path(self.base_path.clone())
.homeserver_url(homeserver_url)
.username(username.clone())
.username(whoami.user_id.to_string())
.build()
.map_err(AuthenticationError::from)?;
// Restore the client using the session from the login request.
client
.login(username, password)
.restore_session(session.clone())
.map(|_| client.clone())
.map_err(AuthenticationError::from)
}
None => Err(AuthenticationError::ClientMissing),
}
}
/// Restore an existing session on the current homeserver using an access
/// token issued by an authentication server.
/// # Arguments
///
/// * `token` - The access token issued by the authentication server.
///
/// * `device_id` - The device ID that the access token was scoped for.
pub fn restore_with_access_token(
&self,
token: String,
device_id: String,
) -> Result<Arc<Client>, AuthenticationError> {
match self.client.read().as_ref() {
Some(client) => {
// Restore the client and ask the server for the full user ID as this
// could be different from the username that was entered.
let discovery_user_id = UserId::parse("@unknown:unknown")
.map_err(|e| AuthenticationError::Generic { message: e.to_string() })?;
let device_id: OwnedDeviceId = device_id.as_str().into();
let discovery_session = Session {
access_token: token.clone(),
user_id: discovery_user_id,
device_id: device_id.clone(),
};
client.restore_session(discovery_session).map_err(AuthenticationError::from)?;
let whoami = client.whoami()?;
// Create the actual client with a store path from the user ID.
let homeserver_url = client.homeserver();
let session =
Session { access_token: token, user_id: whoami.user_id.clone(), device_id };
let client = Arc::new(ClientBuilder::new())
.base_path(self.base_path.clone())
.homeserver_url(homeserver_url)
.username(whoami.user_id.to_string())
.build()
.map_err(AuthenticationError::from)?;
// Restore the client using the session.
client
.restore_session(session)
.map(|_| client.clone())
.map_err(AuthenticationError::from)
}
+21 -2
View File
@@ -1,10 +1,12 @@
use std::sync::Arc;
use anyhow::anyhow;
use matrix_sdk::{
config::SyncSettings,
media::{MediaFormat, MediaRequest},
ruma::{
api::client::{
account::whoami,
filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter},
session::get_login_types,
sync::sync_events::v3::Filter,
@@ -12,7 +14,7 @@ use matrix_sdk::{
events::room::MediaSource,
TransactionId,
},
Client as MatrixClient, LoopCtrl,
Client as MatrixClient, LoopCtrl, Session,
};
use parking_lot::RwLock;
@@ -51,6 +53,7 @@ impl Client {
}
}
/// Login using a username and password.
pub fn login(&self, username: String, password: String) -> anyhow::Result<()> {
RUNTIME.block_on(async move {
self.client.login_username(&username, &password).send().await?;
@@ -58,10 +61,16 @@ impl Client {
})
}
/// Restores the client from a `RestoreToken`.
pub fn restore_login(&self, restore_token: String) -> anyhow::Result<()> {
let RestoreToken { session, homeurl: _, is_guest: _ } =
serde_json::from_str(&restore_token)?;
self.restore_session(session)
}
/// Restores the client from a `Session`.
pub fn restore_session(&self, session: Session) -> anyhow::Result<()> {
RUNTIME.block_on(async move {
self.client.restore_login(session).await?;
Ok(())
@@ -97,7 +106,13 @@ impl Client {
Ok(supports_password)
}
pub fn start_sync(&self) {
/// Gets information about the owner of a given access token.
pub fn whoami(&self) -> anyhow::Result<whoami::v3::Response> {
RUNTIME
.block_on(async move { self.client.whoami().await.map_err(|e| anyhow!(e.to_string())) })
}
pub fn start_sync(&self, timeline_limit: Option<u16>) {
let client = self.client.clone();
let state = self.state.clone();
let delegate = self.delegate.clone();
@@ -106,12 +121,16 @@ impl Client {
let mut filter = FilterDefinition::default();
let mut room_filter = RoomFilter::default();
let mut event_filter = RoomEventFilter::default();
let mut timeline_filter = RoomEventFilter::default();
event_filter.lazy_load_options =
LazyLoadOptions::Enabled { include_redundant_members: false };
room_filter.state = event_filter;
filter.room = room_filter;
timeline_filter.limit = timeline_limit.map(|limit| limit.into());
filter.room.timeline = timeline_filter;
let filter_id = client.get_or_upload_filter("sync", filter).await.unwrap();
let sync_settings = SyncSettings::new().filter(Filter::FilterId(&filter_id));
+29 -15
View File
@@ -1,9 +1,10 @@
use std::{fs, path::PathBuf, sync::Arc};
use anyhow::Context;
use anyhow::anyhow;
use matrix_sdk::{
ruma::UserId, store::make_store_config, Client as MatrixClient,
ClientBuilder as MatrixClientBuilder,
ruma::{ServerName, UserId},
store::make_store_config,
Client as MatrixClient, ClientBuilder as MatrixClientBuilder,
};
use sanitize_filename_reader_friendly::sanitize;
@@ -13,6 +14,7 @@ use super::{client::Client, ClientState, RUNTIME};
pub struct ClientBuilder {
base_path: Option<String>,
username: Option<String>,
server_name: Option<String>,
homeserver_url: Option<String>,
inner: MatrixClientBuilder,
}
@@ -22,6 +24,7 @@ impl ClientBuilder {
Self {
base_path: None,
username: None,
server_name: None,
homeserver_url: None,
inner: MatrixClient::builder().user_agent("rust-sdk-ios"),
}
@@ -39,6 +42,12 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn server_name(self: Arc<Self>, server_name: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.server_name = Some(server_name);
Arc::new(builder)
}
pub fn homeserver_url(self: Arc<Self>, url: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.homeserver_url = Some(url);
@@ -47,25 +56,30 @@ impl ClientBuilder {
pub fn build(self: Arc<Self>) -> anyhow::Result<Arc<Client>> {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = builder.inner;
let base_path = builder.base_path.context("Base path was not set")?;
let username = builder
.username
.context("Username to determine homeserver and home path was not set")?;
if let (Some(base_path), Some(username)) = (builder.base_path, &builder.username) {
// Determine store path
let data_path = PathBuf::from(base_path).join(sanitize(username));
fs::create_dir_all(&data_path)?;
let store_config = make_store_config(&data_path, None)?;
// Determine store path
let data_path = PathBuf::from(base_path).join(sanitize(&username));
fs::create_dir_all(&data_path)?;
let store_config = make_store_config(&data_path, None)?;
inner_builder = inner_builder.store_config(store_config);
}
let mut inner_builder = builder.inner.store_config(store_config);
// Determine server either from explicitly set homeserver or from userId
// Determine server either from URL, server name or user ID.
if let Some(homeserver_url) = builder.homeserver_url {
inner_builder = inner_builder.homeserver_url(homeserver_url);
} else {
} else if let Some(server_name) = builder.server_name {
let server_name = ServerName::parse(server_name)?;
inner_builder = inner_builder.server_name(&server_name);
} else if let Some(username) = builder.username {
let user = UserId::parse(username)?;
inner_builder = inner_builder.server_name(user.server_name());
} else {
return Err(anyhow!(
"Failed to build: One of homeserver_url, server_name or username must be called."
));
}
RUNTIME.block_on(async move {
+14
View File
@@ -18,6 +18,12 @@ pub trait RoomDelegate: Sync + Send {
fn did_receive_message(&self, messages: Arc<AnyMessage>);
}
pub enum Membership {
Invited,
Joined,
Left,
}
pub struct Room {
room: MatrixRoom,
delegate: Arc<RwLock<Option<Box<dyn RoomDelegate>>>>,
@@ -80,6 +86,14 @@ impl Room {
})
}
pub fn membership(&self) -> Membership {
match &self.room {
MatrixRoom::Invited(_) => Membership::Invited,
MatrixRoom::Joined(_) => Membership::Joined,
MatrixRoom::Left(_) => Membership::Left,
}
}
pub fn is_direct(&self) -> bool {
self.room.is_direct()
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
authors = ["Johannes Becker <j.becker@famedly.com>"]
edition = "2018"
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
description = "Appservice SDK based on the matrix-sdk"
@@ -60,11 +60,12 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
let appservice = AppService::new(homeserver_url, server_name, registration).await?;
appservice.register_user_query(Box::new(|_, _| Box::pin(async { true }))).await;
appservice
.virtual_user(None)
.await?
.register_event_handler_context(appservice.clone())
.register_event_handler(
let virtual_user = appservice.virtual_user(None).await?;
virtual_user.add_event_handler_context(appservice.clone());
virtual_user
.add_event_handler(
move |event: OriginalSyncRoomMemberEvent,
room: Room,
Ctx(appservice): Ctx<AppService>| {
+25 -19
View File
@@ -43,8 +43,8 @@
//! # async {
//! #
//! use matrix_sdk_appservice::{
//! ruma::events::room::member::SyncRoomMemberEvent,
//! AppService, AppServiceRegistration
//! ruma::events::room::member::SyncRoomMemberEvent, AppService,
//! AppServiceRegistration,
//! };
//!
//! let homeserver_url = "http://127.0.0.1:8008";
@@ -60,13 +60,15 @@
//! users:
//! - exclusive: true
//! regex: '@_appservice_.*'
//! ")?;
//! ",
//! )?;
//!
//! let mut appservice = AppService::new(homeserver_url, server_name, registration).await?;
//! let mut appservice =
//! AppService::new(homeserver_url, server_name, registration).await?;
//! appservice
//! .virtual_user(None)
//! .await?
//! .register_event_handler(|_ev: SyncRoomMemberEvent| async {
//! .add_event_handler(|_ev: SyncRoomMemberEvent| async {
//! // do stuff
//! })
//! .await;
@@ -84,7 +86,7 @@
//! [matrix-org/matrix-rust-sdk#228]: https://github.com/matrix-org/matrix-rust-sdk/issues/228
//! [examples directory]: https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-appservice/examples
use std::{convert::TryInto, sync::Arc};
use std::sync::Arc;
use dashmap::DashMap;
pub use error::Error;
@@ -122,7 +124,7 @@ pub use virtual_user::VirtualUserBuilder;
pub type Result<T, E = Error> = std::result::Result<T, E>;
const USER_KEY: &[u8] = b"appservice.users.";
pub const USER_MEMBER: &[u8] = b"appservice.users.membership.";
const USER_MEMBER: &[u8] = b"appservice.users.membership.";
type Localpart = String;
@@ -256,10 +258,12 @@ impl AppService {
/// ```no_run
/// # use matrix_sdk_appservice::AppService;
/// # fn run(appservice: AppService) {
/// appservice.register_user_query(Box::new(|appservice, req| Box::pin(async move {
/// println!("Got request for {}", req.user_id);
/// true
/// })));
/// appservice.register_user_query(Box::new(|appservice, req| {
/// Box::pin(async move {
/// println!("Got request for {}", req.user_id);
/// true
/// })
/// }));
/// # }
/// ```
pub async fn register_user_query(
@@ -278,10 +282,12 @@ impl AppService {
/// ```no_run
/// # use matrix_sdk_appservice::AppService;
/// # fn run(appservice: AppService) {
/// appservice.register_room_query(Box::new(|appservice, req| Box::pin(async move {
/// println!("Got request for {}", req.room_alias);
/// true
/// })));
/// appservice.register_room_query(Box::new(|appservice, req| {
/// Box::pin(async move {
/// println!("Got request for {}", req.room_alias);
/// true
/// })
/// }));
/// # }
/// ```
pub async fn register_room_query(
@@ -492,7 +498,7 @@ impl AppService {
}
for task in tasks {
if let Err(e) = task.await {
warn!("Joining sync task failed: {}", e);
warn!("Joining sync task failed: {e}");
}
}
Ok(())
@@ -505,7 +511,7 @@ impl AppService {
pub async fn run(&self, host: impl Into<String>, port: impl Into<u16>) -> Result<()> {
let host = host.into();
let port = port.into();
info!("Starting AppService on {}:{}", &host, &port);
info!(host, port, "Starting AppService");
webserver::run_server(self.clone(), host, port).await?;
Ok(())
@@ -640,7 +646,7 @@ mod tests {
appservice
.virtual_user(None)
.await?
.register_event_handler({
.add_event_handler({
let on_state_member = on_state_member.clone();
move |_ev: OriginalSyncRoomMemberEvent| {
*on_state_member.lock().unwrap() = true;
@@ -796,7 +802,7 @@ mod tests {
appservice
.virtual_user(None)
.await?
.register_event_handler({
.add_event_handler({
let on_state_member = on_state_member.clone();
move |_ev: OriginalSyncRoomMemberEvent| {
*on_state_member.lock().unwrap() = true;
@@ -14,7 +14,7 @@
//! AppService Registration.
use std::{convert::TryFrom, fs::File, ops::Deref, path::PathBuf};
use std::{fs::File, ops::Deref, path::PathBuf};
use http::Uri;
use regex::Regex;
+10 -6
View File
@@ -145,7 +145,7 @@ mod filters {
.and(warp::body::bytes())
.and_then(|method, path: FullPath, query, headers, bytes| async move {
let uri = http::uri::Builder::new()
.path_and_query(format!("{}?{}", path.as_str(), query))
.path_and_query(format!("{}?{query}", path.as_str()))
.build()
.map_err(Error::from)?;
@@ -164,9 +164,13 @@ mod filters {
mod handlers {
use percent_encoding::percent_decode_str;
use serde::Serialize;
use super::*;
#[derive(Serialize)]
struct EmptyObject {}
pub async fn user(
user_id: String,
appservice: AppService,
@@ -177,12 +181,12 @@ mod handlers {
let request = query_user::IncomingRequest::try_from_http_request(request, &[user_id])
.map_err(Error::from)?;
return if user_exists(appservice.clone(), request).await {
Ok(warp::reply::json(&String::from("{}")))
Ok(warp::reply::json(&EmptyObject {}))
} else {
Err(warp::reject::not_found())
};
}
Ok(warp::reply::json(&String::from("{}")))
Ok(warp::reply::json(&EmptyObject {}))
}
pub async fn room(
@@ -195,12 +199,12 @@ mod handlers {
let request = query_room::IncomingRequest::try_from_http_request(request, &[room_id])
.map_err(Error::from)?;
return if room_exists(appservice.clone(), request).await {
Ok(warp::reply::json(&String::from("{}")))
Ok(warp::reply::json(&EmptyObject {}))
} else {
Err(warp::reject::not_found())
};
}
Ok(warp::reply::json(&String::from("{}")))
Ok(warp::reply::json(&EmptyObject {}))
}
pub async fn transaction(
@@ -213,7 +217,7 @@ mod handlers {
.map_err(Error::from)?;
appservice.receive_transaction(incoming_transaction).await?;
Ok(warp::reply::json(&String::from("{}")))
Ok(warp::reply::json(&EmptyObject {}))
}
}
+8 -10
View File
@@ -305,11 +305,12 @@ impl BaseClient {
#[cfg(feature = "e2e-encryption")]
AnySyncRoomEvent::MessageLike(e) => match e {
AnySyncMessageLikeEvent::RoomEncrypted(
SyncMessageLikeEvent::Original(encrypted),
SyncMessageLikeEvent::Original(_),
) => {
if let Some(olm) = self.olm_machine() {
if let Ok(decrypted) =
olm.decrypt_room_event(encrypted, room_id).await
if let Ok(decrypted) = olm
.decrypt_room_event(event.event.cast_ref(), room_id)
.await
{
event = decrypted.into();
}
@@ -400,8 +401,8 @@ impl BaseClient {
}
Err(err) => {
warn!(
"Couldn't deserialize stripped state event for room {}: {:?}",
room_info.room_id, err
room_id = %room_info.room_id,
"Couldn't deserialize stripped state event: {err:?}",
);
}
}
@@ -429,10 +430,7 @@ impl BaseClient {
let event = match raw_event.deserialize() {
Ok(e) => e,
Err(e) => {
warn!(
"Couldn't deserialize state event for room {}: {:?} {:#?}",
room_id, e, raw_event
);
warn!(%room_id, "Couldn't deserialize state event: {e:?}");
continue;
}
};
@@ -831,7 +829,7 @@ impl BaseClient {
.filter_map(|event| match event.deserialize() {
Ok(ev) => Some(ev),
Err(e) => {
debug!(?event, "Failed to deserialize m.room.member event: {}", e);
debug!(?event, "Failed to deserialize m.room.member event: {e}");
None
}
})
+2 -2
View File
@@ -47,9 +47,9 @@ impl fmt::Display for DisplayName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DisplayName::Named(s) | DisplayName::Calculated(s) | DisplayName::Aliased(s) => {
write!(f, "{}", s)
write!(f, "{s}")
}
DisplayName::EmptyWas(s) => write!(f, "Empty Room (was {})", s),
DisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
DisplayName::Empty => write!(f, "Empty Room"),
}
}
+14 -3
View File
@@ -44,6 +44,7 @@ use ruma::{
RoomVersionId, UserId,
};
use serde::{Deserialize, Serialize};
use tracing::debug;
use super::{BaseRoomInfo, DisplayName, RoomMember};
use crate::{
@@ -396,7 +397,7 @@ impl Room {
_ => (summary.joined_member_count, summary.invited_member_count),
};
tracing::debug!(
debug!(
room_id = self.room_id().as_str(),
own_user = self.own_user_id.as_str(),
joined, invited,
@@ -589,6 +590,8 @@ impl Room {
/// Add a new timeline slice to the timeline streams.
#[cfg(feature = "experimental-timeline")]
pub async fn add_timeline_slice(&self, timeline: &TimelineSlice) {
use tracing::warn;
if timeline.sync {
let mut streams = self.forward_timeline_streams.lock().await;
let mut remaining_streams = Vec::with_capacity(streams.len());
@@ -596,7 +599,11 @@ impl Room {
if !forward.is_closed() {
if let Err(error) = forward.try_send(timeline.clone()) {
if error.is_full() {
tracing::warn!("Drop timeline slice because the limit of the buffer for the forward stream is reached");
warn!(
room_id = %self.room_id(),
"Dropping timeline slice because the limit of the buffer for the \
forward stream is reached"
);
}
} else {
remaining_streams.push(forward);
@@ -611,7 +618,11 @@ impl Room {
if !backward.is_closed() {
if let Err(error) = backward.try_send(timeline.clone()) {
if error.is_full() {
tracing::warn!("Drop timeline slice because the limit of the buffer for the backward stream is reached");
warn!(
room_id = %self.room_id(),
"Dropping timeline slice because the limit of the buffer for the \
backward stream is reached"
);
}
} else {
remaining_streams.push(backward);
@@ -123,7 +123,7 @@ impl AmbiguityCache {
member_ambiguous: ambiguous,
};
trace!("Handling display name ambiguity for {}: {:#?}", member_event.state_key(), change);
trace!(user_id = %member_event.state_key(), "Handling display name ambiguity: {change:#?}");
self.add_change(room_id, member_event.event_id().to_owned(), change);
@@ -17,13 +17,13 @@
/// #[cfg(test)]
/// mod tests {
///
/// use super::{MyStore, StoreResult, StateStore};
/// use super::{MyStore, StateStore, StoreResult};
///
/// async fn get_store() -> StoreResult<impl StateStore> {
/// Ok(MyStore::new())
/// }
/// async fn get_store() -> StoreResult<impl StateStore> {
/// Ok(MyStore::new())
/// }
///
/// statestore_integration_tests! { integration }
/// statestore_integration_tests! { integration }
/// }
/// ```
#[allow(unused_macros, unused_extern_crates)]
@@ -775,13 +775,14 @@ macro_rules! statestore_integration_tests {
.zip(stored_events.iter())
.enumerate()
{
assert_eq!(a.expect("not a value").event_id(), b.event_id(), "pos {} not equal - expected: {:#?}, but found {:#?}", idx, expected, found);
assert_eq!(
a.expect("not a value").event_id(),
b.event_id(),
"pos {idx} not equal - expected: {expected:#?}, but found {found:#?}",
);
}
}
}
)*
}
}
@@ -43,6 +43,7 @@ use ruma::{
serde::Raw,
EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
};
use tracing::info;
#[cfg(feature = "experimental-timeline")]
use super::BoxStream;
@@ -342,26 +343,25 @@ impl MemoryStore {
}
#[cfg(feature = "experimental-timeline")]
for (room, timeline) in &changes.timeline {
for (room_id, timeline) in &changes.timeline {
use tracing::warn;
if timeline.sync {
tracing::info!("Save new timeline batch from sync response for {}", room);
info!(%room_id, "Saving new timeline batch from sync response");
} else {
tracing::info!("Save new timeline batch from messages response for {}", room);
info!(%room_id, "Saving new timeline batch from messages response");
}
let mut delete_timeline = false;
if timeline.limited {
tracing::info!(
"Delete stored timeline for {} because the sync response was limited",
room
);
info!(%room_id, "Deleting stored timeline because the sync response was limited");
delete_timeline = true;
} else if let Some(mut data) = self.room_timeline.get_mut(room) {
} else if let Some(mut data) = self.room_timeline.get_mut(room_id) {
if !timeline.sync && Some(&timeline.start) != data.end.as_ref() {
// This should only happen when a developer adds a wrong timeline
// batch to the `StateChanges` or the server returns a wrong response
// to our request.
tracing::warn!("Drop unexpected timeline batch for {}", room);
warn!(%room_id, "Dropping unexpected timeline batch");
return Ok(());
}
@@ -385,12 +385,12 @@ impl MemoryStore {
}
if delete_timeline {
tracing::info!("Delete stored timeline for {} because of duplicated events", room);
self.room_timeline.remove(room);
info!(%room_id, "Deleting stored timeline because of duplicated events");
self.room_timeline.remove(room_id);
}
let mut data =
self.room_timeline.entry(room.to_owned()).or_insert_with(|| TimelineData {
self.room_timeline.entry(room_id.to_owned()).or_insert_with(|| TimelineData {
start: timeline.start.clone(),
end: timeline.end.clone(),
..Default::default()
@@ -398,13 +398,10 @@ impl MemoryStore {
let make_room_version = || {
self.room_info
.get(room)
.get(room_id)
.and_then(|info| info.room_version().cloned())
.unwrap_or_else(|| {
tracing::warn!(
"Unable to find the room version for {}, assume version 9",
room
);
warn!(%room_id, "Unable to find the room version, assuming version 9");
RoomVersionId::V9
})
};
@@ -455,7 +452,7 @@ impl MemoryStore {
}
}
tracing::info!("Saved changes in {:?}", now.elapsed());
info!("Saved changes in {:?}", now.elapsed());
Ok(())
}
@@ -676,7 +673,7 @@ impl MemoryStore {
let (events, end_token) = if let Some(data) = self.room_timeline.get(room_id) {
(data.events.clone(), data.end.clone())
} else {
tracing::info!("No timeline for {} was previously stored", room_id);
info!(%room_id, "Couldn't find a previously stored timeline");
return Ok(None);
};
@@ -686,11 +683,7 @@ impl MemoryStore {
}
};
tracing::info!(
"Found previously stored timeline for {}, with end token {:?}",
room_id,
end_token
);
info!(%room_id, ?end_token, "Found previously stored timeline");
Ok(Some((Box::pin(stream), end_token)))
}
+18 -5
View File
@@ -51,12 +51,25 @@ zeroize = { version = "1.3.0", features = ["zeroize_derive"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.18", default-features = false, features = ["time"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c", "rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] }
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
ruma = { git = "https://github.com/ruma/ruma", rev = "ca8c66c885241a7ba3805399604eda4a38979f6b", features = ["client-api-c", "js", "rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] }
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd", features = ["js"] }
[target.'cfg(target_arch = "wasm32")'.dependencies.ruma]
git = "https://github.com/ruma/ruma"
rev = "ca8c66c885241a7ba3805399604eda4a38979f6b"
features = ["client-api-c", "js", "rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.ruma]
git = "https://github.com/ruma/ruma"
rev = "ca8c66c885241a7ba3805399604eda4a38979f6b"
features = ["client-api-c", "rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.vodozemac]
git = "https://github.com/matrix-org/vodozemac/"
rev = "18bcbc3359298894415931547ea41abb75af2d4a"
[target.'cfg(target_arch = "wasm32")'.dependencies.vodozemac]
git = "https://github.com/matrix-org/vodozemac/"
rev = "18bcbc3359298894415931547ea41abb75af2d4a"
features = ["js"]
[dev-dependencies]
futures = { version = "0.3.21", default-features = false, features = ["executor"] }
+1 -1
View File
@@ -17,7 +17,7 @@ The state machine works in a push/pull manner:
state machine
```rust,no_run
use std::{collections::BTreeMap, convert::TryFrom};
use std::collections::BTreeMap;
use matrix_sdk_crypto::{OlmMachine, OlmError};
use ruma::{
@@ -13,7 +13,6 @@
// limitations under the License.
use std::{
convert::TryFrom,
io::{Cursor, Read},
ops::DerefMut,
};
+2 -2
View File
@@ -39,7 +39,7 @@ pub enum OlmError {
/// The received room key couldn't be converted into a valid Megolm session.
#[error(transparent)]
SessionCreation(#[from] vodozemac::megolm::SessionKeyDecodeError),
SessionCreation(#[from] SessionCreationError),
/// The storage layer returned an error.
#[error("failed to read or write to the crypto store {0}")]
@@ -135,7 +135,7 @@ pub enum EventError {
#[error(
"the room id of the room key doesn't match the room id of the \
decrypted event: expected {0}, got {:1}"
decrypted event: expected {0}, got {1:?}"
)]
MismatchedRoom(OwnedRoomId, Option<OwnedRoomId>),
}
@@ -374,6 +374,6 @@ mod tests {
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
let mut decrypted_data = Vec::new();
assert!(decryptor.read_to_end(&mut decrypted_data).is_err())
decryptor.read_to_end(&mut decrypted_data).unwrap_err();
}
}
@@ -312,7 +312,7 @@ mod tests {
#[test]
fn test_decode() {
let export = export_without_headers();
assert!(decode(export).is_ok());
decode(export).unwrap();
}
#[test]
@@ -369,7 +369,7 @@ mod tests {
)]),
);
assert_eq!(machine.import_keys(export, false, |_, _| {}).await?, keys,);
assert_eq!(machine.import_keys(export, false, |_, _| {}).await?, keys);
let export = vec![session.export_at_index(10).await];
assert_eq!(
@@ -379,7 +379,7 @@ mod tests {
let better_export = vec![session.export().await];
assert_eq!(machine.import_keys(better_export, false, |_, _| {}).await?, keys,);
assert_eq!(machine.import_keys(better_export, false, |_, _| {}).await?, keys);
let another_session = machine.create_inbound_session(room_id).await?;
let export = vec![another_session.export_at_index(10).await];
@@ -396,7 +396,7 @@ mod tests {
)]),
);
assert_eq!(machine.import_keys(export, false, |_, _| {}).await?, keys,);
assert_eq!(machine.import_keys(export, false, |_, _| {}).await?, keys);
Ok(())
}
@@ -48,7 +48,7 @@ use crate::{
requests::{OutgoingRequest, ToDeviceRequest},
session_manager::GroupSessionCache,
store::{Changes, CryptoStoreError, SecretImportError, Store},
types::events::secret_send::SecretSendEvent,
types::events::{secret_send::SecretSendEvent, EventType},
Device,
};
@@ -435,7 +435,8 @@ impl GossipMachine {
let request = ToDeviceRequest::new(
device.user_id(),
device.device_id().to_owned(),
AnyToDeviceEventContent::RoomEncrypted(content),
content.event_type(),
content.cast(),
);
let request = OutgoingRequest {
@@ -459,7 +460,8 @@ impl GossipMachine {
let request = ToDeviceRequest::new(
device.user_id(),
device.device_id().to_owned(),
AnyToDeviceEventContent::RoomEncrypted(content),
content.event_type(),
content.cast(),
);
let request = OutgoingRequest {
@@ -589,9 +591,10 @@ impl GossipMachine {
room_id: &RoomId,
sender_key: &str,
session_id: &str,
algorithm: &EventEncryptionAlgorithm,
) -> Result<(Option<OutgoingRequest>, OutgoingRequest), CryptoStoreError> {
let key_info = RequestedKeyInfo::new(
EventEncryptionAlgorithm::MegolmV1AesSha2,
algorithm.to_owned(),
room_id.to_owned(),
sender_key.to_owned(),
session_id.to_owned(),
@@ -666,9 +669,10 @@ impl GossipMachine {
room_id: &RoomId,
sender_key: &str,
session_id: &str,
algorithm: &EventEncryptionAlgorithm,
) -> Result<bool, CryptoStoreError> {
let key_info = RequestedKeyInfo::new(
EventEncryptionAlgorithm::MegolmV1AesSha2,
algorithm.to_owned(),
room_id.to_owned(),
sender_key.to_owned(),
session_id.to_owned(),
@@ -950,10 +954,9 @@ mod tests {
device_id,
events::{
forwarded_room_key::ToDeviceForwardedRoomKeyEventContent,
room::encrypted::ToDeviceRoomEncryptedEventContent,
room_key_request::ToDeviceRoomKeyRequestEventContent,
secret::request::{RequestAction, SecretName, ToDeviceSecretRequestEventContent},
AnyToDeviceEvent, ToDeviceEvent,
AnyToDeviceEvent, ToDeviceEvent as RumaToDeviceEvent,
},
room_id,
to_device::DeviceIdOrAllDevices,
@@ -966,6 +969,8 @@ mod tests {
olm::{Account, PrivateCrossSigningIdentity, ReadOnlyAccount},
session_manager::GroupSessionCache,
store::{Changes, CryptoStore, MemoryStore, Store},
types::events::{room::encrypted::ToDeviceEncryptedEventContent, ToDeviceEvent},
utilities::json_convert,
verification::VerificationMachine,
OutgoingRequests,
};
@@ -1064,7 +1069,12 @@ mod tests {
assert!(machine.outgoing_to_device_requests().await.unwrap().is_empty());
let (cancel, request) = machine
.request_key(session.room_id(), &session.sender_key, session.session_id())
.request_key(
session.room_id(),
&session.sender_key,
session.session_id(),
session.algorithm(),
)
.await
.unwrap();
@@ -1073,7 +1083,12 @@ mod tests {
machine.mark_outgoing_request_as_sent(&request.request_id).await.unwrap();
let (cancel, _) = machine
.request_key(session.room_id(), &session.sender_key, session.session_id())
.request_key(
session.room_id(),
&session.sender_key,
session.session_id(),
session.algorithm(),
)
.await
.unwrap();
@@ -1099,6 +1114,7 @@ mod tests {
session.room_id(),
&session.sender_key,
session.session_id(),
session.algorithm(),
)
.await
.unwrap();
@@ -1110,6 +1126,7 @@ mod tests {
session.room_id(),
&session.sender_key,
session.session_id(),
session.algorithm(),
)
.await
.unwrap();
@@ -1141,6 +1158,7 @@ mod tests {
session.room_id(),
&session.sender_key,
session.session_id(),
session.algorithm(),
)
.await
.unwrap();
@@ -1155,7 +1173,7 @@ mod tests {
let content: ToDeviceForwardedRoomKeyEventContent = export.try_into().unwrap();
let event = ToDeviceEvent { sender: alice_id().to_owned(), content };
let event = RumaToDeviceEvent { sender: alice_id().to_owned(), content };
assert!(
machine
@@ -1189,6 +1207,7 @@ mod tests {
session.room_id(),
&session.sender_key,
session.session_id(),
session.algorithm(),
)
.await
.unwrap();
@@ -1202,7 +1221,7 @@ mod tests {
let content: ToDeviceForwardedRoomKeyEventContent = export.try_into().unwrap();
let event = ToDeviceEvent { sender: alice_id().to_owned(), content };
let event = RumaToDeviceEvent { sender: alice_id().to_owned(), content };
let second_session =
machine.receive_forwarded_room_key(&session.sender_key, &event).await.unwrap();
@@ -1213,7 +1232,7 @@ mod tests {
let content: ToDeviceForwardedRoomKeyEventContent = export.try_into().unwrap();
let event = ToDeviceEvent { sender: alice_id().to_owned(), content };
let event = RumaToDeviceEvent { sender: alice_id().to_owned(), content };
let second_session =
machine.receive_forwarded_room_key(&session.sender_key, &event).await.unwrap();
@@ -1238,7 +1257,7 @@ mod tests {
);
own_device.set_trust_state(LocalTrust::Verified);
// Now we do want to share the keys.
assert!(machine.should_share_key(&own_device, &inbound).await.is_ok());
machine.should_share_key(&own_device, &inbound).await.unwrap();
let bob_device = ReadOnlyDevice::from_account(&bob_account()).await;
machine.store.save_devices(&[bob_device]).await.unwrap();
@@ -1284,7 +1303,7 @@ mod tests {
bob_device.curve25519_key().unwrap(),
)
.await;
assert!(machine.should_share_key(&bob_device, &inbound).await.is_ok());
machine.should_share_key(&bob_device, &inbound).await.unwrap();
let (other_outbound, other_inbound) =
account.create_group_session_pair_with_defaults(room_id()).await;
@@ -1375,6 +1394,7 @@ mod tests {
room_id(),
&bob_account.identity_keys.curve25519.to_base64(),
group_session.session_id(),
&group_session.settings().algorithm,
)
.await
.unwrap();
@@ -1406,7 +1426,7 @@ mod tests {
alice_machine.mark_outgoing_request_as_sent(id).await.unwrap();
let event = ToDeviceEvent { sender: alice_id().to_owned(), content };
let event = RumaToDeviceEvent { sender: alice_id().to_owned(), content };
// Bob doesn't have any outgoing requests.
assert!(bob_machine.outgoing_requests.is_empty());
@@ -1431,11 +1451,13 @@ mod tests {
.unwrap()
.get(&DeviceIdOrAllDevices::DeviceId(alice_device_id().to_owned()))
.unwrap();
let content: ToDeviceRoomEncryptedEventContent = content.deserialize_as().unwrap();
let content: ToDeviceEncryptedEventContent = content.deserialize_as().unwrap();
bob_machine.mark_outgoing_request_as_sent(id).await.unwrap();
let event = ToDeviceEvent { sender: bob_id().to_owned(), content };
let event =
ToDeviceEvent { sender: bob_id().to_owned(), content, other: Default::default() };
let event = json_convert(&event).unwrap();
// Check that alice doesn't have the session.
assert!(alice_machine
@@ -1488,7 +1510,7 @@ mod tests {
alice_machine.store.save_sessions(&[alice_session]).await.unwrap();
let event = ToDeviceEvent {
let event = RumaToDeviceEvent {
sender: bob_account.user_id().to_owned(),
content: ToDeviceSecretRequestEventContent::new(
RequestAction::Request(SecretName::CrossSigningMasterKey),
@@ -1517,7 +1539,7 @@ mod tests {
alice_machine.collect_incoming_key_requests().await.unwrap();
assert!(alice_machine.outgoing_requests.is_empty());
let event = ToDeviceEvent {
let event = RumaToDeviceEvent {
sender: alice_id().to_owned(),
content: ToDeviceSecretRequestEventContent::new(
RequestAction::Request(SecretName::CrossSigningMasterKey),
@@ -1577,6 +1599,7 @@ mod tests {
room_id(),
&bob_account.identity_keys.curve25519.to_base64(),
group_session.session_id(),
&group_session.settings().algorithm,
)
.await
.unwrap();
@@ -1608,7 +1631,7 @@ mod tests {
alice_machine.mark_outgoing_request_as_sent(id).await.unwrap();
let event = ToDeviceEvent { sender: alice_id().to_owned(), content };
let event = RumaToDeviceEvent { sender: alice_id().to_owned(), content };
// Bob doesn't have any outgoing requests.
assert!(bob_machine.outgoing_to_device_requests().await.unwrap().is_empty());
@@ -1653,11 +1676,13 @@ mod tests {
.unwrap()
.get(&DeviceIdOrAllDevices::DeviceId(alice_device_id().to_owned()))
.unwrap();
let content: ToDeviceRoomEncryptedEventContent = content.deserialize_as().unwrap();
let content: ToDeviceEncryptedEventContent = content.deserialize_as().unwrap();
bob_machine.mark_outgoing_request_as_sent(id).await.unwrap();
let event = ToDeviceEvent { sender: bob_id().to_owned(), content };
let event =
ToDeviceEvent { sender: bob_id().to_owned(), content, other: Default::default() };
let event = json_convert(&event).unwrap();
// Check that alice doesn't have the session.
assert!(alice_machine
@@ -180,10 +180,11 @@ impl GossipRequest {
}
};
let request = ToDeviceRequest::new(
let request = ToDeviceRequest::with_id(
&self.request_recipient,
DeviceIdOrAllDevices::AllDevices,
content,
TransactionId::new(),
);
OutgoingRequest { request_id: request.txn_id.clone(), request: Arc::new(request.into()) }
@@ -28,9 +28,9 @@ use ruma::{
api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest,
events::{
forwarded_room_key::ToDeviceForwardedRoomKeyEventContent,
key::verification::VerificationMethod, room::encrypted::ToDeviceRoomEncryptedEventContent,
AnyToDeviceEventContent,
key::verification::VerificationMethod, AnyToDeviceEventContent,
},
serde::Raw,
DeviceId, DeviceKeyAlgorithm, DeviceKeyId, EventEncryptionAlgorithm, OwnedDeviceId,
OwnedDeviceKeyId, UserId,
};
@@ -46,7 +46,10 @@ use crate::{
identities::{ReadOnlyOwnUserIdentity, ReadOnlyUserIdentities},
olm::{InboundGroupSession, Session, SignedJsonObject, VerifyJson},
store::{Changes, CryptoStore, DeviceChanges, Result as StoreResult},
types::{DeviceKey, DeviceKeys, Signatures, SignedKey},
types::{
events::room::encrypted::ToDeviceEncryptedEventContent, DeviceKey, DeviceKeys, Signatures,
SignedKey,
},
verification::VerificationMachine,
OutgoingVerificationRequest, ReadOnlyAccount, Sas, ToDeviceRequest, VerificationRequest,
};
@@ -255,7 +258,7 @@ impl Device {
pub(crate) async fn encrypt(
&self,
content: AnyToDeviceEventContent,
) -> OlmResult<(Session, ToDeviceRoomEncryptedEventContent)> {
) -> OlmResult<(Session, Raw<ToDeviceEncryptedEventContent>)> {
self.inner.encrypt(self.verification_machine.store.inner(), content).await
}
@@ -265,7 +268,7 @@ impl Device {
&self,
session: InboundGroupSession,
message_index: Option<u32>,
) -> OlmResult<(Session, ToDeviceRoomEncryptedEventContent)> {
) -> OlmResult<(Session, Raw<ToDeviceEncryptedEventContent>)> {
let export = if let Some(index) = message_index {
session.export_at_index(index).await
} else {
@@ -512,7 +515,7 @@ impl ReadOnlyDevice {
&self,
store: &dyn CryptoStore,
content: AnyToDeviceEventContent,
) -> OlmResult<(Session, ToDeviceRoomEncryptedEventContent)> {
) -> OlmResult<(Session, Raw<ToDeviceEncryptedEventContent>)> {
let sender_key = if let Some(k) = self.curve25519_key() {
k
} else {
@@ -14,7 +14,6 @@
use std::{
collections::{BTreeMap, BTreeSet, HashSet},
convert::TryFrom,
ops::Deref,
sync::Arc,
time::Duration,
@@ -623,7 +622,7 @@ impl IdentityManager {
}
if let Err(e) = self.store.update_tracked_user(user, true).await {
warn!("Error storing users for tracking {}", e);
warn!("Error storing users for tracking: {e}");
}
}
}
@@ -861,7 +860,7 @@ pub(crate) mod tests {
manager.receive_keys_query_response(&other_key_query()).await.unwrap();
assert!(task.await.unwrap().is_ok());
task.await.unwrap().unwrap();
let devices = manager.store.get_user_devices(other_user).await.unwrap();
assert_eq!(devices.devices().count(), 1);
@@ -875,7 +874,7 @@ pub(crate) mod tests {
let identity = manager.store.get_user_identity(other_user).await.unwrap().unwrap();
let identity = identity.other().unwrap();
assert!(identity.is_device_signed(&device).is_ok())
identity.is_device_signed(&device).unwrap();
}
#[async_test]
@@ -899,7 +898,7 @@ pub(crate) mod tests {
let identity = manager.store.get_user_identity(other_user).await.unwrap().unwrap();
let identity = identity.other().unwrap();
assert!(identity.is_device_signed(&device).is_ok())
identity.is_device_signed(&device).unwrap();
}
#[async_test]
@@ -1014,8 +1014,8 @@ pub(crate) mod tests {
let identity = get_own_identity();
let (first, second) = device(&response);
assert!(identity.is_device_signed(&first).is_err());
assert!(identity.is_device_signed(&second).is_ok());
identity.is_device_signed(&first).unwrap_err();
identity.is_device_signed(&second).unwrap();
let private_identity =
Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(second.user_id())));
+130 -104
View File
@@ -35,12 +35,7 @@ use ruma::{
},
assign,
events::{
room::encrypted::{
EncryptedEventScheme, MegolmV1AesSha2Content, OriginalSyncRoomEncryptedEvent,
RoomEncryptedEventContent, ToDeviceRoomEncryptedEvent,
},
secret::request::SecretName,
AnyMessageLikeEvent, AnyRoomEvent, MessageLikeEventContent,
secret::request::SecretName, AnyMessageLikeEvent, AnyRoomEvent, MessageLikeEventContent,
},
serde::Raw,
DeviceId, DeviceKeyAlgorithm, OwnedDeviceKeyId, OwnedTransactionId, OwnedUserId, RoomId,
@@ -69,6 +64,10 @@ use crate::{
},
types::{
events::{
room::encrypted::{
EncryptedEvent, EncryptedToDeviceEvent, RoomEncryptedEventContent,
RoomEventEncryptionScheme, SupportedEventEncryptionSchemes,
},
room_key::{RoomKeyContent, RoomKeyEvent},
ToDeviceEvents,
},
@@ -538,7 +537,7 @@ impl OlmMachine {
/// * `event` - The to-device event that should be decrypted.
async fn decrypt_to_device_event(
&self,
event: &ToDeviceRoomEncryptedEvent,
event: &EncryptedToDeviceEvent,
) -> OlmResult<OlmDecryptionInfo> {
let mut decrypted = self.account.decrypt_to_device_event(event).await?;
// Handle the decrypted event, e.g. fetch out Megolm sessions out of
@@ -555,6 +554,15 @@ impl OlmMachine {
signing_key: &str,
event: &RoomKeyEvent,
) -> OlmResult<Option<InboundGroupSession>> {
let unsupported_warning = || {
warn!(
sender = %event.sender,
sender_key = sender_key,
algorithm = %event.algorithm(),
"Received room key with unsupported key algorithm",
);
};
match &event.content {
RoomKeyContent::MegolmV1AesSha2(content) => {
let session = InboundGroupSession::new(
@@ -562,26 +570,14 @@ impl OlmMachine {
signing_key,
&content.room_id,
&content.session_key,
event.algorithm(),
None,
);
info!(
sender = %event.sender,
sender_key = sender_key,
room_id = %content.room_id,
session_id = session.session_id(),
"Received a new room key",
);
Ok(Some(session))
}
RoomKeyContent::Unknown(content) => {
warn!(
sender = %event.sender,
sender_key = sender_key,
algorithm = ?content.algorithm,
"Received room key with unsupported key algorithm",
);
RoomKeyContent::Unknown(_) => {
unsupported_warning();
Ok(None)
}
}
@@ -636,7 +632,7 @@ impl OlmMachine {
&self,
room_id: &RoomId,
content: impl MessageLikeEventContent,
) -> MegolmResult<RoomEncryptedEventContent> {
) -> MegolmResult<Raw<RoomEncryptedEventContent>> {
let event_type = content.event_type().to_string();
let content = serde_json::to_value(&content)?;
self.encrypt_room_event_raw(room_id, content, &event_type).await
@@ -666,7 +662,7 @@ impl OlmMachine {
room_id: &RoomId,
content: Value,
event_type: &str,
) -> MegolmResult<RoomEncryptedEventContent> {
) -> MegolmResult<Raw<RoomEncryptedEventContent>> {
self.group_session_manager.encrypt(room_id, content, event_type).await
}
@@ -988,11 +984,13 @@ impl OlmMachine {
/// * `session_id` - The id that uniquely identifies the session.
pub async fn request_room_key(
&self,
event: &OriginalSyncRoomEncryptedEvent,
event: &Raw<EncryptedEvent>,
room_id: &RoomId,
) -> MegolmResult<(Option<OutgoingRequest>, OutgoingRequest)> {
let content = match &event.content.scheme {
EncryptedEventScheme::MegolmV1AesSha2(c) => c,
let event = event.deserialize()?;
let content: SupportedEventEncryptionSchemes<'_> = match &event.content.scheme {
RoomEventEncryptionScheme::MegolmV1AesSha2(c) => c.into(),
_ => return Err(EventError::UnsupportedAlgorithm.into()),
};
@@ -1001,8 +999,9 @@ impl OlmMachine {
.request_key(
room_id,
#[allow(deprecated)]
&content.sender_key,
&content.session_id,
&content.sender_key().to_base64(),
content.session_id(),
&content.algorithm(),
)
.await?)
}
@@ -1043,19 +1042,19 @@ impl OlmMachine {
})
}
async fn decrypt_megolm_v1_event(
async fn decrypt_megolm_events(
&self,
room_id: &RoomId,
event: &OriginalSyncRoomEncryptedEvent,
content: &MegolmV1AesSha2Content,
event: &EncryptedEvent,
content: &SupportedEventEncryptionSchemes<'_>,
) -> MegolmResult<RoomEvent> {
if let Some(session) = self
.store
.get_inbound_group_session(
room_id,
#[allow(deprecated)]
&content.sender_key,
&content.session_id,
&content.sender_key().to_base64(),
content.session_id(),
)
.await?
{
@@ -1071,6 +1070,7 @@ impl OlmMachine {
room_id = room_id.as_str(),
session_id = session.session_id(),
sender_key = session.sender_key(),
algorithm = %session.algorithm(),
"Successfully decrypted a room event"
);
@@ -1084,6 +1084,7 @@ impl OlmMachine {
room_id = room_id.as_str(),
session_id = session.session_id(),
sender_key = session.sender_key(),
algorithm = %session.algorithm(),
error = ?e,
"Event was successfully decrypted but has an invalid format"
);
@@ -1095,7 +1096,7 @@ impl OlmMachine {
&session,
&event.sender,
#[allow(deprecated)]
&content.device_id,
content.device_id(),
)
.await?;
@@ -1105,8 +1106,9 @@ impl OlmMachine {
.create_outgoing_key_request(
room_id,
#[allow(deprecated)]
&content.sender_key,
&content.session_id,
&content.sender_key().to_base64(),
content.session_id(),
&content.algorithm(),
)
.await?;
@@ -1123,49 +1125,50 @@ impl OlmMachine {
/// * `room_id` - The ID of the room where the event was sent to.
pub async fn decrypt_room_event(
&self,
event: &OriginalSyncRoomEncryptedEvent,
event: &Raw<EncryptedEvent>,
room_id: &RoomId,
) -> MegolmResult<RoomEvent> {
match &event.content.scheme {
EncryptedEventScheme::MegolmV1AesSha2(c) => {
match self.decrypt_megolm_v1_event(room_id, event, c).await {
Ok(r) => Ok(r),
Err(e) => {
#[allow(deprecated)]
if let MegolmError::MissingRoomKey = e {
// TODO log the withheld reason if we have one.
debug!(
sender = event.sender.as_str(),
room_id = room_id.as_str(),
sender_key = c.sender_key.as_str(),
session_id = c.session_id.as_str(),
"Failed to decrypt a room event, the room key is missing"
);
} else {
warn!(
sender = event.sender.as_str(),
room_id = room_id.as_str(),
sender_key = c.sender_key.as_str(),
session_id = c.session_id.as_str(),
error = ?e,
"Failed to decrypt a room event"
);
}
let event = event.deserialize()?;
Err(e)
}
}
}
algorithm => {
let content = match &event.content.scheme {
RoomEventEncryptionScheme::MegolmV1AesSha2(c) => c.into(),
RoomEventEncryptionScheme::Unknown(c) => {
warn!(
sender = event.sender.as_str(),
room_id = room_id.as_str(),
?algorithm,
algorithm = %c.algorithm,
"Received an encrypted room event with an unsupported algorithm"
);
Err(EventError::UnsupportedAlgorithm.into())
return Err(EventError::UnsupportedAlgorithm.into());
}
}
};
self.decrypt_megolm_events(room_id, &event, &content).await.map_err(|e| {
if let MegolmError::MissingRoomKey = e {
// TODO log the withheld reason if we have one.
debug!(
sender = event.sender.as_str(),
room_id = room_id.as_str(),
sender_key = content.sender_key().to_base64(),
session_id = content.session_id(),
algorithm = %content.algorithm(),
"Failed to decrypt a room event, the room key is missing"
);
} else {
warn!(
sender = event.sender.as_str(),
room_id = room_id.as_str(),
sender_key = content.sender_key().to_base64(),
session_id = content.session_id(),
algorithm = %content.algorithm(),
error = ?e,
"Failed to decrypt a room event"
);
}
e
})
}
/// Update the tracked users.
@@ -1211,7 +1214,6 @@ impl OlmMachine {
/// # Example
///
/// ```
/// # use std::convert::TryFrom;
/// # use matrix_sdk_crypto::OlmMachine;
/// # use ruma::{device_id, user_id};
/// # use futures::executor::block_on;
@@ -1269,7 +1271,6 @@ impl OlmMachine {
/// # Example
///
/// ```
/// # use std::convert::TryFrom;
/// # use matrix_sdk_crypto::OlmMachine;
/// # use ruma::{device_id, user_id};
/// # use futures::executor::block_on;
@@ -1372,24 +1373,35 @@ impl OlmMachine {
let mut keys = BTreeMap::new();
for (i, key) in exported_keys.into_iter().enumerate() {
let session = InboundGroupSession::from_export(key);
match InboundGroupSession::from_export(&key) {
Ok(session) => {
// Only import the session if we didn't have this session or if it's
// a better version of the same session, that is the first known
// index is lower.
if !existing_sessions.has_better_session(&session) {
#[cfg(feature = "backups_v1")]
if from_backup {
session.mark_as_backed_up();
}
// Only import the session if we didn't have this session or if it's
// a better version of the same session, that is the first known
// index is lower.
if !existing_sessions.has_better_session(&session) {
#[cfg(feature = "backups_v1")]
if from_backup {
session.mark_as_backed_up();
keys.entry(session.room_id().to_owned())
.or_insert_with(BTreeMap::new)
.entry(session.sender_key().to_owned())
.or_insert_with(BTreeSet::new)
.insert(session.session_id().to_owned());
sessions.push(session);
}
}
Err(e) => {
warn!(
sender_key= key.sender_key,
room_id = %key.room_id,
session_id = key.session_id,
error = ?e,
"Couldn't import a room key from a file export."
);
}
keys.entry(session.room_id().to_owned())
.or_insert_with(BTreeMap::new)
.entry(session.sender_key().to_owned())
.or_insert_with(BTreeSet::new)
.insert(session.session_id().to_owned());
sessions.push(session);
}
progress_listener(i, total_count);
@@ -1554,7 +1566,7 @@ pub(crate) mod testing {
#[cfg(test)]
pub(crate) mod tests {
use std::{collections::BTreeMap, convert::TryInto, iter, sync::Arc};
use std::{collections::BTreeMap, iter, sync::Arc};
use matrix_sdk_test::{async_test, test_json};
use ruma::{
@@ -1572,26 +1584,29 @@ pub(crate) mod tests {
dummy::ToDeviceDummyEventContent,
key::verification::VerificationMethod,
room::{
encrypted::ToDeviceRoomEncryptedEventContent,
encrypted::OriginalSyncRoomEncryptedEvent,
message::{MessageType, RoomMessageEventContent},
},
AnyMessageLikeEvent, AnyMessageLikeEventContent, AnyRoomEvent, AnyToDeviceEvent,
AnyToDeviceEventContent, MessageLikeEvent, MessageLikeUnsigned,
OriginalMessageLikeEvent, OriginalSyncMessageLikeEvent, ToDeviceEvent,
OriginalMessageLikeEvent,
},
room_id,
serde::Raw,
uint, user_id, DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch,
OwnedDeviceKeyId, UserId,
};
use serde_json::value::to_raw_value;
use vodozemac::Ed25519PublicKey;
use super::testing::response_from_file;
use crate::{
machine::OlmMachine,
olm::VerifyJson,
types::{DeviceKeys, SignedKey},
types::{
events::{room::encrypted::ToDeviceEncryptedEventContent, ToDeviceEvent},
DeviceKeys, SignedKey,
},
utilities::json_convert,
verification::tests::{outgoing_request_to_event, request_to_event},
EncryptionSettings, ReadOnlyDevice, ToDeviceRequest,
};
@@ -1625,7 +1640,7 @@ pub(crate) mod tests {
fn to_device_requests_to_content(
requests: Vec<Arc<ToDeviceRequest>>,
) -> ToDeviceRoomEncryptedEventContent {
) -> ToDeviceEncryptedEventContent {
let to_device_request = &requests[0];
to_device_request
@@ -1706,7 +1721,11 @@ pub(crate) mod tests {
.unwrap();
alice.store.save_sessions(&[session]).await.unwrap();
let event = ToDeviceEvent { sender: alice.user_id().to_owned(), content };
let event = ToDeviceEvent {
sender: alice.user_id().to_owned(),
content: content.deserialize_as().unwrap(),
other: Default::default(),
};
let decrypted = bob.decrypt_to_device_event(&event).await.unwrap();
bob.store.save_sessions(&[decrypted.session.session()]).await.unwrap();
@@ -1749,7 +1768,7 @@ pub(crate) mod tests {
&DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, machine.device_id()),
&device_keys,
);
assert!(ret.is_ok());
ret.unwrap();
}
#[async_test]
@@ -1782,7 +1801,7 @@ pub(crate) mod tests {
&DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, machine.device_id()),
&device_keys,
);
assert!(ret.is_err());
ret.unwrap_err();
}
#[async_test]
@@ -1832,7 +1851,7 @@ pub(crate) mod tests {
&DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, machine.device_id()),
&one_time_key,
);
assert!(ret.is_ok());
ret.unwrap();
let device_keys: DeviceKeys = request.device_keys.unwrap().deserialize_as().unwrap();
@@ -1841,7 +1860,7 @@ pub(crate) mod tests {
&DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, machine.device_id()),
&device_keys,
);
assert!(ret.is_ok());
ret.unwrap();
let mut response = keys_upload_response();
response.one_time_key_counts.insert(
@@ -1928,7 +1947,10 @@ pub(crate) mod tests {
.encrypt(AnyToDeviceEventContent::Dummy(ToDeviceDummyEventContent::new()))
.await
.unwrap()
.1,
.1
.deserialize_as()
.unwrap(),
other: Default::default(),
};
let event = bob.decrypt_to_device_event(&event).await.unwrap().event.deserialize().unwrap();
@@ -1954,8 +1976,9 @@ pub(crate) mod tests {
let event = ToDeviceEvent {
sender: alice.user_id().to_owned(),
content: to_device_requests_to_content(to_device_requests),
other: Default::default(),
};
let event = Raw::from_json(to_raw_value(&event).unwrap());
let event = json_convert(&event).unwrap();
let alice_session =
alice.group_session_manager.get_outbound_group_session(room_id).unwrap();
@@ -2002,6 +2025,7 @@ pub(crate) mod tests {
let event = ToDeviceEvent {
sender: alice.user_id().to_owned(),
content: to_device_requests_to_content(to_device_requests),
other: Default::default(),
};
let group_session =
@@ -2017,14 +2041,16 @@ pub(crate) mod tests {
.await
.unwrap();
let event = OriginalSyncMessageLikeEvent {
let event = OriginalSyncRoomEncryptedEvent {
event_id: event_id!("$xxxxx:example.org").to_owned(),
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
sender: alice.user_id().to_owned(),
content: encrypted_content,
content: encrypted_content.deserialize_as().unwrap(),
unsigned: MessageLikeUnsigned::default(),
};
let event = json_convert(&event).unwrap();
let decrypted_event =
bob.decrypt_room_event(&event, room_id).await.unwrap().event.deserialize().unwrap();
+92 -109
View File
@@ -14,7 +14,6 @@
use std::{
collections::{BTreeMap, HashMap},
convert::TryInto,
fmt,
ops::Deref,
sync::{
@@ -29,12 +28,7 @@ use ruma::{
upload_keys,
upload_signatures::v3::{Request as SignatureUploadRequest, SignedKeys},
},
events::{
room::encrypted::{
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, ToDeviceRoomEncryptedEvent,
},
AnyToDeviceEvent, OlmV1Keys,
},
events::{AnyToDeviceEvent, OlmV1Keys},
serde::Raw,
DeviceId, DeviceKeyAlgorithm, DeviceKeyId, EventEncryptionAlgorithm, OwnedDeviceId,
OwnedDeviceKeyId, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UInt, UserId,
@@ -57,7 +51,12 @@ use crate::{
identities::{MasterPubkey, ReadOnlyDevice},
requests::UploadSigningKeysRequest,
store::{Changes, Store},
types::{CrossSigningKey, DeviceKeys, OneTimeKey, SignedKey},
types::{
events::room::encrypted::{
EncryptedToDeviceEvent, OlmV1Curve25519AesSha2Content, ToDeviceEncryptedEventContent,
},
CrossSigningKey, DeviceKeys, OneTimeKey, SignedKey,
},
utilities::encode,
CryptoStoreError, OlmError, SignatureError,
};
@@ -118,14 +117,17 @@ pub struct OlmMessageHash {
}
impl OlmMessageHash {
fn new(sender_key: &str, message_type: u8, ciphertext: &str) -> Self {
fn new(sender_key: Curve25519PublicKey, ciphertext: &OlmMessage) -> Self {
let (message_type, ciphertext) = ciphertext.clone().to_parts();
let sender_key = sender_key.to_base64();
let sha = Sha256::new()
.chain_update(sender_key)
.chain_update(&[message_type])
.chain_update(sender_key.as_bytes())
.chain_update(&[message_type as u8])
.chain_update(&ciphertext)
.finalize();
Self { sender_key: sender_key.to_owned(), hash: encode(sha.as_slice()) }
Self { sender_key, hash: encode(sha.as_slice()) }
}
}
@@ -138,100 +140,86 @@ impl Deref for Account {
}
impl Account {
fn parse_message(
sender_key: &str,
message_type: UInt,
ciphertext: String,
) -> Result<(OlmMessage, OlmMessageHash), EventError> {
let message_type: u8 = message_type
.try_into()
.map_err(|_| EventError::UnsupportedOlmType(message_type.into()))?;
let message_hash = OlmMessageHash::new(sender_key, message_type, &ciphertext);
let message = OlmMessage::from_parts(message_type.into(), &ciphertext)
.map_err(|_| EventError::UnsupportedOlmType(message_type.into()))?;
Ok((message, message_hash))
}
pub async fn save(&self) -> Result<(), CryptoStoreError> {
self.store.save_account(self.inner.clone()).await
}
async fn decrypt_olm_helper(
&self,
sender: &UserId,
sender_key: Curve25519PublicKey,
ciphertext: &OlmMessage,
) -> OlmResult<OlmDecryptionInfo> {
let message_hash = OlmMessageHash::new(sender_key, ciphertext);
match self.decrypt_olm_message(sender, sender_key, ciphertext).await {
Ok((session, event, signing_key)) => Ok(OlmDecryptionInfo {
sender: sender.to_owned(),
session,
message_hash,
event,
signing_key,
sender_key: sender_key.to_base64(),
inbound_group_session: None,
}),
Err(OlmError::SessionWedged(user_id, sender_key)) => {
if self.store.is_message_known(&message_hash).await? {
info!(
sender = sender.as_str(),
sender_key, "An Olm message got replayed, decryption failed"
);
Err(OlmError::ReplayedMessage(user_id, sender_key))
} else {
Err(OlmError::SessionWedged(user_id, sender_key))
}
}
Err(e) => Err(e),
}
}
async fn decrypt_olm_v1(
&self,
sender: &UserId,
content: &OlmV1Curve25519AesSha2Content,
) -> OlmResult<OlmDecryptionInfo> {
let identity_keys = self.inner.identity_keys();
// Try to find a ciphertext that was meant for our device.
if let Some(ciphertext) = content.ciphertext.get(&identity_keys.curve25519.to_base64()) {
let (message, message_hash) = match Self::parse_message(
&content.sender_key,
ciphertext.message_type,
ciphertext.body.clone(),
) {
Ok(m) => m,
Err(e) => {
warn!(error = ?e, "Encrypted to-device event isn't valid");
return Err(e.into());
}
};
// Decrypt the OlmMessage and get a Ruma event out of it.
match self.decrypt_olm_message(sender, &content.sender_key, message).await {
Ok((session, event, signing_key)) => Ok(OlmDecryptionInfo {
sender: sender.to_owned(),
session,
message_hash,
event,
signing_key,
sender_key: content.sender_key.clone(),
inbound_group_session: None,
}),
Err(OlmError::SessionWedged(user_id, sender_key)) => {
if self.store.is_message_known(&message_hash).await? {
info!(
sender = sender.as_str(),
sender_key = content.sender_key.as_str(),
"An Olm message got replayed, decryption failed"
);
Err(OlmError::ReplayedMessage(user_id, sender_key))
} else {
Err(OlmError::SessionWedged(user_id, sender_key))
}
}
Err(e) => Err(e),
}
} else {
if content.recipient_key != self.identity_keys().curve25519 {
warn!(
sender = sender.as_str(),
sender_key = content.sender_key.as_str(),
sender_key = content.sender_key.to_base64(),
"Olm event doesn't contain a ciphertext for our key"
);
Err(EventError::MissingCiphertext.into())
} else {
self.decrypt_olm_helper(sender, content.sender_key, &content.ciphertext).await
}
}
pub(crate) async fn decrypt_to_device_event(
&self,
event: &ToDeviceRoomEncryptedEvent,
event: &EncryptedToDeviceEvent,
) -> OlmResult<OlmDecryptionInfo> {
trace!(sender = event.sender.as_str(), "Decrypting a to-device event");
trace!(
sender = event.sender.as_str(),
algorithm = %event.content.algorithm(),
"Decrypting a to-device event"
);
if let EncryptedEventScheme::OlmV1Curve25519AesSha2(c) = &event.content.scheme {
self.decrypt_olm_v1(&event.sender, c).await
} else {
warn!(
sender = event.sender.as_str(),
algorithm = ?event.content.scheme,
"Error, unsupported encryption algorithm"
);
match &event.content {
ToDeviceEncryptedEventContent::OlmV1Curve25519AesSha2(c) => {
self.decrypt_olm_v1(&event.sender, c).await
}
ToDeviceEncryptedEventContent::Unknown(_) => {
warn!(
sender = event.sender.as_str(),
algorithm = %event.content.algorithm(),
"Error decrypting an to-device event, unsupported \
encryption algorithm"
);
Err(EventError::UnsupportedAlgorithm.into())
Err(EventError::UnsupportedAlgorithm.into())
}
}
}
@@ -258,10 +246,10 @@ impl Account {
/// with the given sender.
async fn decrypt_with_existing_sessions(
&self,
sender_key: &str,
sender_key: Curve25519PublicKey,
message: &OlmMessage,
) -> OlmResult<Option<(Session, String)>> {
let s = self.store.get_sessions(sender_key).await?;
let s = self.store.get_sessions(&sender_key.to_base64()).await?;
// We don't have any existing sessions, return early.
let sessions = if let Some(s) = s {
@@ -293,12 +281,12 @@ impl Account {
async fn decrypt_olm_message(
&self,
sender: &UserId,
sender_key: &str,
message: OlmMessage,
sender_key: Curve25519PublicKey,
message: &OlmMessage,
) -> OlmResult<(SessionType, Raw<AnyToDeviceEvent>, String)> {
// First try to decrypt using an existing session.
let (session, plaintext) = if let Some(d) =
self.decrypt_with_existing_sessions(sender_key, &message).await?
self.decrypt_with_existing_sessions(sender_key, message).await?
{
// Decryption succeeded, de-structure the session/plaintext out of
// the Option.
@@ -306,17 +294,17 @@ impl Account {
} else {
// Decryption failed with every known session, let's try to create a
// new session.
match &message {
match message {
// A new session can only be created using a pre-key message,
// return with an error if it isn't one.
OlmMessage::Normal(_) => {
warn!(
sender = sender.as_str(),
sender_key = sender_key,
sender_key = sender_key.to_base64(),
"Failed to decrypt a non-pre-key message with all \
available sessions",
);
return Err(OlmError::SessionWedged(sender.to_owned(), sender_key.to_owned()));
return Err(OlmError::SessionWedged(sender.to_owned(), sender_key.to_base64()));
}
OlmMessage::PreKey(m) => {
@@ -326,14 +314,14 @@ impl Account {
Err(e) => {
warn!(
sender = sender.as_str(),
sender_key = sender_key,
sender_key = sender_key.to_base64(),
error = ?e,
"Failed to create a new Olm session from a \
prekey message",
);
return Err(OlmError::SessionWedged(
sender.to_owned(),
sender_key.to_owned(),
sender_key.to_base64(),
));
}
};
@@ -356,7 +344,7 @@ impl Account {
trace!(
sender = sender.as_str(),
sender_key = sender_key,
sender_key = sender_key.to_base64(),
"Successfully decrypted an Olm message"
);
@@ -382,7 +370,7 @@ impl Account {
warn!(
sender = sender.as_str(),
sender_key = sender_key,
sender_key = sender_key.to_base64(),
error = ?e,
"A to-device message was successfully decrypted but \
parsing and checking the event fields failed"
@@ -577,9 +565,8 @@ impl ReadOnlyAccount {
// so.
if count != old_count {
debug!(
"Updated uploaded one-time key count {} -> {}.",
"Updated uploaded one-time key count {} -> {count}.",
self.uploaded_key_count(),
count
);
}
@@ -1017,10 +1004,9 @@ impl ReadOnlyAccount {
/// account.
pub async fn create_inbound_session(
&self,
their_identity_key: &str,
their_identity_key: Curve25519PublicKey,
message: &PreKeyMessage,
) -> Result<InboundCreationResult, SessionCreationError> {
let their_identity_key = Curve25519PublicKey::from_base64(their_identity_key)?;
let result = self.inner.lock().await.create_inbound_session(their_identity_key, message)?;
let now = SecondsSinceUnixEpoch::now();
@@ -1084,6 +1070,7 @@ impl ReadOnlyAccount {
&signing_key,
room_id,
&outbound.session_key().await,
outbound.settings().algorithm.to_owned(),
Some(visibility),
);
@@ -1122,20 +1109,16 @@ impl ReadOnlyAccount {
let message = our_session
.encrypt(&device, AnyToDeviceEventContent::Dummy(ToDeviceDummyEventContent::new()))
.await
.unwrap()
.deserialize()
.unwrap();
let content = if let EncryptedEventScheme::OlmV1Curve25519AesSha2(c) = message.scheme {
let content = if let ToDeviceEncryptedEventContent::OlmV1Curve25519AesSha2(c) = message {
c
} else {
panic!("Invalid encrypted event algorithm");
panic!("Invalid encrypted event algorithm {}", message.algorithm());
};
let own_ciphertext =
content.ciphertext.get(&other.identity_keys.curve25519.to_base64()).unwrap();
let message_type: u8 = own_ciphertext.message_type.try_into().unwrap();
let message = OlmMessage::from_parts(message_type.into(), &own_ciphertext.body).unwrap();
let prekey = if let OlmMessage::PreKey(m) = message.clone() {
let prekey = if let OlmMessage::PreKey(m) = content.ciphertext {
m
} else {
panic!("Wrong Olm message type");
@@ -1143,7 +1126,7 @@ impl ReadOnlyAccount {
let our_device = ReadOnlyDevice::from_account(self).await;
let other_session = other
.create_inbound_session(&our_device.curve25519_key().unwrap().to_base64(), &prekey)
.create_inbound_session(our_device.curve25519_key().unwrap(), &prekey)
.await
.unwrap();
@@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#![warn(missing_docs)]
use std::{
collections::BTreeMap,
fmt,
@@ -27,11 +25,7 @@ use matrix_sdk_common::locks::Mutex;
use ruma::{
events::{
forwarded_room_key::ToDeviceForwardedRoomKeyEventContent,
room::{
encrypted::{EncryptedEventScheme, OriginalSyncRoomEncryptedEvent},
history_visibility::HistoryVisibility,
},
AnyRoomEvent,
room::history_visibility::HistoryVisibility, AnyRoomEvent,
},
serde::Raw,
DeviceKeyAlgorithm, EventEncryptionAlgorithm, OwnedRoomId, RoomId,
@@ -41,13 +35,16 @@ use serde_json::Value;
use vodozemac::{
megolm::{
DecryptedMessage, DecryptionError, ExportedSessionKey, InboundGroupSession as InnerSession,
InboundGroupSessionPickle, MegolmMessage, SessionKeyDecodeError,
InboundGroupSessionPickle, MegolmMessage,
},
PickleError,
};
use super::{BackedUpRoomKey, ExportedRoomKey, SessionKey};
use crate::error::{EventError, MegolmResult};
use super::{BackedUpRoomKey, ExportedRoomKey, SessionCreationError, SessionKey};
use crate::{
error::{EventError, MegolmResult},
types::events::room::encrypted::{EncryptedEvent, RoomEventEncryptionScheme},
};
// TODO add creation times to the inbound group sessions so we can export
// sessions that were created between some time period, this should only be set
@@ -72,6 +69,7 @@ pub struct InboundGroupSession {
pub room_id: Arc<RoomId>,
forwarding_chains: Arc<Vec<String>>,
imported: bool,
algorithm: Arc<EventEncryptionAlgorithm>,
backed_up: Arc<AtomicBool>,
}
@@ -97,6 +95,7 @@ impl InboundGroupSession {
signing_key: &str,
room_id: &RoomId,
session_key: &SessionKey,
encryption_algorithm: EventEncryptionAlgorithm,
history_visibility: Option<HistoryVisibility>,
) -> Self {
let session = InnerSession::new(session_key);
@@ -108,14 +107,15 @@ impl InboundGroupSession {
InboundGroupSession {
inner: Arc::new(Mutex::new(session)),
session_id: session_id.into(),
history_visibility: history_visibility.into(),
sender_key: sender_key.to_owned().into(),
session_id: session_id.into(),
first_known_index,
sender_key: sender_key.to_owned().into(),
signing_keys: keys.into(),
room_id: room_id.into(),
forwarding_chains: Vec::new().into(),
imported: false,
algorithm: encryption_algorithm.into(),
backed_up: AtomicBool::new(false).into(),
}
}
@@ -127,16 +127,19 @@ impl InboundGroupSession {
/// previous [`export()`] call.
///
/// [`export()`]: #method.export
pub fn from_export(exported_session: ExportedRoomKey) -> Self {
Self::from(exported_session)
pub fn from_export(exported_session: &ExportedRoomKey) -> Result<Self, SessionCreationError> {
Self::try_from(exported_session)
}
#[allow(dead_code)]
fn from_backup(room_id: &RoomId, backup: BackedUpRoomKey) -> Self {
fn from_backup(
room_id: &RoomId,
backup: BackedUpRoomKey,
) -> Result<Self, SessionCreationError> {
let session = InnerSession::import(&backup.session_key);
let session_id = session.session_id();
Self::from_export(ExportedRoomKey {
Self::from_export(&ExportedRoomKey {
algorithm: backup.algorithm,
room_id: room_id.to_owned(),
sender_key: backup.sender_key,
@@ -159,10 +162,12 @@ impl InboundGroupSession {
pub fn from_forwarded_key(
sender_key: &str,
content: &ToDeviceForwardedRoomKeyEventContent,
) -> Result<Self, SessionKeyDecodeError> {
) -> Result<Self, SessionCreationError> {
let key = ExportedSessionKey::from_base64(&content.session_key)?;
let algorithm = EventEncryptionAlgorithm::from(content.algorithm.as_str());
let session = InnerSession::import(&key);
let first_known_index = session.first_known_index();
let mut forwarding_chains = content.forwarding_curve25519_key_chain.clone();
forwarding_chains.push(sender_key.to_owned());
@@ -182,6 +187,7 @@ impl InboundGroupSession {
forwarding_chains: forwarding_chains.into(),
imported: true,
backed_up: AtomicBool::new(false).into(),
algorithm: algorithm.into(),
})
}
@@ -203,6 +209,7 @@ impl InboundGroupSession {
imported: self.imported,
backed_up: self.backed_up(),
history_visibility: self.history_visibility.as_ref().clone(),
algorithm: (*self.algorithm).to_owned(),
}
}
@@ -293,6 +300,7 @@ impl InboundGroupSession {
room_id: (*pickle.room_id).into(),
forwarding_chains: pickle.forwarding_chains.into(),
backed_up: AtomicBool::from(pickle.backed_up).into(),
algorithm: pickle.algorithm.into(),
imported: pickle.imported,
})
}
@@ -307,6 +315,12 @@ impl InboundGroupSession {
&self.session_id
}
/// The algorithm that this inbound group session is using to decrypt
/// events.
pub fn algorithm(&self) -> &EventEncryptionAlgorithm {
&self.algorithm
}
/// Get the first message index we know how to decrypt.
pub fn first_known_index(&self) -> u32 {
self.first_known_index
@@ -339,18 +353,16 @@ impl InboundGroupSession {
/// # Arguments
///
/// * `event` - The event that should be decrypted.
pub async fn decrypt(
&self,
event: &OriginalSyncRoomEncryptedEvent,
) -> MegolmResult<(Raw<AnyRoomEvent>, u32)> {
let content = match &event.content.scheme {
EncryptedEventScheme::MegolmV1AesSha2(c) => c,
_ => return Err(EventError::UnsupportedAlgorithm.into()),
pub async fn decrypt(&self, event: &EncryptedEvent) -> MegolmResult<(Raw<AnyRoomEvent>, u32)> {
let decrypted = match &event.content.scheme {
RoomEventEncryptionScheme::MegolmV1AesSha2(c) => {
self.decrypt_helper(&c.ciphertext).await?
}
RoomEventEncryptionScheme::Unknown(_) => {
return Err(EventError::UnsupportedAlgorithm.into());
}
};
let message = MegolmMessage::from_base64(&content.ciphertext)?;
let decrypted = self.decrypt_helper(&message).await?;
let plaintext = String::from_utf8_lossy(&decrypted.plaintext);
let mut decrypted_value = serde_json::from_str::<Value>(&plaintext)?;
@@ -432,24 +444,34 @@ pub struct PickledInboundGroupSession {
pub backed_up: bool,
/// History visibility of the room when the session was created.
pub history_visibility: Option<HistoryVisibility>,
/// The algorithm of this inbound group session.
#[serde(default = "default_algorithm")]
pub algorithm: EventEncryptionAlgorithm,
}
impl From<ExportedRoomKey> for InboundGroupSession {
fn from(key: ExportedRoomKey) -> Self {
fn default_algorithm() -> EventEncryptionAlgorithm {
EventEncryptionAlgorithm::MegolmV1AesSha2
}
impl TryFrom<&ExportedRoomKey> for InboundGroupSession {
type Error = SessionCreationError;
fn try_from(key: &ExportedRoomKey) -> Result<Self, Self::Error> {
let session = InnerSession::import(&key.session_key);
let first_known_index = session.first_known_index();
InboundGroupSession {
Ok(InboundGroupSession {
inner: Mutex::new(session).into(),
session_id: key.session_id.into(),
sender_key: key.sender_key.into(),
session_id: key.session_id.to_owned().into(),
sender_key: key.sender_key.to_owned().into(),
history_visibility: None.into(),
first_known_index,
signing_keys: key.sender_claimed_keys.into(),
room_id: (*key.room_id).into(),
forwarding_chains: key.forwarding_curve25519_key_chain.into(),
signing_keys: key.sender_claimed_keys.to_owned().into(),
room_id: key.room_id.to_owned().into(),
forwarding_chains: key.forwarding_curve25519_key_chain.to_owned().into(),
imported: true,
algorithm: key.algorithm.to_owned().into(),
backed_up: AtomicBool::from(false).into(),
}
})
}
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::BTreeMap, convert::TryInto};
use std::collections::BTreeMap;
use ruma::{
events::forwarded_room_key::{
@@ -30,10 +30,22 @@ pub(crate) use outbound::ShareState;
pub use outbound::{
EncryptionSettings, GroupSession, OutboundGroupSession, PickledOutboundGroupSession, ShareInfo,
};
use thiserror::Error;
use vodozemac::megolm::SessionKeyDecodeError;
pub use vodozemac::megolm::{ExportedSessionKey, SessionKey};
use zeroize::Zeroize;
/// An error type for the creation of group sessions.
#[derive(Debug, Error)]
pub enum SessionCreationError {
/// The provided algorithm is not supported.
#[error("The provided algorithm is not supported: {0}")]
Algorithm(EventEncryptionAlgorithm),
/// The room key key couldn't be decoded.
#[error(transparent)]
Decode(#[from] SessionKeyDecodeError),
}
/// An exported version of an `InboundGroupSession`
///
/// This can be used to share the `InboundGroupSession` in an exported file.
@@ -27,16 +27,11 @@ use dashmap::DashMap;
use matrix_sdk_common::locks::Mutex;
use ruma::{
events::{
room::{
encrypted::{
EncryptedEventScheme, MegolmV1AesSha2ContentInit, RoomEncryptedEventContent,
},
encryption::RoomEncryptionEventContent,
history_visibility::HistoryVisibility,
},
room::{encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility},
room_key::ToDeviceRoomKeyEventContent,
AnyToDeviceEventContent,
},
serde::Raw,
DeviceId, EventEncryptionAlgorithm, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomId,
SecondsSinceUnixEpoch, TransactionId, UserId,
};
@@ -50,7 +45,12 @@ pub use vodozemac::{
PickleError,
};
use crate::{Device, ToDeviceRequest};
use crate::{
types::events::room::encrypted::{
MegolmV1AesSha2Content, RoomEncryptedEventContent, RoomEventEncryptionScheme,
},
Device, ToDeviceRequest,
};
const ROTATION_PERIOD: Duration = Duration::from_millis(604800000);
const ROTATION_MESSAGES: u64 = 100;
@@ -272,7 +272,11 @@ impl OutboundGroupSession {
/// # Panics
///
/// Panics if the content can't be serialized.
pub async fn encrypt(&self, content: Value, event_type: &str) -> RoomEncryptedEventContent {
pub async fn encrypt(
&self,
content: Value,
event_type: &str,
) -> Raw<RoomEncryptedEventContent> {
let json_content = json!({
"content": content,
"room_id": &*self.room_id,
@@ -280,22 +284,21 @@ impl OutboundGroupSession {
});
let plaintext = json_content.to_string();
let relation = serde_json::from_value(content).ok();
let relates_to = content.get("relates_to").cloned();
let ciphertext = self.encrypt_helper(plaintext).await;
let encrypted_content = MegolmV1AesSha2ContentInit {
ciphertext: ciphertext.to_base64(),
sender_key: self.account_identity_keys.curve25519.to_base64(),
let scheme: RoomEventEncryptionScheme = MegolmV1AesSha2Content {
ciphertext,
sender_key: self.account_identity_keys.curve25519,
session_id: self.session_id().to_owned(),
device_id: (*self.device_id).to_owned(),
}
.into();
RoomEncryptedEventContent::new(
EncryptedEventScheme::MegolmV1AesSha2(encrypted_content),
relation,
)
let content = RoomEncryptedEventContent { scheme, relates_to };
Raw::new(&content).expect("m.room.encrypted event content can always be serialized")
}
fn elapsed(&self) -> bool {
+15 -22
View File
@@ -28,7 +28,8 @@ pub use account::{OlmMessageHash, PickledAccount, ReadOnlyAccount};
pub(crate) use group_sessions::ShareState;
pub use group_sessions::{
EncryptionSettings, ExportedRoomKey, InboundGroupSession, OutboundGroupSession,
PickledInboundGroupSession, PickledOutboundGroupSession, SessionKey, ShareInfo,
PickledInboundGroupSession, PickledOutboundGroupSession, SessionCreationError, SessionKey,
ShareInfo,
};
pub use session::{PickledSession, Session};
pub use signing::{CrossSigningStatus, PickledCrossSigningIdentity, PrivateCrossSigningIdentity};
@@ -44,15 +45,17 @@ pub(crate) mod tests {
events::{
forwarded_room_key::ToDeviceForwardedRoomKeyEventContent,
room::message::{Relation, Replacement, RoomMessageEventContent},
AnyMessageLikeEvent, AnyRoomEvent, AnySyncMessageLikeEvent, AnySyncRoomEvent,
MessageLikeEvent, SyncMessageLikeEvent,
AnyMessageLikeEvent, AnyRoomEvent, MessageLikeEvent,
},
room_id, user_id, DeviceId, UserId,
};
use serde_json::json;
use vodozemac::olm::OlmMessage;
use crate::olm::{ExportedRoomKey, InboundGroupSession, ReadOnlyAccount, Session};
use crate::{
olm::{ExportedRoomKey, InboundGroupSession, ReadOnlyAccount, Session},
utilities::json_convert,
};
fn alice_id() -> &'static UserId {
user_id!("@alice:example.org")
@@ -136,10 +139,8 @@ pub(crate) mod tests {
};
let bob_keys = bob.identity_keys();
let result = alice
.create_inbound_session(&bob_keys.curve25519.to_base64(), &prekey_message)
.await
.unwrap();
let result =
alice.create_inbound_session(bob_keys.curve25519, &prekey_message).await.unwrap();
assert_eq!(bob_session.session_id(), result.session.session_id());
@@ -163,6 +164,7 @@ pub(crate) mod tests {
"test_key",
room_id,
&outbound.session_key().await,
outbound.settings().algorithm.to_owned(),
None,
);
@@ -203,6 +205,7 @@ pub(crate) mod tests {
"test_key",
room_id,
&outbound.session_key().await,
outbound.settings().algorithm.to_owned(),
None,
);
@@ -220,20 +223,9 @@ pub(crate) mod tests {
"room_id": room_id,
"type": "m.room.encrypted",
"content": encrypted_content,
})
.to_string();
let event: AnySyncRoomEvent = serde_json::from_str(&event).unwrap();
let event = if let AnySyncRoomEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
SyncMessageLikeEvent::Original(event),
)) = event
{
event
} else {
panic!("Invalid event type")
};
});
let event = json_convert(&event).unwrap();
let decrypted = inbound.decrypt(&event).await.unwrap().0;
if let AnyRoomEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(
@@ -257,7 +249,8 @@ pub(crate) mod tests {
let export: ToDeviceForwardedRoomKeyEventContent = export.try_into().unwrap();
let export = ExportedRoomKey::try_from(export).unwrap();
let imported = InboundGroupSession::from_export(export);
let imported = InboundGroupSession::from_export(&export)
.expect("We can always import an inbound group session from a fresh export");
assert_eq!(inbound.session_id(), imported.session_id());
}
+16 -19
View File
@@ -12,17 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::BTreeMap, fmt, sync::Arc};
use std::{fmt, sync::Arc};
use matrix_sdk_common::locks::Mutex;
use ruma::{
events::{
room::encrypted::{
CiphertextInfo, EncryptedEventScheme, OlmV1Curve25519AesSha2Content,
ToDeviceRoomEncryptedEventContent,
},
AnyToDeviceEventContent, EventContent,
},
events::{AnyToDeviceEventContent, EventContent},
serde::Raw,
DeviceId, SecondsSinceUnixEpoch, UserId,
};
use serde::{Deserialize, Serialize};
@@ -35,6 +30,9 @@ use vodozemac::{
use super::IdentityKeys;
use crate::{
error::{EventError, OlmResult},
types::events::room::encrypted::{
OlmV1Curve25519AesSha2Content, ToDeviceEncryptedEventContent,
},
ReadOnlyDevice,
};
@@ -120,7 +118,7 @@ impl Session {
&mut self,
recipient_device: &ReadOnlyDevice,
content: AnyToDeviceEventContent,
) -> OlmResult<ToDeviceRoomEncryptedEventContent> {
) -> OlmResult<Raw<ToDeviceEncryptedEventContent>> {
let recipient_signing_key =
recipient_device.ed25519_key().ok_or(EventError::MissingSigningKey)?;
@@ -141,19 +139,18 @@ impl Session {
});
let plaintext = serde_json::to_string(&payload)?;
let ciphertext = self.encrypt_helper(&plaintext).await.to_parts();
let ciphertext = self.encrypt_helper(&plaintext).await;
let message_type = ciphertext.0;
let ciphertext = CiphertextInfo::new(ciphertext.1, (message_type as u32).into());
let content = OlmV1Curve25519AesSha2Content {
ciphertext,
recipient_key: self.sender_key,
sender_key: self.our_identity_keys.curve25519,
}
.into();
let mut content = BTreeMap::new();
content.insert(self.sender_key.to_base64(), ciphertext);
let content = Raw::new(&content).expect("A encrypted can always be serialized");
Ok(EncryptedEventScheme::OlmV1Curve25519AesSha2(OlmV1Curve25519AesSha2Content::new(
content,
self.our_identity_keys.curve25519.to_base64(),
))
.into())
Ok(content)
}
/// Returns the unique identifier for this session.
@@ -715,15 +715,15 @@ mod tests {
let master_key = identity.master_key.lock().await;
let master_key = master_key.as_ref().unwrap();
assert!(master_key
master_key
.public_key
.verify_subkey(&identity.self_signing_key.lock().await.as_ref().unwrap().public_key,)
.is_ok());
.verify_subkey(&identity.self_signing_key.lock().await.as_ref().unwrap().public_key)
.unwrap();
assert!(master_key
master_key
.public_key
.verify_subkey(&identity.user_signing_key.lock().await.as_ref().unwrap().public_key,)
.is_ok());
.verify_subkey(&identity.user_signing_key.lock().await.as_ref().unwrap().public_key)
.unwrap();
}
#[async_test]
@@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::convert::TryInto;
use ruma::{CanonicalJsonValue, DeviceKeyAlgorithm, DeviceKeyId, UserId};
use serde::Serialize;
use serde_json::Value;
+18 -9
View File
@@ -78,9 +78,14 @@ impl ToDeviceRequest {
pub(crate) fn new(
recipient: &UserId,
recipient_device: impl Into<DeviceIdOrAllDevices>,
content: AnyToDeviceEventContent,
event_type: &str,
content: Raw<AnyToDeviceEventContent>,
) -> Self {
Self::with_id(recipient, recipient_device, content, TransactionId::new())
let event_type = ToDeviceEventType::from(event_type);
let user_messages = iter::once((recipient_device.into(), content)).collect();
let messages = iter::once((recipient.to_owned(), user_messages)).collect();
ToDeviceRequest { event_type, txn_id: TransactionId::new(), messages }
}
pub(crate) fn for_recipients(
@@ -89,20 +94,24 @@ impl ToDeviceRequest {
content: AnyToDeviceEventContent,
txn_id: OwnedTransactionId,
) -> Self {
let event_type = content.event_type();
let raw_content = Raw::new(&content).expect("Failed to serialize to-device event");
if recipient_devices.is_empty() {
Self::new(recipient, DeviceIdOrAllDevices::AllDevices, content)
Self::new(
recipient,
DeviceIdOrAllDevices::AllDevices,
&event_type.to_string(),
raw_content,
)
} else {
let event_type = content.event_type();
let device_messages = recipient_devices
.into_iter()
.map(|d| {
let raw_content =
Raw::new(&content).expect("Failed to serialize to-device event");
(DeviceIdOrAllDevices::DeviceId(d), raw_content)
})
.map(|d| (DeviceIdOrAllDevices::DeviceId(d), raw_content.clone()))
.collect();
let messages = iter::once((recipient.to_owned(), device_messages)).collect();
ToDeviceRequest { event_type, txn_id, messages }
}
}
@@ -22,10 +22,7 @@ use dashmap::DashMap;
use futures_util::future::join_all;
use matrix_sdk_common::executor::spawn;
use ruma::{
events::{
room::{encrypted::RoomEncryptedEventContent, history_visibility::HistoryVisibility},
AnyToDeviceEventContent, ToDeviceEventType,
},
events::{AnyToDeviceEventContent, ToDeviceEventType},
serde::Raw,
to_device::DeviceIdOrAllDevices,
DeviceId, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId,
@@ -38,6 +35,7 @@ use crate::{
error::{EventError, MegolmResult, OlmResult},
olm::{Account, InboundGroupSession, OutboundGroupSession, Session, ShareInfo, ShareState},
store::{Changes, Result as StoreResult, Store},
types::events::room::encrypted::RoomEncryptedEventContent,
Device, EncryptionSettings, OlmError, ToDeviceRequest,
};
@@ -160,7 +158,7 @@ impl GroupSessionManager {
room_id: &RoomId,
content: Value,
event_type: &str,
) -> MegolmResult<RoomEncryptedEventContent> {
) -> MegolmResult<Raw<RoomEncryptedEventContent>> {
let session = self.sessions.get(room_id).expect("Session wasn't created nor shared");
assert!(!session.expired(), "Session expired");
@@ -252,8 +250,7 @@ impl GroupSessionManager {
.or_insert_with(BTreeMap::new)
.insert(
DeviceIdOrAllDevices::DeviceId(device.device_id().into()),
Raw::new(&AnyToDeviceEventContent::RoomEncrypted(encrypted))
.expect("Failed to serialize encrypted event"),
encrypted.cast(),
);
share_info
.entry(device.user_id().to_owned())
@@ -323,7 +320,7 @@ impl GroupSessionManager {
pub async fn collect_session_recipients(
&self,
users: impl Iterator<Item = &UserId>,
history_visibility: HistoryVisibility,
settings: &EncryptionSettings,
outbound: &OutboundGroupSession,
) -> OlmResult<(bool, HashMap<OwnedUserId, Vec<Device>>)> {
let users: HashSet<&UserId> = users.collect();
@@ -331,7 +328,7 @@ impl GroupSessionManager {
trace!(
?users,
?history_visibility,
?settings,
session_id = outbound.session_id(),
room_id = outbound.room_id().as_str(),
"Calculating group session recipients"
@@ -347,16 +344,19 @@ impl GroupSessionManager {
// get the session but is in the set of users that received the session.
let user_left = !users_shared_with.difference(&users).collect::<HashSet<_>>().is_empty();
let visibility_changed = outbound.settings().history_visibility != history_visibility;
let visibility_changed =
outbound.settings().history_visibility != settings.history_visibility;
let algorithm_changed = outbound.settings().algorithm != settings.algorithm;
// To protect the room history we need to rotate the session if either:
//
// 1. Any user left the room.
// 2. Any of the users' devices got deleted or blacklisted.
// 3. The history visibility changed.
// 4. The encryption algorithm changed.
//
// This is calculated in the following code and stored in this variable.
let mut should_rotate = user_left || visibility_changed;
let mut should_rotate = user_left || visibility_changed || algorithm_changed;
for user_id in users {
let user_devices = self.store.get_user_devices_filtered(user_id).await?;
@@ -445,22 +445,27 @@ impl GroupSessionManager {
users: impl Iterator<Item = &UserId>,
encryption_settings: impl Into<EncryptionSettings>,
) -> OlmResult<Vec<Arc<ToDeviceRequest>>> {
trace!(room_id = room_id.as_str(), "Checking if a room key needs to be shared",);
trace!(room_id = room_id.as_str(), "Checking if a room key needs to be shared");
let encryption_settings = encryption_settings.into();
let history_visibility = encryption_settings.history_visibility.clone();
let mut changes = Changes::default();
// Try to get an existing session or create a new one.
let (outbound, inbound) =
self.get_or_create_outbound_session(room_id, encryption_settings.clone()).await?;
// Having an inbound group session here means that we created a new
// group session pair, which we then need to store.
if let Some(inbound) = inbound {
changes.outbound_group_sessions.push(outbound.clone());
changes.inbound_group_sessions.push(inbound);
}
// Collect the recipient devices and check if either the settings
// or the recipient list changed in a way that requires the
// session to be rotated.
let (should_rotate, devices) =
self.collect_session_recipients(users, history_visibility, &outbound).await?;
self.collect_session_recipients(users, &encryption_settings, &outbound).await?;
let outbound = if should_rotate {
let old_session_id = outbound.session_id();
@@ -475,7 +480,8 @@ impl GroupSessionManager {
old_session_id = old_session_id,
session_id = outbound.session_id(),
"A user or device has left the room since we last sent a \
message, rotating the room key.",
message, or the encryption settings have changed. Rotating the \
room key.",
);
outbound
@@ -483,6 +489,8 @@ impl GroupSessionManager {
outbound
};
// Filter out the devices that already received this room key or have a
// to-device message already queued up.
let devices: Vec<Device> = devices
.into_iter()
.flat_map(|(_, d)| {
@@ -494,12 +502,17 @@ impl GroupSessionManager {
let key_content = outbound.as_content().await;
let message_index = outbound.message_index().await;
// If we have some recipients, log them here.
if !devices.is_empty() {
let recipients = devices.iter().fold(BTreeMap::new(), |mut acc, d| {
acc.entry(d.user_id()).or_insert_with(BTreeSet::new).insert(d.device_id());
acc
});
// If there are new recipients we need to persist the outbound group
// session as the to-device requests are persisted with the session.
changes.outbound_group_sessions = vec![outbound.clone()];
info!(
index = message_index,
?recipients,
@@ -509,6 +522,10 @@ impl GroupSessionManager {
);
}
// Chunk the recipients out so each to-device request will contain a
// limited amount of to-device messages.
//
// Create concurrent tasks for each chunk of recipients.
let tasks: Vec<_> = devices
.chunks(Self::MAX_TO_DEVICE_MESSAGES)
.map(|chunk| {
@@ -522,12 +539,19 @@ impl GroupSessionManager {
})
.collect();
// Wait for all the tasks to finish up and queue up the Olm session that
// was used to encrypt the room key to be persisted again. This is
// needed because each encryption step will mutate the Olm session,
// ratcheting its state forward.
for result in join_all(tasks).await {
let used_sessions: OlmResult<Vec<Session>> = result.expect("Encryption task panicked");
changes.sessions.extend(used_sessions?);
}
// The to-device requests get added to the outbound group session, this
// way we're making sure that they are persisted and scoped to the
// session.
let requests = outbound.pending_requests();
if requests.is_empty() {
@@ -545,6 +569,7 @@ impl GroupSessionManager {
let mut recipients: BTreeMap<&UserId, BTreeSet<&DeviceIdOrAllDevices>> =
BTreeMap::new();
// We're just collecting the recipients for logging reasons.
for request in &requests {
for (user_id, device_map) in &request.messages {
let devices = device_map.keys();
@@ -565,6 +590,7 @@ impl GroupSessionManager {
);
}
// Persist any changes we might have collected.
if !changes.is_empty() {
let session_count = changes.sessions.len();
@@ -584,19 +610,22 @@ impl GroupSessionManager {
#[cfg(test)]
mod tests {
use std::ops::Deref;
use std::{collections::HashSet, ops::Deref};
use matrix_sdk_test::{async_test, response_from_file};
use ruma::{
api::{
client::keys::{claim_keys, get_keys},
client::{
keys::{claim_keys, get_keys},
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
},
IncomingResponse,
},
device_id,
events::room::history_visibility::HistoryVisibility,
room_id, user_id, DeviceId, TransactionId, UserId,
room_id, user_id, DeviceId, EventEncryptionAlgorithm, TransactionId, UserId,
};
use serde_json::Value;
use serde_json::{json, Value};
use crate::{EncryptionSettings, OlmMachine};
@@ -616,12 +645,67 @@ mod tests {
.expect("Can't parse the keys upload response")
}
fn bob_keys_query_response() -> get_keys::v3::Response {
let data = json!({
"device_keys": {
"@bob:localhost": {
"BOBDEVICE": {
"user_id": "@bob:localhost",
"device_id": "BOBDEVICE",
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2",
"m.megolm.v2.aes-sha2"
],
"keys": {
"curve25519:BOBDEVICE": "QzXDFZj0Pt5xG4r11XGSrqE4mnFOTgRM5pz7n3tzohU",
"ed25519:BOBDEVICE": "T7QMEXcEo/NfiC/8doVHT+2XnMm0pDpRa27bmE8PlPI"
},
"signatures": {
"@bob:localhost": {
"ed25519:BOBDEVICE": "1Ee9J02KoVf4DKhT+LkurpZJEygiznqpgkT4lqvMTLtZyzShsVTnwmoMPttuGcJkLp9lMK1egveNYCEaYP80Cw"
}
}
}
}
}
});
let data = response_from_file(&data);
get_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the keys upload response")
}
fn bob_one_time_key() -> claim_keys::v3::Response {
let data = json!({
"failures": {},
"one_time_keys":{
"@bob:localhost":{
"BOBDEVICE":{
"signed_curve25519:AAAAAAAAAAA": {
"key":"bm1olfbksjC5SwKxCLLK4XaINCA0FwR/155J85gIpCk",
"signatures":{
"@bob:localhost":{
"ed25519:BOBDEVICE":"BKyS/+EV76zdZkWgny2D0svZ0ycS3etfyHCrsDgm7MYe166HqQmSoX29HsjGLvE/5F+Sg2zW7RJileUvquPwDA"
}
}
}
}
}
}
});
let data = response_from_file(&data);
claim_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the keys claim response")
}
fn keys_claim_response() -> claim_keys::v3::Response {
let data = include_bytes!("../../../../benchmarks/benches/crypto_bench/keys_claim.json");
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
claim_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the keys upload response")
.expect("Can't parse the keys claim response")
}
async fn machine_with_user(user_id: &UserId, device_id: &DeviceId) -> OlmMachine {
@@ -633,6 +717,8 @@ mod tests {
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
machine.mark_request_as_sent(&txn_id, &keys_claim).await.unwrap();
machine.mark_request_as_sent(&txn_id, &bob_keys_query_response()).await.unwrap();
machine.mark_request_as_sent(&txn_id, &bob_one_time_key()).await.unwrap();
machine
}
@@ -641,6 +727,31 @@ mod tests {
machine_with_user(alice_id(), alice_device_id()).await
}
async fn machine_with_shared_room_key() -> OlmMachine {
let machine = machine().await;
let room_id = room_id!("!test:localhost");
let keys_claim = keys_claim_response();
let users = keys_claim.one_time_keys.keys().map(Deref::deref);
let requests =
machine.share_room_key(room_id, users, EncryptionSettings::default()).await.unwrap();
let outbound = machine.group_session_manager.get_outbound_group_session(room_id).unwrap();
assert!(!outbound.pending_requests().is_empty());
assert!(!outbound.shared());
let response = ToDeviceResponse::new();
for request in requests {
machine.mark_request_as_sent(&request.txn_id, &response).await.unwrap();
}
assert!(outbound.shared());
assert!(outbound.pending_requests().is_empty());
machine
}
#[async_test]
async fn test_sharing() {
let machine = machine().await;
@@ -660,6 +771,73 @@ mod tests {
assert_eq!(event_count, 148);
}
#[async_test]
async fn ratcheted_sharing() {
let machine = machine_with_shared_room_key().await;
let room_id = room_id!("!test:localhost");
let late_joiner = user_id!("@bob:localhost");
let keys_claim = keys_claim_response();
let mut users: HashSet<_> = keys_claim.one_time_keys.keys().map(Deref::deref).collect();
users.insert(late_joiner);
let requests = machine
.share_room_key(room_id, users.into_iter(), EncryptionSettings::default())
.await
.unwrap();
let event_count: usize = requests.iter().map(|r| r.message_count()).sum();
let outbound = machine.group_session_manager.get_outbound_group_session(room_id).unwrap();
assert_eq!(event_count, 1);
assert!(!outbound.pending_requests().is_empty());
}
#[async_test]
async fn changing_encryption_settings() {
let machine = machine_with_shared_room_key().await;
let room_id = room_id!("!test:localhost");
let keys_claim = keys_claim_response();
let users = keys_claim.one_time_keys.keys().map(Deref::deref);
let outbound = machine.group_session_manager.get_outbound_group_session(room_id).unwrap();
let (should_rotate, _) = machine
.group_session_manager
.collect_session_recipients(users.clone(), &EncryptionSettings::default(), &outbound)
.await
.unwrap();
assert!(!should_rotate);
let settings = EncryptionSettings {
history_visibility: HistoryVisibility::Invited,
..Default::default()
};
let (should_rotate, _) = machine
.group_session_manager
.collect_session_recipients(users.clone(), &settings, &outbound)
.await
.unwrap();
assert!(should_rotate);
let settings = EncryptionSettings {
algorithm: EventEncryptionAlgorithm::from("m.megolm.v2.aes-sha2"),
..Default::default()
};
let (should_rotate, _) = machine
.group_session_manager
.collect_session_recipients(users, &settings, &outbound)
.await
.unwrap();
assert!(should_rotate);
}
#[async_test]
async fn key_recipient_collecting() {
// The user id comes from the fact that the keys_query.json file uses
@@ -676,12 +854,13 @@ mod tests {
.await
.expect("We should be able to create a new session");
let history_visibility = HistoryVisibility::Joined;
let settings = EncryptionSettings { history_visibility, ..Default::default() };
let users = [user_id].into_iter();
let (_, recipients) = machine
.group_session_manager
.collect_session_recipients(users, history_visibility, &outbound)
.collect_session_recipients(users, &settings, &outbound)
.await
.expect("We should be able to collect the session recipients");
@@ -37,6 +37,7 @@ use crate::{
olm::Account,
requests::{OutgoingRequest, ToDeviceRequest},
store::{Changes, Result as StoreResult, Store},
types::events::EventType,
ReadOnlyDevice,
};
@@ -145,7 +146,8 @@ impl SessionManager {
let request = ToDeviceRequest::new(
device.user_id(),
device.device_id().to_owned(),
AnyToDeviceEventContent::RoomEncrypted(content),
content.event_type(),
content.cast(),
);
let request = OutgoingRequest {
@@ -338,10 +340,7 @@ impl SessionManager {
self.key_request_machine.retry_keyshare(user_id, device_id);
if let Err(e) = self.check_if_unwedged(user_id, device_id).await {
error!(
"Error while treating an unwedged device {} {} {:?}",
user_id, device_id, e
);
error!(%user_id, %device_id, "Error while treating an unwedged device: {e:?}");
}
changes.sessions.push(session);
@@ -240,6 +240,7 @@ mod tests {
"test_key",
room_id,
&outbound.session_key().await,
outbound.settings().algorithm.to_owned(),
None,
);
@@ -288,7 +288,7 @@ macro_rules! cryptostore_integration_tests {
export.forwarding_curve25519_key_chain = vec!["some_chain".to_owned()];
let session = InboundGroupSession::from_export(export);
let session = InboundGroupSession::from_export(&export).unwrap();
let changes =
Changes { inbound_group_sessions: vec![session.clone()], ..Default::default() };
@@ -350,6 +350,7 @@ mod tests {
"test_key",
room_id,
&outbound.session_key().await,
outbound.settings().algorithm.to_owned(),
None,
);
+2 -2
View File
@@ -75,7 +75,7 @@ use crate::{
},
olm::{
InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity,
ReadOnlyAccount, Session,
ReadOnlyAccount, Session, SessionCreationError,
},
utilities::encode,
verification::VerificationMachine,
@@ -603,7 +603,7 @@ pub enum CryptoStoreError {
/// The received room key couldn't be converted into a valid Megolm session.
#[error(transparent)]
SessionCreation(#[from] vodozemac::megolm::SessionKeyDecodeError),
SessionCreation(#[from] SessionCreationError),
/// A Matrix identifier failed to be validated.
#[error(transparent)]
@@ -18,16 +18,30 @@
//! types. Once deserialized they aim to zeroize all the secret material once
//! the type is dropped.
pub mod room;
pub mod room_key;
pub mod secret_send;
mod to_device;
use ruma::serde::Raw;
pub use to_device::{ToDeviceCustomEvent, ToDeviceEvent, ToDeviceEvents};
/// A trait for event contents to define their event type.
pub trait EventType {
/// The event type of the event content.
const EVENT_TYPE: &'static str;
/// Get the event type of the event content.
fn event_type(&self) -> &str;
///
/// **Note**: This should never be implemented manually, this takes the
/// event type from the constant.
fn event_type(&self) -> &'static str {
Self::EVENT_TYPE
}
}
impl<T: EventType> EventType for Raw<T> {
const EVENT_TYPE: &'static str = T::EVENT_TYPE;
}
fn from_str<'a, T, E>(string: &'a str) -> Result<T, E>
@@ -0,0 +1,400 @@
// Copyright 2022 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.
//! Types for the `m.room.encrypted` room events.
use std::collections::BTreeMap;
use ruma::{DeviceId, EventEncryptionAlgorithm, OwnedDeviceId};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use vodozemac::{megolm::MegolmMessage, olm::OlmMessage, Curve25519PublicKey};
use super::Event;
use crate::types::{
deserialize_curve_key,
events::{EventType, ToDeviceEvent},
serialize_curve_key,
};
/// An m.room.encrypted room event.
pub type EncryptedEvent = Event<RoomEncryptedEventContent>;
/// An m.room.encrypted to-device event.
pub type EncryptedToDeviceEvent = ToDeviceEvent<ToDeviceEncryptedEventContent>;
impl EncryptedToDeviceEvent {
/// Get the algorithm of the encrypted event content.
pub fn algorithm(&self) -> EventEncryptionAlgorithm {
self.content.algorithm()
}
}
/// The content for `m.room.encrypted` to-device events.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(try_from = "Helper")]
pub enum ToDeviceEncryptedEventContent {
/// The event content for events encrypted with the m.megolm.v1.aes-sha2
/// algorithm.
OlmV1Curve25519AesSha2(Box<OlmV1Curve25519AesSha2Content>),
/// An event content that was encrypted with an unknown encryption
/// algorithm.
Unknown(UnknownEncryptedContent),
}
impl EventType for ToDeviceEncryptedEventContent {
const EVENT_TYPE: &'static str = "m.room.encrypted";
}
impl ToDeviceEncryptedEventContent {
/// Get the algorithm of the event content.
pub fn algorithm(&self) -> EventEncryptionAlgorithm {
match self {
ToDeviceEncryptedEventContent::OlmV1Curve25519AesSha2(_) => {
EventEncryptionAlgorithm::OlmV1Curve25519AesSha2
}
ToDeviceEncryptedEventContent::Unknown(c) => c.algorithm.to_owned(),
}
}
}
/// The event content for events encrypted with the m.olm.v1.curve25519-aes-sha2
/// algorithm.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(try_from = "OlmHelper")]
pub struct OlmV1Curve25519AesSha2Content {
/// The encrypted content of the event.
pub ciphertext: OlmMessage,
/// The Curve25519 key of the recipient device.
pub recipient_key: Curve25519PublicKey,
/// The Curve25519 key of the sender.
pub sender_key: Curve25519PublicKey,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
struct OlmHelper {
#[serde(deserialize_with = "deserialize_curve_key", serialize_with = "serialize_curve_key")]
sender_key: Curve25519PublicKey,
ciphertext: BTreeMap<String, OlmMessage>,
}
impl Serialize for OlmV1Curve25519AesSha2Content {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let ciphertext =
BTreeMap::from([(self.recipient_key.to_base64(), self.ciphertext.clone())]);
OlmHelper { sender_key: self.sender_key, ciphertext }.serialize(serializer)
}
}
impl TryFrom<OlmHelper> for OlmV1Curve25519AesSha2Content {
type Error = serde_json::Error;
fn try_from(value: OlmHelper) -> Result<Self, Self::Error> {
let (recipient_key, ciphertext) = value.ciphertext.into_iter().next().ok_or_else(|| {
serde::de::Error::custom(
"The `m.room.encrypted` event is missing a ciphertext".to_owned(),
)
})?;
let recipient_key =
Curve25519PublicKey::from_base64(&recipient_key).map_err(serde::de::Error::custom)?;
Ok(Self { ciphertext, recipient_key, sender_key: value.sender_key })
}
}
/// The content for `m.room.encrypted` room events.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomEncryptedEventContent {
/// Algorithm-specific fields.
#[serde(flatten)]
pub scheme: RoomEventEncryptionScheme,
/// Information about related events.
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub relates_to: Option<Value>,
}
impl RoomEncryptedEventContent {
/// Get the algorithm of the event content.
pub fn algorithm(&self) -> EventEncryptionAlgorithm {
self.scheme.algorithm()
}
}
impl EventType for RoomEncryptedEventContent {
const EVENT_TYPE: &'static str = "m.room.encrypted";
}
/// An enum for per encryption algorithm event contents.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(try_from = "Helper")]
pub enum RoomEventEncryptionScheme {
/// The event content for events encrypted with the m.megolm.v1.aes-sha2
/// algorithm.
MegolmV1AesSha2(MegolmV1AesSha2Content),
/// An event content that was encrypted with an unknown encryption
/// algorithm.
Unknown(UnknownEncryptedContent),
}
impl RoomEventEncryptionScheme {
/// Get the algorithm of the event content.
pub fn algorithm(&self) -> EventEncryptionAlgorithm {
match self {
RoomEventEncryptionScheme::MegolmV1AesSha2(_) => {
EventEncryptionAlgorithm::MegolmV1AesSha2
}
RoomEventEncryptionScheme::Unknown(c) => c.algorithm.to_owned(),
}
}
}
pub(crate) enum SupportedEventEncryptionSchemes<'a> {
MegolmV1AesSha2(&'a MegolmV1AesSha2Content),
}
impl SupportedEventEncryptionSchemes<'_> {
/// The Curve25519 key of the sender.
pub fn sender_key(&self) -> Curve25519PublicKey {
match self {
SupportedEventEncryptionSchemes::MegolmV1AesSha2(c) => c.sender_key,
}
}
/// The ID of the session used to encrypt the message.
pub fn session_id(&self) -> &str {
match self {
SupportedEventEncryptionSchemes::MegolmV1AesSha2(c) => &c.session_id,
}
}
/// The ID of the sending device.
pub fn device_id(&self) -> &DeviceId {
match self {
SupportedEventEncryptionSchemes::MegolmV1AesSha2(c) => &c.device_id,
}
}
/// The algorithm that was used to encrypt the event content.
pub fn algorithm(&self) -> EventEncryptionAlgorithm {
match self {
SupportedEventEncryptionSchemes::MegolmV1AesSha2(_) => {
EventEncryptionAlgorithm::MegolmV1AesSha2
}
}
}
}
impl<'a> From<&'a MegolmV1AesSha2Content> for SupportedEventEncryptionSchemes<'a> {
fn from(c: &'a MegolmV1AesSha2Content) -> Self {
Self::MegolmV1AesSha2(c)
}
}
/// The event content for events encrypted with the m.megolm.v1.aes-sha2
/// algorithm.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MegolmV1AesSha2Content {
/// The encrypted content of the event.
pub ciphertext: MegolmMessage,
/// The Curve25519 key of the sender.
#[serde(deserialize_with = "deserialize_curve_key", serialize_with = "serialize_curve_key")]
pub sender_key: Curve25519PublicKey,
/// The ID of the sending device.
pub device_id: OwnedDeviceId,
/// The ID of the session used to encrypt the message.
pub session_id: String,
}
/// An unknown and unsupported `m.room.encrypted` event content.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnknownEncryptedContent {
/// The algorithm that was used to encrypt the given event content.
pub algorithm: EventEncryptionAlgorithm,
/// The other data of the unknown encryped content.
#[serde(flatten)]
other: BTreeMap<String, Value>,
}
#[derive(Debug, Deserialize, Serialize)]
struct Helper {
algorithm: EventEncryptionAlgorithm,
#[serde(flatten)]
other: Value,
}
macro_rules! scheme_serialization {
($something:ident, $($algorithm:ident => $content:ident),+ $(,)?) => {
$(
impl From<$content> for $something {
fn from(c: $content) -> Self {
Self::$algorithm(c.into())
}
}
)+
impl TryFrom<Helper> for $something {
type Error = serde_json::Error;
fn try_from(value: Helper) -> Result<Self, Self::Error> {
Ok(match value.algorithm {
$(
EventEncryptionAlgorithm::$algorithm => {
let content: $content = serde_json::from_value(value.other)?;
content.into()
}
)+
_ => Self::Unknown(UnknownEncryptedContent {
algorithm: value.algorithm,
other: serde_json::from_value(value.other)?,
}),
})
}
}
impl Serialize for $something {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let helper = match self {
$(
Self::$algorithm(r) => Helper {
algorithm: self.algorithm(),
other: serde_json::to_value(r).map_err(serde::ser::Error::custom)?,
},
)+
Self::Unknown(r) => Helper {
algorithm: r.algorithm.clone(),
other: serde_json::to_value(r.other.clone()).map_err(serde::ser::Error::custom)?,
},
};
helper.serialize(serializer)
}
}
};
}
scheme_serialization!(
RoomEventEncryptionScheme,
MegolmV1AesSha2 => MegolmV1AesSha2Content,
);
scheme_serialization!(
ToDeviceEncryptedEventContent,
OlmV1Curve25519AesSha2 => OlmV1Curve25519AesSha2Content,
);
#[cfg(test)]
pub(crate) mod test {
use matches::assert_matches;
use serde_json::{json, Value};
use vodozemac::Curve25519PublicKey;
use super::{
EncryptedEvent, EncryptedToDeviceEvent, OlmV1Curve25519AesSha2Content,
RoomEventEncryptionScheme, ToDeviceEncryptedEventContent,
};
pub fn json() -> Value {
json!({
"sender": "@alice:example.org",
"event_id": "$Nhl3rsgHMjk-DjMJANawr9HHAhLg4GcoTYrSiYYGqEE",
"content": {
"m.custom": "something custom",
"algorithm": "m.megolm.v1.aes-sha2",
"device_id": "DEWRCMENGS",
"session_id": "ZFD6+OmV7fVCsJ7Gap8UnORH8EnmiAkes8FAvQuCw/I",
"sender_key": "WJ6Ce7U67a6jqkHYHd8o0+5H4bqdi9hInZdk0+swuXs",
"ciphertext": "AwgAEiBQs2LgBD2CcB+RLH2bsgp9VadFUJhBXOtCmcJuttBD\
OeDNjL21d9z0AcVSfQFAh9huh4or7sWuNrHcvu9/sMbweTgc\
0UtdA5xFLheubHouXy4aewze+ShndWAaTbjWJMLsPSQDUMQH\
BA"
},
"type": "m.room.encrypted",
"origin_server_ts": 1632491098485u64,
"m.custom.top": "something custom in the top",
})
}
pub fn olm_v1_json() -> Value {
json!({
"algorithm": "m.olm.v1.curve25519-aes-sha2",
"ciphertext": {
"Nn0L2hkcCMFKqynTjyGsJbth7QrVmX3lbrksMkrGOAw": {
"body": "Awogv7Iysf062hV1gZNfG/SdO5TdLYtkRI12em6LxralPxoSIC\
C/Avnha6NfkaMWSC+5h+khS0wHiUzA2bPmAvVo/iYhGiAfDNh4\
F0eqPvOc4Hw9wMgd+frzedZgmhUNfKT0UzHQZSJPAwogF8fTdT\
cPt1ppJ/KAEivFZ4dIyAlRUjzhlqzYsw9C1HoQACIgb9MK/a9T\
RLtwol9gfy7OeKdpmSe39YhP+5OchhKvX6eO3/aED3X1oA",
"type": 0
}
},
"sender_key": "mjkTX0I0Cp44ZfolOVbFe5WYPRmT6AX3J0ZbnGWnnWs"
})
}
pub fn to_device_json() -> Value {
json!({
"content": olm_v1_json(),
"sender": "@example:morpheus.localhost",
"type": "m.room.encrypted"
})
}
#[test]
fn deserialization() -> Result<(), serde_json::Error> {
let json = json();
let event: EncryptedEvent = serde_json::from_value(json.clone())?;
assert_matches!(event.content.scheme, RoomEventEncryptionScheme::MegolmV1AesSha2(_));
let serialized = serde_json::to_value(event)?;
assert_eq!(json, serialized);
let json = olm_v1_json();
let content: OlmV1Curve25519AesSha2Content = serde_json::from_value(json)?;
assert_eq!(
content.sender_key,
Curve25519PublicKey::from_base64("mjkTX0I0Cp44ZfolOVbFe5WYPRmT6AX3J0ZbnGWnnWs")
.unwrap()
);
assert_eq!(
content.recipient_key,
Curve25519PublicKey::from_base64("Nn0L2hkcCMFKqynTjyGsJbth7QrVmX3lbrksMkrGOAw")
.unwrap()
);
let json = to_device_json();
let event: EncryptedToDeviceEvent = serde_json::from_value(json.clone())?;
assert_matches!(event.content, ToDeviceEncryptedEventContent::OlmV1Curve25519AesSha2(_));
let serialized = serde_json::to_value(event)?;
assert_eq!(json, serialized);
Ok(())
}
}
@@ -0,0 +1,91 @@
// Copyright 2022 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.
//! Types for room events.
use std::{collections::BTreeMap, fmt::Debug};
use ruma::{EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, UserId};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::EventType;
pub mod encrypted;
/// Generic room event with a known type and content.
#[derive(Debug, Deserialize)]
pub struct Event<C>
where
C: EventType + Debug + Sized + Serialize,
{
/// Contains the fully-qualified ID of the user who sent this event.
pub sender: OwnedUserId,
/// The globally unique identifier for this event.
pub event_id: OwnedEventId,
/// The body of this event, as created by the client which sent it.
pub content: C,
/// Timestamp (in milliseconds since the unix epoch) on originating
/// homeserver when this event was sent.
pub origin_server_ts: MilliSecondsSinceUnixEpoch,
/// Contains optional extra information about the event.
#[serde(default)]
pub unsigned: BTreeMap<String, Value>,
/// Any other unknown data of the room event.
#[serde(flatten)]
other: BTreeMap<String, Value>,
}
impl<C> Serialize for Event<C>
where
C: EventType + Debug + Sized + Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#[derive(Serialize)]
struct Helper<'a, C> {
sender: &'a UserId,
event_id: &'a EventId,
#[serde(rename = "type")]
event_type: &'a str,
content: &'a C,
origin_server_ts: MilliSecondsSinceUnixEpoch,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
unsigned: &'a BTreeMap<String, Value>,
#[serde(flatten)]
other: &'a BTreeMap<String, Value>,
}
let event_type = C::EVENT_TYPE;
let helper = Helper {
sender: &self.sender,
content: &self.content,
event_type,
other: &self.other,
event_id: &self.event_id,
origin_server_ts: self.origin_server_ts,
unsigned: &self.unsigned,
};
helper.serialize(serializer)
}
}
@@ -26,12 +26,17 @@ use super::{EventType, ToDeviceEvent};
/// The `m.room_key` to-device event.
pub type RoomKeyEvent = ToDeviceEvent<RoomKeyContent>;
impl EventType for RoomKeyContent {
fn event_type(&self) -> &str {
"m.room_key"
impl RoomKeyEvent {
/// Get the algorithm of the room key.
pub fn algorithm(&self) -> EventEncryptionAlgorithm {
self.content.algorithm()
}
}
impl EventType for RoomKeyContent {
const EVENT_TYPE: &'static str = "m.room_key";
}
/// The `m.room_key` event content.
///
/// This is an enum over the different room key algorithms we support.
@@ -49,6 +54,14 @@ pub enum RoomKeyContent {
}
impl RoomKeyContent {
/// Get the algorithm of the room key.
pub fn algorithm(&self) -> EventEncryptionAlgorithm {
match &self {
RoomKeyContent::MegolmV1AesSha2(_) => EventEncryptionAlgorithm::MegolmV1AesSha2,
RoomKeyContent::Unknown(c) => c.algorithm.to_owned(),
}
}
pub(super) fn serialize_zeroized(&self) -> Result<Raw<RoomKeyContent>, serde_json::Error> {
#[derive(Serialize)]
struct Helper<'a> {
@@ -69,7 +82,7 @@ impl RoomKeyContent {
};
let helper = RoomKeyHelper {
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
algorithm: self.algorithm(),
other: serde_json::to_value(helper)?,
};
@@ -69,9 +69,7 @@ impl std::fmt::Debug for SecretSendContent {
}
impl EventType for SecretSendContent {
fn event_type(&self) -> &str {
"m.secret.send"
}
const EVENT_TYPE: &'static str = "m.secret.send";
}
#[cfg(test)]
@@ -24,7 +24,6 @@ use ruma::{
mac::ToDeviceKeyVerificationMacEvent, ready::ToDeviceKeyVerificationReadyEvent,
request::ToDeviceKeyVerificationRequestEvent, start::ToDeviceKeyVerificationStartEvent,
},
room::encrypted::ToDeviceRoomEncryptedEvent,
room_key_request::ToDeviceRoomKeyRequestEvent,
secret::request::{SecretName, ToDeviceSecretRequestEvent},
EventContent, ToDeviceEventType,
@@ -39,7 +38,10 @@ use serde_json::{
};
use zeroize::Zeroize;
use super::{room_key::RoomKeyEvent, secret_send::SecretSendEvent, EventType};
use super::{
room::encrypted::EncryptedToDeviceEvent, room_key::RoomKeyEvent, secret_send::SecretSendEvent,
EventType,
};
use crate::types::events::from_str;
/// An enum over the various to-device events we support.
@@ -68,7 +70,7 @@ pub enum ToDeviceEvents {
KeyVerificationRequest(ToDeviceKeyVerificationRequestEvent),
/// The `m.room.encrypted` to-device event.
RoomEncrypted(ToDeviceRoomEncryptedEvent),
RoomEncrypted(EncryptedToDeviceEvent),
/// The `m.room_key` to-device event.
RoomKey(RoomKeyEvent),
/// The `m.room_key_request` to-device event.
@@ -122,7 +124,7 @@ impl ToDeviceEvents {
ToDeviceEvents::KeyVerificationReady(e) => e.content.event_type(),
ToDeviceEvents::KeyVerificationRequest(e) => e.content.event_type(),
ToDeviceEvents::RoomEncrypted(e) => e.content.event_type(),
ToDeviceEvents::RoomEncrypted(_) => ToDeviceEventType::RoomEncrypted,
ToDeviceEvents::RoomKey(_) => ToDeviceEventType::RoomKey,
ToDeviceEvents::RoomKeyRequest(e) => e.content.event_type(),
ToDeviceEvents::ForwardedRoomKey(e) => e.content.event_type(),
@@ -241,7 +243,7 @@ where
pub content: C,
/// Any other unknown data of the to-device event.
#[serde(flatten)]
other: BTreeMap<String, Value>,
pub(crate) other: BTreeMap<String, Value>,
}
impl<C> Serialize for ToDeviceEvent<C>
@@ -396,23 +398,6 @@ mod test {
})
}
fn room_encrypted_event() -> Value {
json!({
"sender": "@alice:example.org",
"content": {
"algorithm": "m.olm.v1.curve25519-aes-sha2",
"sender_key": "<sender_curve25519_key>",
"ciphertext": {
"<device_curve25519_key>": {
"type": 0,
"body": "<encrypted_payload_base_64>"
}
}
},
"type": "m.room.encrypted",
})
}
fn forwarded_room_key_event() -> Value {
json!({
"sender": "@alice:example.org",
@@ -490,7 +475,7 @@ mod test {
dummy_event => Dummy,
// `m.room.encrypted`
room_encrypted_event => RoomEncrypted,
crate::types::events::room::encrypted::test::to_device_json => RoomEncrypted,
);
Ok(())
+10
View File
@@ -24,3 +24,13 @@ pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
pub fn encode(input: impl AsRef<[u8]>) -> String {
encode_config(input, STANDARD_NO_PAD)
}
#[cfg(test)]
pub(crate) fn json_convert<T, U>(value: &T) -> serde_json::Result<U>
where
T: serde::Serialize,
U: serde::de::DeserializeOwned,
{
let json = serde_json::to_string(value)?;
serde_json::from_str(&json)
}
@@ -169,7 +169,12 @@ impl VerificationCache {
) {
match content {
OutgoingContent::ToDevice(c) => {
let request = ToDeviceRequest::new(recipient, recipient_device.to_owned(), c);
let request = ToDeviceRequest::with_id(
recipient,
recipient_device.to_owned(),
c,
TransactionId::new(),
);
let request_id = request.txn_id.clone();
let request = OutgoingRequest {
@@ -753,7 +753,7 @@ impl TryFrom<ToDeviceRequest> for OutgoingContent {
serde_json::from_value(json).map_err(|e| e.to_string())?,
)
}
e => return Err(format!("Unsupported event type {}", e)),
e => return Err(format!("Unsupported event type {e}")),
};
Ok(content.into())
@@ -132,8 +132,12 @@ impl VerificationMachine {
RoomMessageRequest { room_id: r, txn_id: TransactionId::new(), content: c }.into()
}
OutgoingContent::ToDevice(c) => {
let request =
ToDeviceRequest::new(device.user_id(), device.device_id().to_owned(), c);
let request = ToDeviceRequest::with_id(
device.user_id(),
device.device_id().to_owned(),
c,
TransactionId::new(),
);
self.verifications.insert_sas(sas.clone());
@@ -506,7 +510,7 @@ impl VerificationMachine {
#[cfg(test)]
mod tests {
use std::{convert::TryFrom, sync::Arc, time::Duration};
use std::{sync::Arc, time::Duration};
use matrix_sdk_common::{instant::Instant, locks::Mutex};
use matrix_sdk_test::async_test;
@@ -501,10 +501,9 @@ impl IdentitiesBeingVerified {
}
Err(e) => {
error!(
"Error signing device keys for {} {} {:?}",
device.user_id(),
device.device_id(),
e
user_id = %device.user_id(),
device_id = %device.device_id(),
"Error signing device keys: {e:?}",
);
None
}
@@ -527,17 +526,16 @@ impl IdentitiesBeingVerified {
Ok(r) => Some(r),
Err(SignatureError::MissingSigningKey) => {
warn!(
"Can't sign the public cross signing keys for {}, \
no private user signing key found",
i.user_id()
user_id = %i.user_id(),
"Can't sign the public cross signing keys, \
no private user signing key found",
);
None
}
Err(e) => {
error!(
"Error signing the public cross signing keys for {} {:?}",
i.user_id(),
e
user_id = %i.user_id(),
"Error signing the public cross signing keys: {e:?}",
);
None
}
@@ -707,7 +705,6 @@ impl IdentitiesBeingVerified {
#[cfg(test)]
pub(crate) mod tests {
use std::convert::TryInto;
use ruma::{
events::{AnyToDeviceEventContent, ToDeviceEvent},
@@ -291,10 +291,11 @@ impl QrVerification {
OutgoingContent::Room(room_id, content) => {
RoomMessageRequest { room_id, txn_id: TransactionId::new(), content }.into()
}
OutgoingContent::ToDevice(c) => ToDeviceRequest::new(
OutgoingContent::ToDevice(c) => ToDeviceRequest::with_id(
self.identities.other_user_id(),
self.identities.other_device_id().to_owned(),
c,
TransactionId::new(),
)
.into(),
}
@@ -787,7 +788,7 @@ impl QrState<Reciprocated> {
#[cfg(test)]
mod tests {
use std::{convert::TryFrom, sync::Arc};
use std::sync::Arc;
use matrix_sdk_common::locks::Mutex;
use matrix_sdk_qrcode::QrVerificationData;
@@ -380,9 +380,13 @@ impl VerificationRequest {
let mut inner = self.inner.lock().unwrap();
inner.accept(methods).map(|c| match c {
OutgoingContent::ToDevice(content) => {
ToDeviceRequest::new(self.other_user(), inner.other_device_id(), content).into()
}
OutgoingContent::ToDevice(content) => ToDeviceRequest::with_id(
self.other_user(),
inner.other_device_id(),
content,
TransactionId::new(),
)
.into(),
OutgoingContent::Room(room_id, content) => {
RoomMessageRequest { room_id, txn_id: TransactionId::new(), content }.into()
}
@@ -435,7 +439,13 @@ impl VerificationRequest {
)
.into()
} else {
ToDeviceRequest::new(self.other_user(), other_device, content).into()
ToDeviceRequest::with_id(
self.other_user(),
other_device,
content,
TransactionId::new(),
)
.into()
}
}
OutgoingContent::Room(room_id, content) => {
@@ -627,10 +637,11 @@ impl VerificationRequest {
self.verification_cache.insert_sas(sas.clone());
let request = match content {
OutgoingContent::ToDevice(content) => ToDeviceRequest::new(
OutgoingContent::ToDevice(content) => ToDeviceRequest::with_id(
self.other_user(),
inner.other_device_id(),
content,
TransactionId::new(),
)
.into(),
OutgoingContent::Room(room_id, content) => {
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::BTreeMap, convert::TryInto};
use std::collections::BTreeMap;
use ruma::{
events::{
@@ -197,18 +197,17 @@ pub fn receive_mac_event(
let info = extra_mac_info_receive(ids, flow_id);
trace!(
"Received a key.verification.mac event from {} {}",
sender,
ids.other_device.device_id()
%sender,
device_id = %ids.other_device.device_id(),
"Received a key.verification.mac event"
);
let mut keys = content.mac().keys().map(|k| k.as_str()).collect::<Vec<_>>();
keys.sort_unstable();
let keys = Base64::parse(
sas.calculate_mac_invalid_base64(&keys.join(","), &format!("{}KEY_IDS", &info)),
)
.expect("Can't base64-decode SAS MAC");
let keys =
Base64::parse(sas.calculate_mac_invalid_base64(&keys.join(","), &format!("{info}KEY_IDS")))
.expect("Can't base64-decode SAS MAC");
if keys != *content.keys() {
return Err(CancelCode::KeyMismatch);
@@ -216,10 +215,10 @@ pub fn receive_mac_event(
for (key_id, key_mac) in content.mac() {
trace!(
"Checking MAC for the key id {} from {} {}",
%sender,
device_id = %ids.other_device.device_id(),
key_id,
sender,
ids.other_device.device_id()
"Checking a SAS MAC",
);
let key_id: OwnedDeviceKeyId = match key_id.as_str().try_into() {
@@ -234,7 +233,7 @@ pub fn receive_mac_event(
.expect("Can't base64-decode SAS MAC");
if *key_mac == calculated_mac {
trace!("Successfully verified the device key {} from {}", key_id, sender);
trace!(%sender, %key_id, "Successfully verified a device key");
verified_devices.push(ids.other_device.clone());
} else {
return Err(CancelCode::KeyMismatch);
@@ -243,14 +242,13 @@ pub fn receive_mac_event(
if let Some(key) = identity.master_key().get_key(&key_id) {
// TODO we should check that the master key signs the device,
// this way we know the master key also trusts the device
let calculated_mac = Base64::parse(sas.calculate_mac_invalid_base64(
&key.to_base64(),
&format!("{}{}", info, key_id),
))
let calculated_mac = Base64::parse(
sas.calculate_mac_invalid_base64(&key.to_base64(), &format!("{info}{key_id}")),
)
.expect("Can't base64-decode SAS MAC");
if *key_mac == calculated_mac {
trace!("Successfully verified the master key {} from {}", key_id, sender);
trace!(%sender, %key_id, "Successfully verified a master key");
verified_identities.push(identity.clone())
} else {
return Err(CancelCode::KeyMismatch);
@@ -258,10 +256,8 @@ pub fn receive_mac_event(
}
} else {
warn!(
"Key ID {} in MAC event from {} {} doesn't belong to any device \
"Key ID {key_id} in MAC event from {sender} {} doesn't belong to any device \
or user identity",
key_id,
sender,
ids.other_device.device_id()
);
}
@@ -312,7 +308,7 @@ pub fn get_mac_content(sas: &EstablishedSas, ids: &SasIds, flow_id: &FlowId) ->
mac.insert(
key_id.to_string(),
Base64::parse(sas.calculate_mac_invalid_base64(&key, &format!("{}{}", info, key_id)))
Base64::parse(sas.calculate_mac_invalid_base64(&key, &format!("{info}{key_id}")))
.expect("Can't base64-decode SAS MAC"),
);
@@ -321,10 +317,9 @@ pub fn get_mac_content(sas: &EstablishedSas, ids: &SasIds, flow_id: &FlowId) ->
if let Some(key) = own_identity.master_key().get_first_key() {
let key_id = format!("{}:{}", DeviceKeyAlgorithm::Ed25519, key.to_base64());
let calculated_mac = Base64::parse(sas.calculate_mac_invalid_base64(
&key.to_base64(),
&format!("{}{}", info, &key_id),
))
let calculated_mac = Base64::parse(
sas.calculate_mac_invalid_base64(&key.to_base64(), &format!("{info}{key_id}")),
)
.expect("Can't base64-decode SAS Master key MAC");
mac.insert(key_id, calculated_mac);
@@ -473,7 +473,12 @@ impl Sas {
}
pub(crate) fn content_to_request(&self, content: AnyToDeviceEventContent) -> ToDeviceRequest {
ToDeviceRequest::new(self.other_user_id(), self.other_device_id().to_owned(), content)
ToDeviceRequest::with_id(
self.other_user_id(),
self.other_device_id().to_owned(),
content,
TransactionId::new(),
)
}
}
@@ -508,7 +513,7 @@ impl AcceptSettings {
#[cfg(test)]
mod tests {
use std::{convert::TryFrom, sync::Arc};
use std::sync::Arc;
use matrix_sdk_common::locks::Mutex;
use matrix_sdk_test::async_test;
@@ -13,7 +13,6 @@
// limitations under the License.
use std::{
convert::{TryFrom, TryInto},
matches,
sync::{Arc, Mutex},
time::Duration,
@@ -546,10 +545,10 @@ impl SasState<Started> {
let commitment = calculate_commitment(our_public_key, content);
info!(
"Calculated commitment for pubkey {} and content {:?} {}",
our_public_key.to_base64(),
content,
commitment
public_key = our_public_key.to_base64(),
%commitment,
?content,
"Calculated SAS commitment",
);
if let Ok(accepted_protocols) = AcceptedProtocols::try_from(method_content) {
@@ -1224,8 +1223,6 @@ impl SasState<Cancelled> {
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
use matrix_sdk_test::async_test;
use ruma::{
device_id,
+6 -4
View File
@@ -15,17 +15,19 @@ rustdoc-args = ["--cfg", "docsrs"]
[features]
default = ["e2e-encryption"]
e2e-encryption = ["matrix-sdk-base/e2e-encryption", "dep:matrix-sdk-crypto"]
experimental-timeline = ["matrix-sdk-base/experimental-timeline"]
e2e-encryption = ["matrix-sdk-base/e2e-encryption", "dep:matrix-sdk-crypto", "dashmap"]
experimental-timeline = ["matrix-sdk-base/experimental-timeline", "dep:futures-util"]
[dependencies]
anyhow = "1.0.57"
async-trait = "0.1.53"
base64 = "0.13.0"
dashmap = "5.2.0"
futures-util = { version = "0.3.21", default-features = false }
dashmap = { version = "5.2.0", optional = true }
derive_builder = "0.11.2"
futures-util = { version = " 0.3.21", default-features = false, features = ["alloc"], optional = true }
indexed_db_futures = "0.2.3"
js-sys = { version = "0.3.58" }
matrix-sdk-base = { version = "0.5.0", path = "../matrix-sdk-base" }
matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto", optional = true }
matrix-sdk-store-encryption = { version = "0.1.0", path = "../matrix-sdk-store-encryption" }
+18 -14
View File
@@ -19,7 +19,7 @@ pub use cryptostore::IndexeddbStore as CryptoStore;
#[cfg(feature = "e2e-encryption")]
use cryptostore::IndexeddbStoreError;
#[cfg(target_arch = "wasm32")]
pub use state_store::IndexeddbStore as StateStore;
pub use state_store::{IndexeddbStore as StateStore, IndexeddbStoreBuilder as StateStoreBuilder};
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "e2e-encryption")]
@@ -30,17 +30,18 @@ async fn open_stores_with_name(
passphrase: Option<&str>,
) -> Result<(StateStore, CryptoStore), OpenStoreError> {
let name = name.into();
let mut builder = StateStore::builder();
builder.name(name.clone());
if let Some(passphrase) = passphrase {
let state_store = StateStore::open_with_passphrase(name.clone(), passphrase).await?;
let crypto_store =
CryptoStore::open_with_store_cipher(name, state_store.store_cipher.clone()).await?;
Ok((state_store, crypto_store))
} else {
let state_store = StateStore::open_with_name(name.clone()).await?;
let crypto_store = CryptoStore::open_with_name(name).await?;
Ok((state_store, crypto_store))
builder.passphrase(passphrase.to_owned());
}
let state_store = builder.build().await.map_err(StoreError::from)?;
let crypto_store =
CryptoStore::open_with_store_cipher(name, state_store.store_cipher.clone()).await?;
Ok((state_store, crypto_store))
}
#[cfg(target_arch = "wasm32")]
@@ -61,11 +62,14 @@ pub async fn make_store_config(
#[cfg(not(feature = "e2e-encryption"))]
{
let state_store = if let Some(passphrase) = passphrase {
StateStore::open_with_passphrase(name, passphrase).await?
} else {
StateStore::open_with_name(name).await?
};
let mut builder = StateStore::builder();
builder.name(name.clone());
if let Some(passphrase) = passphrase {
builder.passphrase(passphrase.to_owned());
}
let state_store = builder.build().await.map_err(StoreError::from)?;
Ok(StoreConfig::new().state_store(state_store))
}
@@ -72,8 +72,7 @@ pub trait SafeEncode {
store_cipher: &StoreCipher,
i: usize,
) -> JsValue {
format!("{}{}{:016x}", self.as_secure_string(table_name, store_cipher), KEY_SEPARATOR, i,)
.into()
format!("{}{KEY_SEPARATOR}{i:016x}", self.as_secure_string(table_name, store_cipher)).into()
}
/// Encode self into a IdbKeyRange for searching all keys that are
+461 -144
View File
@@ -12,13 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::BTreeSet, sync::Arc};
use std::{
collections::BTreeSet,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use anyhow::anyhow;
use async_trait::async_trait;
use derive_builder::Builder;
#[cfg(feature = "experimental-timeline")]
use futures_util::stream;
use indexed_db_futures::prelude::*;
use js_sys::Date as JsDate;
use matrix_sdk_base::{
deserialized_responses::MemberEvent,
media::{MediaRequest, UniqueKey},
@@ -46,6 +54,8 @@ use ruma::{
RoomVersionId,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
#[cfg(feature = "experimental-timeline")]
use tracing::{info, warn};
use wasm_bindgen::JsValue;
use web_sys::IdbKeyRange;
@@ -55,7 +65,7 @@ use crate::safe_encode::SafeEncode;
struct StoreKeyWrapper(Vec<u8>);
#[derive(Debug, thiserror::Error)]
pub enum SerializationError {
pub enum IndexeddbStoreError {
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
@@ -64,11 +74,28 @@ pub enum SerializationError {
DomException { name: String, message: String, code: u16 },
#[error(transparent)]
StoreError(#[from] StoreError),
#[error("Can't migrate {name} from {old_version} to {new_version} without deleting data. See MigrationConflictStrategy for ways to configure.")]
MigrationConflict { name: String, old_version: f64, new_version: f64 },
}
impl From<indexed_db_futures::web_sys::DomException> for SerializationError {
fn from(frm: indexed_db_futures::web_sys::DomException) -> SerializationError {
SerializationError::DomException {
/// Sometimes Migrations can't proceed without having to drop existing
/// data. This allows you to configure, how these cases should be handled.
#[allow(dead_code)]
#[derive(PartialEq, Clone, Debug)]
pub enum MigrationConflictStrategy {
/// Just drop the data, we don't care that we have to sync again
Drop,
/// Raise a `IndexedDBStore::MigrationConflict` error with the path to the
/// DB in question. The caller then has to take care about what they want
/// to do and try again after.
Raise,
/// Default.
BackupAndDrop,
}
impl From<indexed_db_futures::web_sys::DomException> for IndexeddbStoreError {
fn from(frm: indexed_db_futures::web_sys::DomException) -> IndexeddbStoreError {
IndexeddbStoreError::DomException {
name: frm.name(),
message: frm.message(),
code: frm.code(),
@@ -76,22 +103,20 @@ impl From<indexed_db_futures::web_sys::DomException> for SerializationError {
}
}
impl From<SerializationError> for StoreError {
fn from(e: SerializationError) -> Self {
impl From<IndexeddbStoreError> for StoreError {
fn from(e: IndexeddbStoreError) -> Self {
match e {
SerializationError::Json(e) => StoreError::Json(e),
SerializationError::StoreError(e) => e,
SerializationError::Encryption(e) => match e {
IndexeddbStoreError::Json(e) => StoreError::Json(e),
IndexeddbStoreError::StoreError(e) => e,
IndexeddbStoreError::Encryption(e) => match e {
EncryptionError::Random(e) => StoreError::Encryption(e.to_string()),
EncryptionError::Serialization(e) => StoreError::Json(e),
EncryptionError::Encryption(e) => StoreError::Encryption(e.to_string()),
EncryptionError::Version(found, expected) => StoreError::Encryption(format!(
"Bad Database Encryption Version: expected {} found {}",
expected, found
"Bad Database Encryption Version: expected {expected}, found {found}",
)),
EncryptionError::Length(found, expected) => StoreError::Encryption(format!(
"The database key an invalid length: expected {} found {}",
expected, found
"The database key an invalid length: expected {expected}, found {found}",
)),
},
_ => StoreError::backend(e),
@@ -103,6 +128,12 @@ impl From<SerializationError> for StoreError {
mod KEYS {
// STORES
pub const CURRENT_DB_VERSION: f64 = 1.1;
pub const CURRENT_META_DB_VERSION: f64 = 2.0;
pub const INTERNAL_STATE: &str = "matrix-sdk-state";
pub const BACKUPS_META: &str = "backups";
pub const SESSION: &str = "session";
pub const ACCOUNT_DATA: &str = "account_data";
@@ -137,16 +168,247 @@ mod KEYS {
pub const CUSTOM: &str = "custom";
pub const SYNC_TOKEN: &str = "sync_token";
/// All names of the state stores for convenience.
pub const ALL_STORES: &[&str] = &[
SESSION,
ACCOUNT_DATA,
MEMBERS,
PROFILES,
DISPLAY_NAMES,
JOINED_USER_IDS,
INVITED_USER_IDS,
ROOM_STATE,
ROOM_INFOS,
PRESENCE,
ROOM_ACCOUNT_DATA,
STRIPPED_ROOM_INFOS,
STRIPPED_MEMBERS,
STRIPPED_ROOM_STATE,
STRIPPED_JOINED_USER_IDS,
STRIPPED_INVITED_USER_IDS,
ROOM_USER_RECEIPTS,
ROOM_EVENT_RECEIPTS,
MEDIA,
CUSTOM,
SYNC_TOKEN,
#[cfg(feature = "experimental-timeline")]
ROOM_TIMELINE,
#[cfg(feature = "experimental-timeline")]
ROOM_TIMELINE_METADATA,
#[cfg(feature = "experimental-timeline")]
ROOM_EVENT_ID_TO_POSITION,
];
// static keys
pub const STORE_KEY: &str = "store_key";
pub const FILTER: &str = "filter";
pub const SYNC_TOKEN: &str = "sync_token";
}
pub use KEYS::ALL_STORES;
fn drop_stores(db: &IdbDatabase) -> Result<(), JsValue> {
for name in ALL_STORES {
db.delete_object_store(name)?;
}
Ok(())
}
fn create_stores(db: &IdbDatabase) -> Result<(), JsValue> {
for name in ALL_STORES {
db.create_object_store(name)?;
}
Ok(())
}
async fn backup(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> {
let now = JsDate::now();
let backup_name = format!("backup-{}-{}", source.name(), now);
let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&backup_name, source.version())?;
db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
// migrating to version 1
let db = evt.db();
for name in ALL_STORES {
db.create_object_store(name)?;
}
Ok(())
}));
let target = db_req.into_future().await?;
for name in ALL_STORES {
let tx = target.transaction_on_one_with_mode(name, IdbTransactionMode::Readwrite)?;
let obj = tx.object_store(name)?;
if let Some(curs) = source
.transaction_on_one_with_mode(name, IdbTransactionMode::Readonly)?
.object_store(name)?
.open_cursor()?
.await?
{
while let Some(key) = curs.key() {
obj.put_key_val(&key, &curs.value())?;
curs.continue_cursor()?.await?;
}
}
tx.await.into_result()?;
}
let tx =
meta.transaction_on_one_with_mode(KEYS::BACKUPS_META, IdbTransactionMode::Readwrite)?;
let backup_store = tx.object_store(KEYS::BACKUPS_META)?;
backup_store.put_key_val(&JsValue::from_f64(now), &JsValue::from_str(&backup_name))?;
tx.await;
Ok(())
}
#[derive(Builder, Debug, PartialEq)]
#[builder(name = "IndexeddbStoreBuilder", build_fn(skip))]
pub struct IndexeddbStoreBuilderConfig {
/// The name for the indexeddb store to use, `state` is none given
name: String,
/// The password the indexeddb should be encrypted with. If not given, the
/// DB is not encrypted
passphrase: String,
/// The strategy to use when a merge conflict is found, see
/// [`MigrationConflictStrategy`] for details
#[builder(default = "MigrationConflictStrategy::BackupAndDrop")]
migration_conflict_strategy: MigrationConflictStrategy,
}
impl IndexeddbStoreBuilder {
pub async fn build(&mut self) -> Result<IndexeddbStore> {
let migration_strategy = self
.migration_conflict_strategy
.clone()
.unwrap_or(MigrationConflictStrategy::BackupAndDrop);
let name = self.name.clone().unwrap_or_else(|| "state".to_owned());
let meta_name = format!("{}::{}", name, KEYS::INTERNAL_STATE);
let mut db_req: OpenDbRequest =
IdbDatabase::open_f64(&meta_name, KEYS::CURRENT_META_DB_VERSION)?;
db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
let db = evt.db();
if evt.old_version() < 1.0 {
// migrating to version 1
db.create_object_store(KEYS::INTERNAL_STATE)?;
db.create_object_store(KEYS::BACKUPS_META)?;
} else if evt.old_version() < 2.0 {
db.create_object_store(KEYS::BACKUPS_META)?;
}
Ok(())
}));
let meta_db: IdbDatabase = db_req.into_future().await?;
let store_cipher = if let Some(passphrase) = &self.passphrase {
let tx: IdbTransaction<'_> = meta_db.transaction_on_one_with_mode(
KEYS::INTERNAL_STATE,
IdbTransactionMode::Readwrite,
)?;
let ob = tx.object_store(KEYS::INTERNAL_STATE)?;
let cipher = if let Some(StoreKeyWrapper(inner)) = ob
.get(&JsValue::from_str(KEYS::STORE_KEY))?
.await?
.map(|v| v.into_serde())
.transpose()?
{
StoreCipher::import(passphrase, &inner)?
} else {
let cipher = StoreCipher::new()?;
ob.put_key_val(
&JsValue::from_str(KEYS::STORE_KEY),
&JsValue::from_serde(&StoreKeyWrapper(cipher.export(passphrase)?))?,
)?;
cipher
};
tx.await.into_result()?;
Some(Arc::new(cipher))
} else {
None
};
let recreate_stores = {
// checkup up in a separate call, whether we have to backup or do anything else
// to the db. Unfortunately the set_on_upgrade_needed doesn't allow async fn
// which we need to execute the backup.
let has_store_cipher = store_cipher.is_some();
let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&name, 1.0)?;
let created = Arc::new(AtomicBool::new(false));
let created_inner = created.clone();
db_req.set_on_upgrade_needed(Some(
move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
// in case this is a fresh db, we dont't want to trigger
// further migrations other than just creating the full
// schema.
if evt.old_version() < 1.0 {
create_stores(evt.db())?;
created_inner.store(true, Ordering::Relaxed);
}
Ok(())
},
));
let pre_db = db_req.into_future().await?;
let old_version = pre_db.version();
if created.load(Ordering::Relaxed) {
// this is a fresh DB, return
false
} else if old_version == 1.0 && has_store_cipher {
match migration_strategy {
MigrationConflictStrategy::BackupAndDrop => {
backup(&pre_db, &meta_db).await?;
true
}
MigrationConflictStrategy::Drop => true,
MigrationConflictStrategy::Raise => {
return Err(IndexeddbStoreError::MigrationConflict {
name,
old_version,
new_version: KEYS::CURRENT_DB_VERSION,
})
}
}
} else {
// Nothing to be done
false
}
};
let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&name, KEYS::CURRENT_DB_VERSION)?;
db_req.set_on_upgrade_needed(Some(
move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
// changing the format can only happen in the upgrade procedure
if recreate_stores {
drop_stores(evt.db())?;
create_stores(evt.db())?;
}
Ok(())
},
));
let db = db_req.into_future().await?;
Ok(IndexeddbStore { name, inner: db, meta: meta_db, store_cipher })
}
}
pub struct IndexeddbStore {
name: String,
pub(crate) inner: IdbDatabase,
pub(crate) meta: IdbDatabase,
pub(crate) store_cipher: Option<Arc<StoreCipher>>,
}
@@ -156,118 +418,65 @@ impl std::fmt::Debug for IndexeddbStore {
}
}
type Result<A, E = SerializationError> = std::result::Result<A, E>;
type Result<A, E = IndexeddbStoreError> = std::result::Result<A, E>;
impl IndexeddbStore {
async fn open_helper(name: String, store_cipher: Option<Arc<StoreCipher>>) -> Result<Self> {
// Open my_db v1
let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&name, 1.0)?;
db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
if evt.old_version() < 1.0 {
// migrating to version 1
let db = evt.db();
/// Generate a IndexeddbStoreBuilder with default parameters
pub fn builder() -> IndexeddbStoreBuilder {
IndexeddbStoreBuilder::default()
}
db.create_object_store(KEYS::SESSION)?;
db.create_object_store(KEYS::SYNC_TOKEN)?;
db.create_object_store(KEYS::ACCOUNT_DATA)?;
/// Whether this database has any migration backups
pub async fn has_backups(&self) -> Result<bool> {
Ok(self
.meta
.transaction_on_one_with_mode(KEYS::BACKUPS_META, IdbTransactionMode::Readonly)?
.object_store(KEYS::BACKUPS_META)?
.count()?
.await?
> 0)
}
db.create_object_store(KEYS::MEMBERS)?;
db.create_object_store(KEYS::PROFILES)?;
db.create_object_store(KEYS::DISPLAY_NAMES)?;
db.create_object_store(KEYS::JOINED_USER_IDS)?;
db.create_object_store(KEYS::INVITED_USER_IDS)?;
db.create_object_store(KEYS::ROOM_STATE)?;
db.create_object_store(KEYS::ROOM_INFOS)?;
db.create_object_store(KEYS::PRESENCE)?;
db.create_object_store(KEYS::ROOM_ACCOUNT_DATA)?;
db.create_object_store(KEYS::STRIPPED_ROOM_INFOS)?;
db.create_object_store(KEYS::STRIPPED_MEMBERS)?;
db.create_object_store(KEYS::STRIPPED_ROOM_STATE)?;
db.create_object_store(KEYS::STRIPPED_JOINED_USER_IDS)?;
db.create_object_store(KEYS::STRIPPED_INVITED_USER_IDS)?;
db.create_object_store(KEYS::ROOM_USER_RECEIPTS)?;
db.create_object_store(KEYS::ROOM_EVENT_RECEIPTS)?;
#[cfg(feature = "experimental-timeline")]
{
db.create_object_store(KEYS::ROOM_TIMELINE)?;
db.create_object_store(KEYS::ROOM_TIMELINE_METADATA)?;
db.create_object_store(KEYS::ROOM_EVENT_ID_TO_POSITION)?;
}
db.create_object_store(KEYS::MEDIA)?;
db.create_object_store(KEYS::CUSTOM)?;
}
Ok(())
}));
let db: IdbDatabase = db_req.into_future().await?;
Ok(Self { name, inner: db, store_cipher })
/// What's the database name of the latest backup<
pub async fn latest_backup(&self) -> Result<Option<String>> {
Ok(self
.meta
.transaction_on_one_with_mode(KEYS::BACKUPS_META, IdbTransactionMode::Readonly)?
.object_store(KEYS::BACKUPS_META)?
.open_cursor_with_direction(indexed_db_futures::prelude::IdbCursorDirection::Prev)?
.await?
.and_then(|c| c.value().as_string()))
}
#[allow(dead_code)]
#[deprecated(note = "Use IndexeddbStoreBuilder instead.")]
pub async fn open() -> StoreResult<Self> {
Ok(IndexeddbStore::open_helper("state".to_owned(), None).await?)
IndexeddbStore::builder()
.name("state".to_owned())
.build()
.await
.map_err(StoreError::backend)
}
#[deprecated(note = "Use IndexeddbStoreBuilder instead.")]
pub async fn open_with_passphrase(name: String, passphrase: &str) -> StoreResult<Self> {
Ok(Self::inner_open_with_passphrase(name, passphrase).await?)
}
pub(crate) async fn inner_open_with_passphrase(name: String, passphrase: &str) -> Result<Self> {
let name = format!("{:0}::matrix-sdk-state", name);
let mut db_req: OpenDbRequest = IdbDatabase::open_u32(&name, 1)?;
db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
if evt.old_version() < 1.0 {
// migrating to version 1
let db = evt.db();
db.create_object_store("matrix-sdk-state")?;
}
Ok(())
}));
let db: IdbDatabase = db_req.into_future().await?;
let tx: IdbTransaction<'_> =
db.transaction_on_one_with_mode("matrix-sdk-state", IdbTransactionMode::Readwrite)?;
let ob = tx.object_store("matrix-sdk-state")?;
let cipher = if let Some(StoreKeyWrapper(inner)) = ob
.get(&JsValue::from_str(KEYS::STORE_KEY))?
.await?
.map(|v| v.into_serde())
.transpose()?
{
StoreCipher::import(passphrase, &inner)?
} else {
let cipher = StoreCipher::new()?;
ob.put_key_val(
&JsValue::from_str(KEYS::STORE_KEY),
&JsValue::from_serde(&StoreKeyWrapper(cipher.export(passphrase)?))?,
)?;
cipher
};
tx.await.into_result()?;
IndexeddbStore::open_helper(name, Some(cipher.into())).await
IndexeddbStore::builder()
.name(name)
.passphrase(passphrase.to_owned())
.build()
.await
.map_err(StoreError::backend)
}
#[deprecated(note = "Use IndexeddbStoreBuilder instead.")]
pub async fn open_with_name(name: String) -> StoreResult<Self> {
Ok(IndexeddbStore::open_helper(name, None).await?)
IndexeddbStore::builder().name(name).build().await.map_err(StoreError::backend)
}
fn serialize_event(
&self,
event: &impl Serialize,
) -> std::result::Result<JsValue, SerializationError> {
) -> std::result::Result<JsValue, IndexeddbStoreError> {
Ok(match &self.store_cipher {
Some(cipher) => JsValue::from_serde(&cipher.encrypt_value_typed(event)?)?,
None => JsValue::from_serde(event)?,
@@ -277,7 +486,7 @@ impl IndexeddbStore {
fn deserialize_event<T: DeserializeOwned>(
&self,
event: JsValue,
) -> std::result::Result<T, SerializationError> {
) -> std::result::Result<T, IndexeddbStoreError> {
match &self.store_cipher {
Some(cipher) => Ok(cipher.decrypt_value_typed(event.into_serde()?)?),
None => Ok(event.into_serde()?),
@@ -298,7 +507,7 @@ impl IndexeddbStore {
&self,
table_name: &str,
key: T,
) -> Result<IdbKeyRange, SerializationError>
) -> Result<IdbKeyRange, IndexeddbStoreError>
where
T: SafeEncode,
{
@@ -306,7 +515,7 @@ impl IndexeddbStore {
Some(cipher) => key.encode_to_range_secure(table_name, cipher),
None => key.encode_to_range(),
}
.map_err(|e| SerializationError::StoreError(StoreError::Backend(anyhow!(e).into())))
.map_err(|e| IndexeddbStoreError::StoreError(StoreError::Backend(anyhow!(e).into())))
}
#[cfg(feature = "experimental-timeline")]
@@ -644,17 +853,14 @@ impl IndexeddbStore {
for (room_id, timeline) in &changes.timeline {
if timeline.sync {
tracing::info!("Save new timeline batch from sync response for {}", room_id);
info!(%room_id, "Saving new timeline batch from sync response");
} else {
tracing::info!(
"Save new timeline batch from messages response for {}",
room_id
);
info!(%room_id, "Saving new timeline batch from messages response");
}
let metadata: Option<TimelineMetadata> = if timeline.limited {
tracing::info!(
"Delete stored timeline for {} because the sync response was limited",
room_id
info!(
%room_id,
"Deleting stored timeline because the sync response was limited",
);
let stores = &[
@@ -681,7 +887,7 @@ impl IndexeddbStore {
// This should only happen when a developer adds a wrong timeline
// batch to the `StateChanges` or the server returns a wrong response
// to our request.
tracing::warn!("Drop unexpected timeline batch for {}", room_id);
warn!(%room_id, "Dropping unexpected timeline batch");
return Ok(());
}
@@ -705,9 +911,9 @@ impl IndexeddbStore {
}
if delete_timeline {
tracing::info!(
"Delete stored timeline for {} because of duplicated events",
room_id
info!(
%room_id,
"Deleting stored timeline because of duplicated events",
);
let stores = &[
@@ -754,9 +960,8 @@ impl IndexeddbStore {
.transpose()?
.and_then(|info| info.room_version().cloned())
.unwrap_or_else(|| {
tracing::warn!(
"Unable to find the room version for {}, assume version 9",
room_id
warn!(
"Unable to find the room version for {room_id}, assume version 9",
);
RoomVersionId::V9
});
@@ -838,7 +1043,7 @@ impl IndexeddbStore {
}
}
tx.await.into_result().map_err::<SerializationError, _>(|e| e.into())
tx.await.into_result().map_err::<IndexeddbStoreError, _>(|e| e.into())
}
pub async fn get_presence_event(&self, user_id: &UserId) -> Result<Option<Raw<PresenceEvent>>> {
@@ -1163,7 +1368,7 @@ impl IndexeddbStore {
tx.object_store(KEYS::CUSTOM)?.put_key_val(&jskey, &self.serialize_event(&value)?)?;
tx.await.into_result().map_err::<SerializationError, _>(|e| e.into())?;
tx.await.into_result().map_err::<IndexeddbStoreError, _>(|e| e.into())?;
Ok(prev)
}
@@ -1236,7 +1441,7 @@ impl IndexeddbStore {
store.delete(&key)?;
}
}
tx.await.into_result().map_err::<SerializationError, _>(|e| e.into())
tx.await.into_result().map_err::<IndexeddbStoreError, _>(|e| e.into())
}
#[cfg(feature = "experimental-timeline")]
@@ -1259,7 +1464,7 @@ impl IndexeddbStore {
{
Some(tl) => tl,
_ => {
tracing::info!("No timeline for {} was previously stored", room_id);
info!(%room_id, "Couldn't find a previously stored timeline");
return Ok(None);
}
};
@@ -1275,11 +1480,7 @@ impl IndexeddbStore {
let stream = Box::pin(stream::iter(timeline.into_iter()));
tracing::info!(
"Found previously stored timeline for {}, with end token {:?}",
room_id,
end_token
);
info!(%room_id, ?end_token, "Found previously stored timeline");
Ok(Some((stream, end_token)))
}
@@ -1475,8 +1676,8 @@ mod tests {
use super::{IndexeddbStore, Result};
async fn get_store() -> Result<IndexeddbStore> {
let db_name = format!("test-state-plain-{}", Uuid::new_v4().as_hyphenated().to_string());
Ok(IndexeddbStore::open_helper(db_name, None).await?)
let db_name = format!("test-state-plain-{}", Uuid::new_v4().as_hyphenated());
Ok(IndexeddbStore::builder().name(db_name).build().await?)
}
statestore_integration_tests! { integration }
@@ -1487,19 +1688,135 @@ mod encrypted_tests {
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
use std::sync::Arc;
use matrix_sdk_base::statestore_integration_tests;
use uuid::Uuid;
use super::{IndexeddbStore, Result, StoreCipher};
use super::{IndexeddbStore, Result};
async fn get_store() -> Result<IndexeddbStore> {
let db_name =
format!("test-state-encrypted-{}", Uuid::new_v4().as_hyphenated().to_string());
let key = StoreCipher::new()?;
Ok(IndexeddbStore::open_helper(db_name, Some(Arc::new(key))).await?)
let db_name = format!("test-state-encrypted-{}", Uuid::new_v4().as_hyphenated());
let passphrase = format!("some_passphrase-{}", Uuid::new_v4().as_hyphenated());
Ok(IndexeddbStore::builder().name(db_name).passphrase(passphrase).build().await?)
}
statestore_integration_tests! { integration }
}
#[cfg(test)]
mod migration_tests {
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
use indexed_db_futures::prelude::*;
use matrix_sdk_test::async_test;
use uuid::Uuid;
use wasm_bindgen::JsValue;
use super::{
IndexeddbStore, IndexeddbStoreError, MigrationConflictStrategy, Result, ALL_STORES,
};
pub async fn create_fake_db(name: &str, version: f64) -> Result<()> {
let mut db_req: OpenDbRequest = IdbDatabase::open_f64(name, version)?;
db_req.set_on_upgrade_needed(Some(
move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
// migrating to version 1
let db = evt.db();
for name in ALL_STORES {
db.create_object_store(name)?;
}
Ok(())
},
));
db_req.into_future().await?;
Ok(())
}
#[async_test]
pub async fn test_no_upgrade() -> Result<()> {
let name = format!("simple-1.1-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
// this transparently migrates to the latest version
let store = IndexeddbStore::builder().name(name).build().await?;
// this didn't create any backup
assert_eq!(store.has_backups().await?, false);
// simple check that the layout exists.
assert_eq!(store.get_sync_token().await?, None);
Ok(())
}
#[async_test]
pub async fn test_migrating_v1_to_1_1_plain() -> Result<()> {
let name =
format!("migrating-1.1-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
create_fake_db(&name, 1.0).await?;
// this transparently migrates to the latest version
let store = IndexeddbStore::builder().name(name).build().await?;
// this didn't create any backup
assert_eq!(store.has_backups().await?, false);
assert_eq!(store.get_sync_token().await?, None);
Ok(())
}
#[async_test]
pub async fn test_migrating_v1_to_1_1_with_pw() -> Result<()> {
let name =
format!("migrating-1.1-with-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
let passphrase = "somepassphrase".to_owned();
create_fake_db(&name, 1.0).await?;
// this transparently migrates to the latest version
let store = IndexeddbStore::builder().name(name).passphrase(passphrase).build().await?;
// this creates a backup by default
assert_eq!(store.has_backups().await?, true);
assert!(store.latest_backup().await?.is_some(), "No backup_found");
assert_eq!(store.get_sync_token().await?, None);
Ok(())
}
#[async_test]
pub async fn test_migrating_v1_to_1_1_with_pw_drops() -> Result<()> {
let name = format!(
"migrating-1.1-with-cipher-drops-{}",
Uuid::new_v4().as_hyphenated().to_string()
);
let passphrase = "some-other-passphrase".to_owned();
create_fake_db(&name, 1.0).await?;
// this transparently migrates to the latest version
let store = IndexeddbStore::builder()
.name(name)
.passphrase(passphrase)
.migration_conflict_strategy(MigrationConflictStrategy::Drop)
.build()
.await?;
// this creates a backup by default
assert_eq!(store.has_backups().await?, false);
assert_eq!(store.get_sync_token().await?, None);
Ok(())
}
#[async_test]
pub async fn test_migrating_v1_to_1_1_with_pw_raise() -> Result<()> {
let name = format!(
"migrating-1.1-with-cipher-raises-{}",
Uuid::new_v4().as_hyphenated().to_string()
);
let passphrase = "some-other-passphrase".to_owned();
create_fake_db(&name, 1.0).await?;
// this transparently migrates to the latest version
let store_res = IndexeddbStore::builder()
.name(name)
.passphrase(passphrase)
.migration_conflict_strategy(MigrationConflictStrategy::Raise)
.build()
.await;
if let Err(IndexeddbStoreError::MigrationConflict { .. }) = store_res {
// all fine!
} else {
assert!(false, "Conflict didn't raise: {:?}", store_res)
}
Ok(())
}
}
+1 -1
View File
@@ -30,4 +30,4 @@ thiserror = "1.0.30"
[dependencies.vodozemac]
git = "https://github.com/matrix-org/vodozemac/"
rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd"
rev = "18bcbc3359298894415931547ea41abb75af2d4a"
+1 -1
View File
@@ -33,7 +33,7 @@ pub use types::{
#[cfg(test)]
mod tests {
#[cfg(feature = "decode_image")]
use std::{convert::TryFrom, io::Cursor};
use std::io::Cursor;
#[cfg(feature = "decode_image")]
use image::{ImageFormat, Luma};
-2
View File
@@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::convert::TryInto;
#[cfg(feature = "decode_image")]
use image::{GenericImage, GenericImageView, Luma};
use qrcode::{bits::Bits, EcLevel, QrCode, Version};
+3
View File
@@ -31,6 +31,8 @@ experimental-timeline = [
async-stream = "0.3.3"
async-trait = "0.1.53"
dashmap = "5.2.0"
derive_builder = "0.11.2"
fs_extra = "1.2.0"
futures-core = "0.3.21"
futures-util = { version = "0.3.21", default-features = false }
matrix-sdk-base = { version = "0.5.0", path = "../matrix-sdk-base", optional = true }
@@ -46,6 +48,7 @@ tokio = { version = "1.17.0", default-features = false, features = ["sync", "fs"
tracing = "0.1.34"
[dev-dependencies]
glob = "0.3.0"
matrix-sdk-base = { path = "../matrix-sdk-base", features = ["testing"] }
matrix-sdk-crypto = { path = "../matrix-sdk-crypto", features = ["testing"] }
matrix-sdk-test = { path = "../../testing/matrix-sdk-test" }
@@ -14,7 +14,6 @@
use std::{
collections::{HashMap, HashSet},
convert::TryInto,
path::{Path, PathBuf},
sync::{Arc, RwLock},
};
+22 -15
View File
@@ -16,7 +16,9 @@ mod state_store;
#[cfg(feature = "crypto-store")]
pub use cryptostore::SledStore as CryptoStore;
#[cfg(feature = "state-store")]
pub use state_store::SledStore as StateStore;
pub use state_store::{
MigrationConflictStrategy, SledStore as StateStore, SledStoreBuilder as StateStoreBuilder,
};
/// All the errors that can occur when opening a sled store.
#[derive(Error, Debug)]
@@ -38,8 +40,10 @@ pub enum OpenStoreError {
}
/// Create a [`StoreConfig`] with an opened sled [`StateStore`] that uses the
/// given path and passphrase. If `encryption` is enabled, a [`CryptoStore`]
/// with the same parameters is also opened.
/// given path and passphrase.
///
/// If the `e2e-encryption` Cargo feature is enabled, a [`CryptoStore`] with the
/// same parameters is also opened.
///
/// [`StoreConfig`]: #StoreConfig
#[cfg(any(feature = "state-store", feature = "crypto-store"))]
@@ -61,11 +65,13 @@ pub fn make_store_config(
#[cfg(not(feature = "crypto-store"))]
{
let state_store = if let Some(passphrase) = passphrase {
StateStore::open_with_passphrase(path, passphrase)?
} else {
StateStore::open_with_path(path)?
let mut store_builder = StateStore::builder();
store_builder.path(path.as_ref().to_path_buf());
if let Some(passphrase) = passphrase {
store_builder.passphrase(passphrase.to_owned());
};
let state_store = store_builder.build().map_err(StoreError::backend)?;
Ok(StoreConfig::new().state_store(state_store))
}
@@ -78,13 +84,14 @@ fn open_stores_with_path(
path: impl AsRef<std::path::Path>,
passphrase: Option<&str>,
) -> Result<(StateStore, CryptoStore), OpenStoreError> {
let mut store_builder = StateStore::builder();
store_builder.path(path.as_ref().to_path_buf());
if let Some(passphrase) = passphrase {
let state_store = StateStore::open_with_passphrase(path, passphrase)?;
let crypto_store = state_store.open_crypto_store()?;
Ok((state_store, crypto_store))
} else {
let state_store = StateStore::open_with_path(path)?;
let crypto_store = state_store.open_crypto_store()?;
Ok((state_store, crypto_store))
}
store_builder.passphrase(passphrase.to_owned());
};
let state_store = store_builder.build().map_err(StoreError::backend)?;
let crypto_store = state_store.open_crypto_store()?;
Ok((state_store, crypto_store))
}
+322 -91
View File
@@ -16,12 +16,13 @@ use std::{
collections::BTreeSet,
path::{Path, PathBuf},
sync::Arc,
time::Instant,
time::{Instant, SystemTime, UNIX_EPOCH},
};
#[cfg(feature = "experimental-timeline")]
use async_stream::stream;
use async_trait::async_trait;
use derive_builder::Builder;
use futures_core::stream::Stream;
use futures_util::stream::{self, StreamExt, TryStreamExt};
use matrix_sdk_base::{
@@ -58,6 +59,7 @@ use sled::{
Config, Db, Transactional, Tree,
};
use tokio::task::spawn_blocking;
use tracing::{debug, info};
#[cfg(feature = "crypto-store")]
use super::OpenStoreError;
@@ -81,6 +83,28 @@ pub enum SledStoreError {
Identifier(#[from] IdParseError),
#[error(transparent)]
Task(#[from] tokio::task::JoinError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
FsExtra(#[from] fs_extra::error::Error),
#[error("Can't migrate {path} from {old_version} to {new_version} without deleting data. See MigrationConflictStrategy for ways to configure.")]
MigrationConflict { path: PathBuf, old_version: usize, new_version: usize },
}
/// Sometimes Migrations can't proceed without having to drop existing
/// data. This allows you to configure, how these cases should be handled.
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum MigrationConflictStrategy {
/// Just drop the data, we don't care that we have to sync again
Drop,
/// Raise a `SledStoreError::MigrationConflict` error with the path to the
/// DB in question. The caller then has to take care about what they want
/// to do and try again after.
Raise,
/// _Default_: The _entire_ database is backed up under
/// `$path.$timestamp.backup` (this includes the crypto store if they
/// are linked), before the state tables are dropped.
BackupAndDrop,
}
impl From<TransactionError<SledStoreError>> for SledStoreError {
@@ -103,12 +127,10 @@ impl Into<StoreError> for SledStoreError {
KeyEncryptionError::Serialization(e) => StoreError::Json(e),
KeyEncryptionError::Encryption(e) => StoreError::Encryption(e.to_string()),
KeyEncryptionError::Version(found, expected) => StoreError::Encryption(format!(
"Bad Database Encryption Version: expected {} found {}",
expected, found
"Bad Database Encryption Version: expected {expected}, found {found}",
)),
KeyEncryptionError::Length(found, expected) => StoreError::Encryption(format!(
"The database key an invalid length: expected {} found {}",
expected, found
"The database key an invalid length: expected {expected}, found {found}",
)),
},
SledStoreError::StoreError(e) => e,
@@ -116,7 +138,7 @@ impl Into<StoreError> for SledStoreError {
}
}
}
const DATABASE_VERSION: u8 = 1;
const DATABASE_VERSION: u8 = 2;
const VERSION_KEY: &str = "state-store-version";
@@ -149,8 +171,135 @@ const TIMELINE_METADATA: &str = "timeline-metadata";
#[cfg(feature = "experimental-timeline")]
const TIMELINE: &str = "timeline";
const ALL_DB_STORES: &[&str] = &[
ACCOUNT_DATA,
SYNC_TOKEN,
DISPLAY_NAME,
INVITED_USER_ID,
JOINED_USER_ID,
MEDIA,
MEMBER,
PRESENCE,
PROFILE,
ROOM_ACCOUNT_DATA,
#[cfg(feature = "experimental-timeline")]
ROOM_EVENT_ID_POSITION,
ROOM_EVENT_RECEIPT,
ROOM_INFO,
ROOM_STATE,
ROOM_USER_RECEIPT,
ROOM,
SESSION,
STRIPPED_INVITED_USER_ID,
STRIPPED_JOINED_USER_ID,
STRIPPED_ROOM_INFO,
STRIPPED_ROOM_MEMBER,
STRIPPED_ROOM_STATE,
CUSTOM,
#[cfg(feature = "experimental-timeline")]
ROOM_EVENT_ID_POSITION,
#[cfg(feature = "experimental-timeline")]
TIMELINE_METADATA,
#[cfg(feature = "experimental-timeline")]
TIMELINE,
];
const ALL_GLOBAL_KEYS: &[&str] = &[VERSION_KEY];
type Result<A, E = SledStoreError> = std::result::Result<A, E>;
#[derive(Builder, Debug, PartialEq, Eq)]
#[builder(name = "SledStoreBuilder", build_fn(skip))]
pub struct SledStoreBuilderConfig {
/// Path to the sled store files, created if not yet existing
path: PathBuf,
/// Set the password the sled store is encrypted with (if any)
passphrase: String,
/// The strategy to use when a merge conflict is found, see
/// [`MigrationConflictStrategy`] for details
#[builder(default = "MigrationConflictStrategy::BackupAndDrop")]
migration_conflict_strategy: MigrationConflictStrategy,
}
impl SledStoreBuilder {
pub fn build(&mut self) -> Result<SledStore> {
let is_temp = self.path.is_none();
let mut cfg = Config::new().temporary(is_temp);
let path = if let Some(path) = &self.path {
let path = path.join("matrix-sdk-state");
cfg = cfg.path(&path);
Some(path)
} else {
None
};
let db = cfg.open().map_err(StoreError::backend)?;
let store_cipher = if let Some(passphrase) = &self.passphrase {
if let Some(inner) = db.get("store_cipher".encode())? {
Some(StoreCipher::import(passphrase, &inner)?.into())
} else {
let cipher = StoreCipher::new()?;
db.insert("store_cipher".encode(), cipher.export(passphrase)?)?;
Some(cipher.into())
}
} else {
None
};
let mut store = SledStore::open_helper(db, path, store_cipher)?;
let migration_res = store.upgrade();
if let Err(SledStoreError::MigrationConflict { path, .. }) = &migration_res {
// how are supposed to react about this?
match self
.migration_conflict_strategy
.as_ref()
.unwrap_or(&MigrationConflictStrategy::BackupAndDrop)
{
MigrationConflictStrategy::BackupAndDrop => {
let mut new_path = path.clone();
new_path.set_extension(format!(
"{}.backup",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time doesn't go backwards")
.as_secs()
));
fs_extra::dir::create_all(&new_path, false)?;
fs_extra::dir::copy(path, new_path, &fs_extra::dir::CopyOptions::new())?;
store.drop_tables()?;
return self.build();
}
MigrationConflictStrategy::Drop => {
store.drop_tables()?;
return self.build();
}
MigrationConflictStrategy::Raise => migration_res?,
}
} else {
migration_res?;
}
Ok(store)
}
// testing only
#[cfg(test)]
fn build_encrypted() -> StoreResult<SledStore> {
let db = Config::new().temporary(true).open().map_err(StoreError::backend)?;
SledStore::open_helper(
db,
None,
Some(StoreCipher::new().expect("can't create store cipher").into()),
)
.map_err(|e| e.into())
}
}
#[derive(Clone)]
pub struct SledStore {
path: Option<PathBuf>,
@@ -194,6 +343,7 @@ impl std::fmt::Debug for SledStore {
}
}
#[allow(deprecated)]
impl SledStore {
fn open_helper(
db: Db,
@@ -234,7 +384,7 @@ impl SledStore {
#[cfg(feature = "experimental-timeline")]
let room_event_id_to_position = db.open_tree(ROOM_EVENT_ID_POSITION)?;
let database = Self {
Ok(Self {
path,
inner: db,
store_cipher,
@@ -264,64 +414,52 @@ impl SledStore {
room_timeline_metadata,
#[cfg(feature = "experimental-timeline")]
room_event_id_to_position,
};
database.upgrade()?;
Ok(database)
})
}
/// Generate a SledStoreBuilder with default parameters
pub fn builder() -> SledStoreBuilder {
SledStoreBuilder::default()
}
#[deprecated(note = "Use SledStoreBuilder instead.")]
pub fn open() -> StoreResult<Self> {
let db = Config::new().temporary(true).open().map_err(StoreError::backend)?;
SledStore::open_helper(db, None, None).map_err(|e| e.into())
}
// testing only
#[cfg(test)]
fn open_encrypted() -> StoreResult<Self> {
let db = Config::new().temporary(true).open().map_err(StoreError::backend)?;
SledStore::open_helper(
db,
None,
Some(StoreCipher::new().expect("can't create store cipher").into()),
)
.map_err(|e| e.into())
SledStore::builder().build().map_err(StoreError::backend)
}
#[deprecated(note = "Use SledStoreBuilder instead.")]
pub fn open_with_passphrase(path: impl AsRef<Path>, passphrase: &str) -> StoreResult<Self> {
Self::inner_open_with_passphrase(path, passphrase).map_err(|e| e.into())
}
fn inner_open_with_passphrase(path: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
let path = path.as_ref().join("matrix-sdk-state");
let db = Config::new().temporary(false).path(&path).open()?;
let store_cipher = if let Some(inner) = db.get("store_cipher".encode())? {
StoreCipher::import(passphrase, &inner)?
} else {
let cipher = StoreCipher::new()?;
db.insert("store_cipher".encode(), cipher.export(passphrase)?)?;
cipher
}
.into();
SledStore::open_helper(db, Some(path), Some(store_cipher))
SledStore::builder()
.path(path.as_ref().into())
.passphrase(passphrase.to_owned())
.build()
.map_err(StoreError::backend)
}
#[deprecated(note = "Use SledStoreBuilder instead.")]
pub fn open_with_path(path: impl AsRef<Path>) -> StoreResult<Self> {
Self::inner_open_with_path(path).map_err(|e| e.into())
SledStore::builder().path(path.as_ref().into()).build().map_err(StoreError::backend)
}
fn inner_open_with_path(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref().join("matrix-sdk-state");
let db = Config::new().temporary(false).path(&path).open()?;
fn drop_tables(self) -> StoreResult<()> {
for name in ALL_DB_STORES {
self.inner.drop_tree(name).map_err(StoreError::backend)?;
}
for name in ALL_GLOBAL_KEYS {
self.inner.remove(name).map_err(StoreError::backend)?;
}
SledStore::open_helper(db, Some(path), None)
Ok(())
}
fn upgrade(&self) -> StoreResult<()> {
let db_version = self.inner.get(VERSION_KEY).map_err(StoreError::backend)?.map(|v| {
fn set_db_version(&self, version: u8) -> Result<()> {
self.inner.insert(VERSION_KEY, version.to_be_bytes().as_ref())?;
self.inner.flush()?;
Ok(())
}
fn upgrade(&mut self) -> Result<()> {
let db_version = self.inner.get(VERSION_KEY)?.map(|v| {
let (version_bytes, _) = v.split_at(std::mem::size_of::<u8>());
u8::from_be_bytes(version_bytes.try_into().unwrap_or_default())
});
@@ -329,11 +467,7 @@ impl SledStore {
let old_version = match db_version {
None => {
// we are fresh, let's write the current version
self.inner
.insert(VERSION_KEY, DATABASE_VERSION.to_be_bytes().as_ref())
.map_err(StoreError::backend)?;
self.inner.flush().map_err(StoreError::backend)?;
return Ok(());
return self.set_db_version(DATABASE_VERSION);
}
Some(version) if version == DATABASE_VERSION => {
// current, we don't have to do anything
@@ -342,16 +476,30 @@ impl SledStore {
Some(version) => version,
};
tracing::debug!(
old_version,
new_version = DATABASE_VERSION,
"Upgrading the Sled state store"
);
debug!(old_version, new_version = DATABASE_VERSION, "Upgrading the Sled state store");
if old_version == 1 {
if self.store_cipher.is_some() {
// we stored some fields un-encrypted. Drop them to force re-creation
return Err(SledStoreError::MigrationConflict {
path: self.path.take().expect("Path must exist for a migration to fail"),
old_version: old_version.into(),
new_version: DATABASE_VERSION.into(),
});
}
// no migration to handle
self.set_db_version(2u8)?;
return Ok(());
}
// FUTURE UPGRADE CODE GOES HERE
// can't upgrade from that version to the new one
Err(StoreError::UnsupportedDatabaseVersion(old_version.into(), DATABASE_VERSION.into()))
Err(SledStoreError::MigrationConflict {
path: self.path.take().expect("Path must exist for a migration to fail"),
old_version: old_version.into(),
new_version: DATABASE_VERSION.into(),
})
}
/// Open a `CryptoStore` that uses the same database as this store.
@@ -687,7 +835,7 @@ impl SledStore {
self.inner.flush_async().await?;
tracing::info!("Saved changes in {:?}", now.elapsed());
info!("Saved changes in {:?}", now.elapsed());
Ok(())
}
@@ -1147,7 +1295,6 @@ impl SledStore {
) -> Result<Option<(BoxStream<StoreResult<SyncRoomEvent>>, Option<String>)>> {
let db = self.clone();
let key = self.encode_key(TIMELINE_METADATA, room_id);
let r_id = room_id.to_owned();
let metadata: Option<TimelineMetadata> = db
.room_timeline_metadata
.get(key.as_slice())?
@@ -1156,7 +1303,7 @@ impl SledStore {
let metadata = match metadata {
Some(m) => m,
None => {
tracing::info!("No timeline for {} was previously stored", r_id);
info!(%room_id, "Couldn't find a previously stored timeline");
return Ok(None);
}
};
@@ -1164,16 +1311,15 @@ impl SledStore {
let mut position = metadata.start_position;
let end_token = metadata.end;
tracing::info!(
"Found previously stored timeline for {}, with end token {:?}",
r_id,
end_token
);
info!(%room_id, ?end_token, "Found previously stored timeline");
let room_id = room_id.to_owned();
let stream = stream! {
while let Ok(Some(item)) = db.room_timeline.get(&db.encode_key_with_counter(TIMELINE, &r_id, position)) {
while let Ok(Some(item)) =
db.room_timeline.get(&db.encode_key_with_counter(TIMELINE, &room_id, position))
{
position += 1;
yield db.deserialize_value(&item).map_err(SledStoreError::from).map_err(|e| e.into());
yield db.deserialize_value(&item).map_err(|e| SledStoreError::from(e).into());
}
};
@@ -1182,7 +1328,7 @@ impl SledStore {
#[cfg(feature = "experimental-timeline")]
async fn remove_room_timeline(&self, room_id: &RoomId) -> Result<()> {
tracing::info!("Remove stored timeline for {}", room_id);
info!(%room_id, "Removing stored timeline");
let mut timeline_batch = sled::Batch::default();
for key in self.room_timeline.scan_prefix(self.encode_key(TIMELINE, &room_id)).keys() {
@@ -1219,22 +1365,21 @@ impl SledStore {
#[cfg(feature = "experimental-timeline")]
async fn save_room_timeline(&self, changes: &StateChanges) -> Result<()> {
use tracing::warn;
let mut timeline_batch = sled::Batch::default();
let mut event_id_to_position_batch = sled::Batch::default();
let mut timeline_metadata_batch = sled::Batch::default();
for (room_id, timeline) in &changes.timeline {
if timeline.sync {
tracing::info!("Save new timeline batch from sync response for {}", room_id);
info!(%room_id, "Saving new timeline batch from sync response");
} else {
tracing::info!("Save new timeline batch from messages response for {}", room_id);
info!(%room_id, "Saving new timeline batch from messages response");
}
let metadata: Option<TimelineMetadata> = if timeline.limited {
tracing::info!(
"Delete stored timeline for {} because the sync response was limited",
room_id
);
info!(%room_id, "Deleting stored timeline because the sync response was limited");
self.remove_room_timeline(room_id).await?;
None
} else {
@@ -1248,7 +1393,7 @@ impl SledStore {
// This should only happen when a developer adds a wrong timeline
// batch to the `StateChanges` or the server returns a wrong response
// to our request.
tracing::warn!("Drop unexpected timeline batch for {}", room_id);
warn!(%room_id, "Dropping unexpected timeline batch");
return Ok(());
}
@@ -1266,10 +1411,7 @@ impl SledStore {
}
if delete_timeline {
tracing::info!(
"Delete stored timeline for {} because of duplicated events",
room_id
);
info!(%room_id, "Deleting stored timeline because of duplicated events");
self.remove_room_timeline(room_id).await?;
None
} else if timeline.sync {
@@ -1301,10 +1443,7 @@ impl SledStore {
.transpose()?
.and_then(|info| info.room_version().cloned())
.unwrap_or_else(|| {
tracing::warn!(
"Unable to find the room version for {}, assume version 9",
room_id
);
warn!(%room_id, "Unable to find the room version, assume version 9");
RoomVersionId::V9
});
@@ -1589,7 +1728,7 @@ mod tests {
use super::{SledStore, StateStore, StoreResult};
async fn get_store() -> StoreResult<impl StateStore> {
SledStore::open().map_err(Into::into)
SledStore::builder().build().map_err(Into::into)
}
statestore_integration_tests! { integration }
@@ -1599,11 +1738,103 @@ mod tests {
mod encrypted_tests {
use matrix_sdk_base::statestore_integration_tests;
use super::{SledStore, StateStore, StoreResult};
use super::{SledStoreBuilder, StateStore, StoreResult};
async fn get_store() -> StoreResult<impl StateStore> {
SledStore::open_encrypted().map_err(Into::into)
SledStoreBuilder::build_encrypted().map_err(Into::into)
}
statestore_integration_tests! { integration }
}
#[cfg(test)]
mod migration {
use matrix_sdk_test::async_test;
use tempfile::TempDir;
use super::{MigrationConflictStrategy, Result, SledStore, SledStoreError};
#[async_test]
pub async fn migrating_v1_to_2_plain() -> Result<()> {
let folder = TempDir::new()?;
let store = SledStore::builder().path(folder.path().to_path_buf()).build()?;
store.set_db_version(1u8)?;
drop(store);
// this transparently migrates to the latest version
let _store = SledStore::builder().path(folder.path().to_path_buf()).build()?;
Ok(())
}
#[async_test]
pub async fn migrating_v1_to_2_with_pw_backed_up() -> Result<()> {
let folder = TempDir::new()?;
let store = SledStore::builder()
.path(folder.path().to_path_buf())
.passphrase("something".to_owned())
.build()?;
store.set_db_version(1u8)?;
drop(store);
// this transparently creates a backup and a fresh db
let _store = SledStore::builder()
.path(folder.path().to_path_buf())
.passphrase("something".to_owned())
.build()?;
assert_eq!(std::fs::read_dir(folder.path())?.count(), 2);
Ok(())
}
#[async_test]
pub async fn migrating_v1_to_2_with_pw_drop() -> Result<()> {
let folder = TempDir::new()?;
let store = SledStore::builder()
.path(folder.path().to_path_buf())
.passphrase("other thing".to_owned())
.build()?;
store.set_db_version(1u8)?;
drop(store);
// this transparently creates a backup and a fresh db
let _store = SledStore::builder()
.path(folder.path().to_path_buf())
.passphrase("other thing".to_owned())
.migration_conflict_strategy(MigrationConflictStrategy::Drop)
.build()?;
assert_eq!(std::fs::read_dir(folder.path())?.count(), 1);
Ok(())
}
#[async_test]
pub async fn migrating_v1_to_2_with_pw_raises() -> Result<()> {
let folder = TempDir::new()?;
let store = SledStore::builder()
.path(folder.path().to_path_buf())
.passphrase("secret".to_owned())
.build()?;
store.set_db_version(1u8)?;
drop(store);
// this transparently creates a backup and a fresh db
let res = SledStore::builder()
.path(folder.path().to_path_buf())
.passphrase("secret".to_owned())
.migration_conflict_strategy(MigrationConflictStrategy::Raise)
.build();
if let Err(SledStoreError::MigrationConflict { .. }) = res {
// all good
} else {
panic!("Didn't raise the expected error: {:?}", res);
}
assert_eq!(std::fs::read_dir(folder.path())?.count(), 1);
Ok(())
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ rustdoc-args = ["--cfg", "docsrs"]
default = [
"e2e-encryption",
"sled",
"native-tls"
"native-tls",
]
e2e-encryption = [
+1 -2
View File
@@ -25,7 +25,6 @@ some event handlers and then syncing.
This is demonstrated in the example below.
```rust,no_run
use std::convert::TryFrom;
use matrix_sdk::{
Client, config::SyncSettings,
ruma::{user_id, events::room::message::SyncRoomMessageEvent},
@@ -40,7 +39,7 @@ async fn main() -> anyhow::Result<()> {
client.login_username(alice, "password").send().await?;
client
.register_event_handler(|ev: SyncRoomMessageEvent| async move {
.add_event_handler(|ev: SyncRoomMessageEvent| async move {
println!("Received a message {:?}", ev);
})
.await;
+7 -10
View File
@@ -22,13 +22,13 @@ async fn on_stripped_state_member(
// retry autojoin due to synapse sending invites, before the
// invited user can join for more information see
// https://github.com/matrix-org/synapse/issues/4345
eprintln!("Failed to join room {} ({:?}), retrying in {}s", room.room_id(), err, delay);
eprintln!("Failed to join room {} ({err:?}), retrying in {delay}s", room.room_id());
sleep(Duration::from_secs(delay)).await;
delay *= 2;
if delay > 3600 {
eprintln!("Can't join room {} ({:?})", room.room_id(), err);
eprintln!("Can't join room {} ({err:?})", room.room_id());
break;
}
}
@@ -47,16 +47,13 @@ async fn login_and_sync(
#[cfg(feature = "sled")]
{
// The location to save files to
let mut home = dirs::home_dir().expect("no home directory found");
home.push("autojoin_bot");
let state_store = matrix_sdk_sled::StateStore::open_with_path(home)?;
client_builder = client_builder.state_store(state_store);
let home = dirs::home_dir().expect("no home directory found").join("autojoin_bot");
client_builder = client_builder.sled_store(home, None)?;
}
#[cfg(feature = "indexeddb")]
{
let state_store = matrix_sdk_indexeddb::StateStore::open();
client_builder = client_builder.state_store(state_store);
client_builder = client_builder.indexeddb_store("autojoin_bot", None).await?;
}
let client = client_builder.build().await?;
@@ -67,9 +64,9 @@ async fn login_and_sync(
.send()
.await?;
println!("logged in as {}", username);
println!("logged in as {username}");
client.register_event_handler(on_stripped_state_member).await;
client.add_event_handler(on_stripped_state_member).await;
client.sync(SyncSettings::default()).await;
+5 -8
View File
@@ -42,16 +42,13 @@ async fn login_and_sync(
#[cfg(feature = "sled")]
{
// The location to save files to
let mut home = dirs::home_dir().expect("no home directory found");
home.push("party_bot");
let state_store = matrix_sdk_sled::StateStore::open_with_path(home)?;
client_builder = client_builder.state_store(state_store);
let home = dirs::home_dir().expect("no home directory found").join("party_bot");
client_builder = client_builder.sled_store(home, None)?;
}
#[cfg(feature = "indexeddb")]
{
let state_store = matrix_sdk_indexeddb::StateStore::open();
client_builder = client_builder.state_store(state_store);
client_builder = client_builder.indexeddb_store("party_bot", None).await?;
}
let client = client_builder.build().await.unwrap();
@@ -61,7 +58,7 @@ async fn login_and_sync(
.send()
.await?;
println!("logged in as {}", username);
println!("logged in as {username}");
// An initial sync to set up state and so our bot doesn't respond to old
// messages. If the `StateStore` finds saved state in the location given the
@@ -69,7 +66,7 @@ async fn login_and_sync(
client.sync_once(SyncSettings::default()).await.unwrap();
// add our CommandBot to be notified of incoming messages, we do this after the
// initial sync to avoid responding to messages before the bot was running.
client.register_event_handler(on_room_message).await;
client.add_event_handler(on_room_message).await;
// since we called `sync_once` before we entered our sync loop we must pass
// that sync token to `sync`
+2 -2
View File
@@ -69,7 +69,7 @@ async fn login_and_sync(
client.sync_once(SyncSettings::default()).await.unwrap();
let image = Arc::new(Mutex::new(image));
client.register_event_handler(move |ev, room| on_room_message(ev, room, image.clone())).await;
client.add_event_handler(move |ev, room| on_room_message(ev, room, image.clone())).await;
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
client.sync(settings).await;
@@ -92,7 +92,7 @@ async fn main() -> anyhow::Result<()> {
}
};
println!("helloooo {} {} {} {:#?}", homeserver_url, username, password, image_path);
println!("helloooo {homeserver_url} {username} {password} {image_path:#?}");
let path = PathBuf::from(image_path);
let image = File::open(path).expect("Can't open image file.");

Some files were not shown because too many files have changed in this diff Show More