Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6501e585a | |||
| 0aed05a7d4 | |||
| 0f722f6eb8 | |||
| ea94975adc | |||
| 6d69a19524 | |||
| 73fe6603ab | |||
| e6a15831c3 | |||
| 2ad554a66a | |||
| 1642ac7934 | |||
| d03277a0bb | |||
| 816e3f5ee5 | |||
| caf27caca0 | |||
| 8453495c69 | |||
| 747c00c265 | |||
| 4fda5e5ff0 | |||
| 8096dc3275 | |||
| ddad613db5 | |||
| ec8dc91b25 | |||
| cb55d94d00 | |||
| b263c14e0f | |||
| 4596372869 | |||
| 6a916b4e52 | |||
| 5ba540a883 | |||
| 8d39a1d9b9 | |||
| 49c6e2cb92 | |||
| 38e10dbd38 | |||
| ee06e1d5ba | |||
| 16dda959e5 |
Generated
+87
-20
@@ -166,6 +166,20 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
|
||||
|
||||
[[package]]
|
||||
name = "aquamarine"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df752953c49ce90719c7bf1fc587bc8227aed04732ea0c0f85e5397d7fdbd1a1"
|
||||
dependencies = [
|
||||
"include_dir",
|
||||
"itertools 0.10.5",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.7"
|
||||
@@ -1522,6 +1536,19 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "example-backups"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"futures-util",
|
||||
"matrix-sdk",
|
||||
"tokio",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "example-command-bot"
|
||||
version = "0.1.0"
|
||||
@@ -1567,6 +1594,20 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "example-encryption-recovery"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"futures-util",
|
||||
"matrix-sdk",
|
||||
"tokio",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "example-get-profiles"
|
||||
version = "0.1.0"
|
||||
@@ -1646,6 +1687,19 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "example-secret-storage"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"futures-util",
|
||||
"matrix-sdk",
|
||||
"tokio",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "example-timeline"
|
||||
version = "0.1.0"
|
||||
@@ -2474,6 +2528,25 @@ dependencies = [
|
||||
"bitmaps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e"
|
||||
dependencies = [
|
||||
"include_dir_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir_macros"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.3"
|
||||
@@ -3036,6 +3109,7 @@ version = "0.6.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"anymap2",
|
||||
"aquamarine",
|
||||
"as_variant",
|
||||
"assert-json-diff",
|
||||
"assert_matches",
|
||||
@@ -3086,6 +3160,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tracing",
|
||||
@@ -3943,9 +4018,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "3.9.1"
|
||||
version = "3.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a54938017eacd63036332b4ae5c8a49fc8c0c1d6d629893057e4f13609edd06"
|
||||
checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
@@ -4433,6 +4508,7 @@ dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
@@ -4884,8 +4960,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc39664df66d707506b1dd318c30e600f25ebc1e7feadf4804ae45db67922c53"
|
||||
source = "git+https://github.com/ruma/ruma/?rev=e37918c9b83f4638af4ad76af4887a68bfab1b92#e37918c9b83f4638af4ad76af4887a68bfab1b92"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"js_int",
|
||||
@@ -4901,8 +4976,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-client-api"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b9f07f9a17ff1fcc515eecf8efdb1cb6b92091555dc3ac25d434653b7b09d8d"
|
||||
source = "git+https://github.com/ruma/ruma/?rev=e37918c9b83f4638af4ad76af4887a68bfab1b92#e37918c9b83f4638af4ad76af4887a68bfab1b92"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"bytes",
|
||||
@@ -4920,8 +4994,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-common"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bd6a9c0dd0dc4986bf5fb963765f59fb0e3f462b7ca8ba466f47d217688865c"
|
||||
source = "git+https://github.com/ruma/ruma/?rev=e37918c9b83f4638af4ad76af4887a68bfab1b92#e37918c9b83f4638af4ad76af4887a68bfab1b92"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"base64 0.21.4",
|
||||
@@ -4951,8 +5024,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-events"
|
||||
version = "0.27.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9135a0b84495fabbe46c477a1fcdef181001a215ec4b1f9215320cf980397636"
|
||||
source = "git+https://github.com/ruma/ruma/?rev=e37918c9b83f4638af4ad76af4887a68bfab1b92#e37918c9b83f4638af4ad76af4887a68bfab1b92"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"indexmap 2.0.2",
|
||||
@@ -4976,8 +5048,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-federation-api"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1901c1f27bc327652d58af2a130c73acef3198abeccd24cee97f7267fdf3fe7"
|
||||
source = "git+https://github.com/ruma/ruma/?rev=e37918c9b83f4638af4ad76af4887a68bfab1b92#e37918c9b83f4638af4ad76af4887a68bfab1b92"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4989,8 +5060,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-html"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9674a149b1a6965fe2174ba528c89ee201258abd9209bbe74953df7073a83a5b"
|
||||
source = "git+https://github.com/ruma/ruma/?rev=e37918c9b83f4638af4ad76af4887a68bfab1b92#e37918c9b83f4638af4ad76af4887a68bfab1b92"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"html5ever",
|
||||
@@ -5002,8 +5072,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-identifiers-validation"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf8ad1259274f2f57c20901bd1cc5e4a8f23169d1c1d887b6338b02f058e9b41"
|
||||
source = "git+https://github.com/ruma/ruma/?rev=e37918c9b83f4638af4ad76af4887a68bfab1b92#e37918c9b83f4638af4ad76af4887a68bfab1b92"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"thiserror",
|
||||
@@ -5012,8 +5081,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-macros"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0280534a4b3e34416f883285fac4f9c408cd0b737890ae66f3e7a7056d14be80"
|
||||
source = "git+https://github.com/ruma/ruma/?rev=e37918c9b83f4638af4ad76af4887a68bfab1b92#e37918c9b83f4638af4ad76af4887a68bfab1b92"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro-crate 2.0.0",
|
||||
@@ -5028,8 +5096,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-push-gateway-api"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f343045c4d4a5943f93b5014160af3c7413e6ee32ea47b147e1e91f2a977486b"
|
||||
source = "git+https://github.com/ruma/ruma/?rev=e37918c9b83f4638af4ad76af4887a68bfab1b92#e37918c9b83f4638af4ad76af4887a68bfab1b92"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
|
||||
+3
-2
@@ -36,8 +36,8 @@ futures-executor = "0.3.21"
|
||||
futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] }
|
||||
http = "0.2.6"
|
||||
itertools = "0.11.0"
|
||||
ruma = { version = "0.9.2", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
|
||||
ruma-common = "0.12.0"
|
||||
ruma = { git = "https://github.com/ruma/ruma/", rev = "e37918c9b83f4638af4ad76af4887a68bfab1b92", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
|
||||
ruma-common = { git = "https://github.com/ruma/ruma", rev = "e37918c9b83f4638af4ad76af4887a68bfab1b92" }
|
||||
once_cell = "1.16.0"
|
||||
serde = "1.0.151"
|
||||
serde_html_form = "0.2.0"
|
||||
@@ -46,6 +46,7 @@ sha2 = "0.10.8"
|
||||
stream_assert = "0.1.1"
|
||||
thiserror = "1.0.38"
|
||||
tokio = { version = "1.30.0", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.14"
|
||||
tracing = { version = "0.1.36", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.30"
|
||||
uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "48b1f51f6e5406cab20f502f535b30a589a5b140" }
|
||||
|
||||
@@ -46,7 +46,7 @@ version = "0.6.0"
|
||||
path = "../../crates/matrix-sdk-crypto"
|
||||
version = "0.6.0"
|
||||
default_features = false
|
||||
features = ["qrcode", "backups_v1", "automatic-room-key-forwarding"]
|
||||
features = ["qrcode", "automatic-room-key-forwarding"]
|
||||
|
||||
[dependencies.matrix-sdk-sqlite]
|
||||
path = "../../crates/matrix-sdk-sqlite"
|
||||
|
||||
@@ -1464,6 +1464,7 @@ impl OlmMachine {
|
||||
progress_listener.on_progress(progress as i32, total as i32)
|
||||
};
|
||||
|
||||
#[allow(deprecated)]
|
||||
let result =
|
||||
self.runtime.block_on(self.inner.import_room_keys(keys, from_backup, listener))?;
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ tracing-opentelemetry = { version = "0.20.0" }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream = "0.1.8"
|
||||
tokio-stream = { workspace = true }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
url = "2.2.2"
|
||||
zeroize = { workspace = true }
|
||||
|
||||
@@ -14,6 +14,13 @@ interface ClientError {
|
||||
Generic(string msg);
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface SteadyStateError {
|
||||
BackupDisabled();
|
||||
Connection();
|
||||
Laged();
|
||||
};
|
||||
|
||||
interface MediaSource {
|
||||
[Name=from_json, Throws=ClientError]
|
||||
constructor(string json);
|
||||
|
||||
@@ -47,6 +47,7 @@ use url::Url;
|
||||
use super::{room::Room, session_verification::SessionVerificationController, RUNTIME};
|
||||
use crate::{
|
||||
client,
|
||||
encryption::Encryption,
|
||||
notification::NotificationClientBuilder,
|
||||
notification_settings::NotificationSettings,
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
@@ -683,6 +684,10 @@ impl Client {
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encryption(&self) -> Arc<Encryption> {
|
||||
Arc::new(self.inner.encryption().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::EncryptionSettings,
|
||||
ruma::{
|
||||
api::{error::UnknownVersionError, MatrixVersion},
|
||||
ServerName, UserId,
|
||||
@@ -249,6 +250,13 @@ impl ClientBuilder {
|
||||
|
||||
impl Default for ClientBuilder {
|
||||
fn default() -> Self {
|
||||
let encryption_settings = EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
auto_download_from_backup: true,
|
||||
auto_enable_backups: true,
|
||||
};
|
||||
let inner = MatrixClient::builder().with_encryption_settings(encryption_settings);
|
||||
|
||||
Self {
|
||||
base_path: None,
|
||||
username: None,
|
||||
@@ -261,7 +269,7 @@ impl Default for ClientBuilder {
|
||||
proxy: None,
|
||||
disable_ssl_verification: false,
|
||||
disable_automatic_token_refresh: false,
|
||||
inner: MatrixClient::builder(),
|
||||
inner,
|
||||
cross_process_refresh_lock_id: None,
|
||||
session_delegate: None,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::encryption::{backups, recovery};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{error::ClientError, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Encryption {
|
||||
inner: matrix_sdk::encryption::Encryption,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::encryption::Encryption> for Encryption {
|
||||
fn from(value: matrix_sdk::encryption::Encryption) -> Self {
|
||||
Self { inner: value }
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait BackupStateListener: Sync + Send {
|
||||
fn on_update(&self, status: BackupState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait BackupSteadyStateListener: Sync + Send {
|
||||
fn on_update(&self, status: BackupUploadState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait RecoveryStateListener: Sync + Send {
|
||||
fn on_update(&self, status: RecoveryState);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum BackupUploadState {
|
||||
Waiting,
|
||||
CheckingIfUploadNeeded { backed_up_count: u32, total_count: u32 },
|
||||
Uploading { backed_up_count: u32, total_count: u32 },
|
||||
Error,
|
||||
Done,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SteadyStateError {
|
||||
#[error("The backup got disabled while waiting for the room keys to be uploaded.")]
|
||||
BackupDisabled,
|
||||
#[error("There was a connection error.")]
|
||||
Connection,
|
||||
#[error("We couldn't read status updates from the upload task quickly enough.")]
|
||||
Laged,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::encryption::backups::SteadyStateError> for SteadyStateError {
|
||||
fn from(value: matrix_sdk::encryption::backups::SteadyStateError) -> Self {
|
||||
match value {
|
||||
backups::SteadyStateError::BackupDisabled => Self::BackupDisabled,
|
||||
backups::SteadyStateError::Connection => Self::Connection,
|
||||
backups::SteadyStateError::Laged => Self::Laged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum BackupState {
|
||||
Unknown,
|
||||
Creating,
|
||||
Enabling,
|
||||
Resuming,
|
||||
Enabled,
|
||||
Downloading,
|
||||
Disabling,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl From<backups::BackupState> for BackupState {
|
||||
fn from(value: backups::BackupState) -> Self {
|
||||
match value {
|
||||
backups::BackupState::Unknown => BackupState::Unknown,
|
||||
backups::BackupState::Creating => BackupState::Creating,
|
||||
backups::BackupState::Enabling => BackupState::Enabling,
|
||||
backups::BackupState::Resuming => BackupState::Resuming,
|
||||
backups::BackupState::Enabled => BackupState::Enabled,
|
||||
backups::BackupState::Downloading => BackupState::Downloading,
|
||||
backups::BackupState::Disabling => BackupState::Disabling,
|
||||
backups::BackupState::Disabled => BackupState::Disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<backups::UploadState> for BackupUploadState {
|
||||
fn from(value: backups::UploadState) -> Self {
|
||||
match value {
|
||||
backups::UploadState::Idle => Self::Waiting,
|
||||
backups::UploadState::CheckingIfUploadNeeded(count) => Self::CheckingIfUploadNeeded {
|
||||
backed_up_count: count.backed_up.try_into().unwrap_or(u32::MAX),
|
||||
total_count: count.total.try_into().unwrap_or(u32::MAX),
|
||||
},
|
||||
backups::UploadState::Uploading(count) => Self::Uploading {
|
||||
backed_up_count: count.backed_up.try_into().unwrap_or(u32::MAX),
|
||||
total_count: count.total.try_into().unwrap_or(u32::MAX),
|
||||
},
|
||||
backups::UploadState::Error => Self::Error,
|
||||
backups::UploadState::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RecoveryState {
|
||||
Unknown,
|
||||
Enabled,
|
||||
Disabled,
|
||||
Incomplete,
|
||||
}
|
||||
|
||||
impl From<recovery::RecoveryState> for RecoveryState {
|
||||
fn from(value: recovery::RecoveryState) -> Self {
|
||||
match value {
|
||||
recovery::RecoveryState::Unknown => Self::Unknown,
|
||||
recovery::RecoveryState::Enabled => Self::Enabled,
|
||||
recovery::RecoveryState::Disabled => Self::Disabled,
|
||||
recovery::RecoveryState::Incomplete => Self::Incomplete,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait EnableRecoveryProgressListener: Sync + Send {
|
||||
fn on_update(&self, status: EnableRecoveryProgress);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum EnableRecoveryProgress {
|
||||
CreatingBackup,
|
||||
CreatingRecoveryKey,
|
||||
BackingUp { backed_up_count: u32, total_count: u32 },
|
||||
Done { recovery_key: String },
|
||||
}
|
||||
|
||||
impl From<recovery::EnableProgress> for EnableRecoveryProgress {
|
||||
fn from(value: recovery::EnableProgress) -> Self {
|
||||
match &value {
|
||||
recovery::EnableProgress::CreatingBackup => Self::CreatingBackup,
|
||||
recovery::EnableProgress::CreatingRecoveryKey => Self::CreatingRecoveryKey,
|
||||
recovery::EnableProgress::BackingUp(counts) => Self::BackingUp {
|
||||
backed_up_count: counts.backed_up.try_into().unwrap_or(u32::MAX),
|
||||
total_count: counts.backed_up.try_into().unwrap_or(u32::MAX),
|
||||
},
|
||||
recovery::EnableProgress::Done { recovery_key } => {
|
||||
Self::Done { recovery_key: recovery_key.to_owned() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl Encryption {
|
||||
pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
|
||||
let mut stream = self.inner.backups().state_stream();
|
||||
|
||||
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while let Some(state) = stream.next().await {
|
||||
// TODO: Do we want to abort if theres's an error here?
|
||||
let Ok(state) = state else { continue };
|
||||
listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
stream_task.into()
|
||||
}
|
||||
|
||||
pub fn backup_state(&self) -> BackupState {
|
||||
self.inner.backups().state().into()
|
||||
}
|
||||
|
||||
pub fn recovery_state(&self) -> RecoveryState {
|
||||
self.inner.recovery().state().into()
|
||||
}
|
||||
|
||||
pub fn recovery_state_listener(
|
||||
&self,
|
||||
listener: Box<dyn RecoveryStateListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut stream = self.inner.recovery().state_stream();
|
||||
|
||||
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while let Some(state) = stream.next().await {
|
||||
listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
stream_task.into()
|
||||
}
|
||||
|
||||
pub async fn enable_backups(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.recovery().enable_backup().await?)
|
||||
}
|
||||
|
||||
pub async fn is_last_device(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.recovery().are_we_the_last_man_standing().await?)
|
||||
}
|
||||
|
||||
pub async fn wait_for_backup_upload_steady_state(
|
||||
&self,
|
||||
progress_listener: Option<Box<dyn BackupSteadyStateListener>>,
|
||||
) -> Result<(), SteadyStateError> {
|
||||
let backups = self.inner.backups();
|
||||
let wait_for_steady_state = backups.wait_for_steady_state();
|
||||
|
||||
let task = if let Some(listener) = progress_listener {
|
||||
let mut progress_stream = wait_for_steady_state.subscribe_to_progress();
|
||||
|
||||
Some(RUNTIME.spawn(async move {
|
||||
while let Some(progress) = progress_stream.next().await {
|
||||
// TODO: Do we want to abort if theres's an error here?
|
||||
let Ok(progress) = progress else { continue };
|
||||
listener.on_update(progress.into());
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let result = wait_for_steady_state.await;
|
||||
|
||||
if let Some(task) = task {
|
||||
task.abort();
|
||||
}
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
pub async fn enable_recovery(
|
||||
&self,
|
||||
wait_for_backups_to_upload: bool,
|
||||
progress_listener: Box<dyn EnableRecoveryProgressListener>,
|
||||
) -> Result<String, ClientError> {
|
||||
let recovery = self.inner.recovery();
|
||||
|
||||
let enable = if wait_for_backups_to_upload {
|
||||
recovery.enable().wait_for_backups_to_upload()
|
||||
} else {
|
||||
recovery.enable()
|
||||
};
|
||||
|
||||
let mut progress_stream = enable.subscribe_to_progress();
|
||||
|
||||
let task = RUNTIME.spawn(async move {
|
||||
while let Some(progress) = progress_stream.next().await {
|
||||
// TODO: Do we want to abort if theres's an error here?
|
||||
let Ok(progress) = progress else { continue };
|
||||
progress_listener.on_update(progress.into());
|
||||
}
|
||||
});
|
||||
|
||||
let ret = enable.await?;
|
||||
|
||||
// TODO: Do we need to abort the task manually?
|
||||
task.abort();
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub async fn disable_recovery(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.recovery().disable().await?)
|
||||
}
|
||||
|
||||
pub async fn reset_recovery_key(&self) -> Result<String, ClientError> {
|
||||
// TODO: This works even if we don't have all secrets on this device. Add
|
||||
// another method which resets the key but requires the old key?
|
||||
//
|
||||
// What does the user even do if they don't remember the old key 🫠
|
||||
Ok(self.inner.recovery().reset_key().await?)
|
||||
}
|
||||
|
||||
pub async fn recover_and_reset(
|
||||
&self,
|
||||
mut old_recovery_key: String,
|
||||
) -> Result<String, ClientError> {
|
||||
// TODO: Is this even useful? I'm not sure how it is.
|
||||
let result = self.inner.recovery().recover_and_reset(&old_recovery_key).await;
|
||||
|
||||
old_recovery_key.zeroize();
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
pub async fn fix_recovery_issues(&self, mut recovery_key: String) -> Result<(), ClientError> {
|
||||
let result = self.inner.recovery().recover(&recovery_key).await;
|
||||
|
||||
recovery_key.zeroize();
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,12 @@ impl From<matrix_sdk::Error> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::encryption::secret_storage::SecretStorageError> for ClientError {
|
||||
fn from(e: matrix_sdk::encryption::secret_storage::SecretStorageError) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StoreError> for ClientError {
|
||||
fn from(e: StoreError) -> Self {
|
||||
Self::new(e)
|
||||
|
||||
@@ -24,6 +24,7 @@ mod authentication_service;
|
||||
mod chunk_iterator;
|
||||
mod client;
|
||||
mod client_builder;
|
||||
mod encryption;
|
||||
mod error;
|
||||
mod event;
|
||||
mod helpers;
|
||||
@@ -48,6 +49,7 @@ use matrix_sdk::ruma::events::room::{
|
||||
use matrix_sdk_ui::timeline::{BackPaginationStatus, EventItemOrigin};
|
||||
|
||||
use self::{
|
||||
encryption::SteadyStateError,
|
||||
error::ClientError,
|
||||
task_handle::TaskHandle,
|
||||
timeline::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# unreleased
|
||||
|
||||
- Add two new methods to import room keys,
|
||||
`OlmMachine::store()::import_exported_room_keys()` for file exports and
|
||||
`OlmMachine::backup_machine()::import_backed_up_room_keys()` for backups. The
|
||||
`OlmMachine::import_room_keys()` method is now deprecated.
|
||||
|
||||
- Add support for secret storage.
|
||||
|
||||
- Add initial support for MSC3814 - dehydrated devices.
|
||||
|
||||
@@ -19,7 +19,6 @@ default = []
|
||||
automatic-room-key-forwarding = []
|
||||
js = ["ruma/js", "vodozemac/js"]
|
||||
qrcode = ["dep:matrix-sdk-qrcode"]
|
||||
backups_v1 = ["dep:cbc"]
|
||||
message-ids = ["dep:ulid"]
|
||||
experimental-algorithms = []
|
||||
|
||||
@@ -33,7 +32,7 @@ async-std = { version = "1.12.0", features = ["unstable"] }
|
||||
async-trait = { workspace = true }
|
||||
bs58 = { version = "0.5.0" }
|
||||
byteorder = { workspace = true }
|
||||
cbc = { version = "0.1.2", features = ["std"], optional = true }
|
||||
cbc = { version = "0.1.2", features = ["std"] }
|
||||
cfg-if = "1.0"
|
||||
ctr = "0.9.1"
|
||||
eyeball = { workspace = true }
|
||||
@@ -53,7 +52,7 @@ serde = { workspace = true, features = ["derive", "rc"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
subtle = "2.5.0"
|
||||
tokio-stream = { version = "0.1.12", features = ["sync"] }
|
||||
tokio-stream = { workspace = true, features = ["sync"] }
|
||||
tokio = { workspace = true, default-features = false, features = ["sync"] }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true, features = ["attributes"] }
|
||||
|
||||
@@ -12,26 +12,25 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ruma::{
|
||||
api::client::backup::{EncryptedSessionDataInit, KeyBackupData, KeyBackupDataInit},
|
||||
serde::Base64,
|
||||
OwnedDeviceKeyId, OwnedUserId,
|
||||
};
|
||||
use vodozemac::Curve25519PublicKey;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::{compat::PkEncryption, decryption::DecodeError};
|
||||
use crate::olm::InboundGroupSession;
|
||||
use crate::{
|
||||
olm::InboundGroupSession,
|
||||
types::{MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InnerBackupKey {
|
||||
key: Curve25519PublicKey,
|
||||
signatures: BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceKeyId, String>>,
|
||||
signatures: Signatures,
|
||||
version: Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
@@ -69,7 +68,7 @@ impl MegolmV1BackupKey {
|
||||
}
|
||||
|
||||
/// Get all the signatures of this `MegolmV1BackupKey`.
|
||||
pub fn signatures(&self) -> BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceKeyId, String>> {
|
||||
pub fn signatures(&self) -> Signatures {
|
||||
self.inner.signatures.to_owned()
|
||||
}
|
||||
|
||||
@@ -138,4 +137,10 @@ impl MegolmV1BackupKey {
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn as_backup_algorithm(&self) -> RoomKeyBackupInfo {
|
||||
let auth_data = MegolmV1AuthData::new(self.inner.key, Default::default());
|
||||
|
||||
RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(auth_data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,20 @@ use std::{
|
||||
};
|
||||
|
||||
use bs58;
|
||||
use ruma::api::client::backup::EncryptedSessionData;
|
||||
use subtle::ConstantTimeEq;
|
||||
use thiserror::Error;
|
||||
use vodozemac::Curve25519PublicKey;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::{
|
||||
compat::{Error as DecryptionError, Message, PkDecryption},
|
||||
MegolmV1BackupKey,
|
||||
};
|
||||
use crate::store::BackupDecryptionKey;
|
||||
use crate::{
|
||||
store::BackupDecryptionKey,
|
||||
types::{MegolmV1AuthData, RoomKeyBackupInfo},
|
||||
};
|
||||
|
||||
/// Error type for the decoding of a [`BackupDecryptionKey`].
|
||||
#[derive(Debug, Error)]
|
||||
@@ -89,6 +95,14 @@ impl std::fmt::Display for BackupDecryptionKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for BackupDecryptionKey {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let choice = self.inner.ct_eq(other.inner.as_ref());
|
||||
|
||||
choice.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl BackupDecryptionKey {
|
||||
const PREFIX: [u8; 2] = [0x8b, 0x01];
|
||||
const PREFIX_PARITY: u8 = Self::PREFIX[0] ^ Self::PREFIX[1];
|
||||
@@ -207,6 +221,49 @@ impl BackupDecryptionKey {
|
||||
|
||||
Ok(String::from_utf8_lossy(&decrypted).to_string())
|
||||
}
|
||||
|
||||
/// Try to decrypt the given session data using this
|
||||
/// [`BackupDecryptionKey`].
|
||||
pub fn decrypt_session_data(
|
||||
&self,
|
||||
session_data: EncryptedSessionData,
|
||||
) -> Result<Vec<u8>, DecryptionError> {
|
||||
let message = Message {
|
||||
ciphertext: session_data.ciphertext.into_inner(),
|
||||
mac: session_data.mac.into_inner(),
|
||||
// TODO: Remove the unwrap.
|
||||
ephemeral_key: Curve25519PublicKey::from_slice(session_data.ephemeral.as_bytes())
|
||||
.unwrap(),
|
||||
};
|
||||
|
||||
let pk = self.get_pk_decryption();
|
||||
|
||||
pk.decrypt(&message)
|
||||
}
|
||||
|
||||
/// Check if the given public key from the [`RoomKeyBackupInfo`] matches to
|
||||
/// this [`BackupDecryptionKey`].
|
||||
pub fn backup_key_matches(&self, info: &RoomKeyBackupInfo) -> bool {
|
||||
match info {
|
||||
RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(info) => {
|
||||
let pk = self.get_pk_decryption();
|
||||
let public_key = pk.public_key();
|
||||
|
||||
info.public_key == public_key
|
||||
}
|
||||
RoomKeyBackupInfo::Other { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`RoomKeyBackupInfo`] from this [`BackupDecryptionKey`].
|
||||
///
|
||||
/// This can be used to upload and enable the backup on the homeserver.
|
||||
pub fn as_room_key_backup_info(&self) -> RoomKeyBackupInfo {
|
||||
let public_key = self.get_pk_decryption().public_key();
|
||||
let auth_data = MegolmV1AuthData::new(public_key, Default::default());
|
||||
|
||||
RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(auth_data)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -36,10 +36,10 @@ use tokio::sync::RwLock;
|
||||
use tracing::{debug, info, instrument, trace, warn};
|
||||
|
||||
use crate::{
|
||||
olm::{InboundGroupSession, SignedJsonObject},
|
||||
olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession, SignedJsonObject},
|
||||
store::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts, Store},
|
||||
types::{MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
|
||||
CryptoStoreError, Device, KeysBackupRequest, OutgoingRequest,
|
||||
CryptoStoreError, Device, KeysBackupRequest, OutgoingRequest, RoomKeyImportResult,
|
||||
};
|
||||
|
||||
mod keys;
|
||||
@@ -349,6 +349,36 @@ impl BackupMachine {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sign_backup(
|
||||
&self,
|
||||
backup_info: &mut RoomKeyBackupInfo,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
if let RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(data) = backup_info {
|
||||
let canonical_json = data.to_canonical_json().unwrap();
|
||||
|
||||
let private_identity = self.store.private_identity();
|
||||
let identity = private_identity.lock().await;
|
||||
|
||||
if let Some(key_id) = identity.master_key_id().await {
|
||||
if let Ok(signature) = identity.sign(&canonical_json).await {
|
||||
data.signatures.add_signature(
|
||||
self.store.user_id().to_owned(),
|
||||
key_id,
|
||||
signature,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let cache = self.store.cache().await?;
|
||||
let account = cache.account().await?;
|
||||
let key_id = account.signing_key_id();
|
||||
let signature = account.sign(&canonical_json);
|
||||
data.signatures.add_signature(self.store.user_id().to_owned(), key_id, signature);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Activate the given backup key to be used to encrypt and backup room
|
||||
/// keys.
|
||||
///
|
||||
@@ -555,6 +585,54 @@ impl BackupMachine {
|
||||
|
||||
(backup, session_record)
|
||||
}
|
||||
|
||||
/// Import the given room keys into our store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_keys` - A list of previously exported keys that should be
|
||||
/// imported into our store. If we already have a better version of a key
|
||||
/// the key will *not* be imported.
|
||||
///
|
||||
/// Returns a [`RoomKeyImportResult`] containing information about room keys
|
||||
/// which were imported.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::io::Cursor;
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, decrypt_room_key_export};
|
||||
/// # use ruma::{device_id, user_id};
|
||||
/// # let alice = user_id!("@alice:example.org");
|
||||
/// # async {
|
||||
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
|
||||
/// # let export = Cursor::new("".to_owned());
|
||||
/// let exported_keys = decrypt_room_key_export(export, "1234")?;
|
||||
/// machine.backup_machine().import_backed_up_room_keys(exported_keys, |_, _| {}).await?;
|
||||
/// # anyhow::Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn import_backed_up_room_keys(
|
||||
&self,
|
||||
room_keys: BTreeMap<OwnedRoomId, BTreeMap<String, BackedUpRoomKey>>,
|
||||
progress_listener: impl Fn(usize, usize),
|
||||
) -> Result<RoomKeyImportResult, CryptoStoreError> {
|
||||
let mut decrypted_room_keys = vec![];
|
||||
|
||||
for (room_id, room_keys) in room_keys {
|
||||
for (session_id, room_key) in room_keys {
|
||||
let room_key = ExportedRoomKey::from_backed_up_room_key(
|
||||
room_id.to_owned(),
|
||||
session_id,
|
||||
room_key,
|
||||
);
|
||||
|
||||
decrypted_room_keys.push(room_key);
|
||||
}
|
||||
}
|
||||
|
||||
self.store.import_room_keys(decrypted_room_keys, true, progress_listener).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -305,7 +305,7 @@ mod tests {
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
machine.import_room_keys(decrypted, false, |_, _| {}).await.unwrap(),
|
||||
machine.store().import_exported_room_keys(decrypted, |_, _| {}).await.unwrap(),
|
||||
RoomKeyImportResult::new(0, 1, BTreeMap::new())
|
||||
);
|
||||
}
|
||||
@@ -332,17 +332,20 @@ mod tests {
|
||||
)]),
|
||||
);
|
||||
|
||||
assert_eq!(machine.import_room_keys(export, false, |_, _| {}).await?, keys);
|
||||
assert_eq!(machine.store().import_exported_room_keys(export, |_, _| {}).await?, keys);
|
||||
|
||||
let export = vec![session.export_at_index(10).await];
|
||||
assert_eq!(
|
||||
machine.import_room_keys(export, false, |_, _| {}).await?,
|
||||
machine.store().import_exported_room_keys(export, |_, _| {}).await?,
|
||||
RoomKeyImportResult::new(0, 1, BTreeMap::new())
|
||||
);
|
||||
|
||||
let better_export = vec![session.export().await];
|
||||
|
||||
assert_eq!(machine.import_room_keys(better_export, false, |_, _| {}).await?, keys);
|
||||
assert_eq!(
|
||||
machine.store().import_exported_room_keys(better_export, |_, _| {}).await?,
|
||||
keys
|
||||
);
|
||||
|
||||
let another_session = machine.create_inbound_session(room_id).await?;
|
||||
let export = vec![another_session.export_at_index(10).await];
|
||||
@@ -359,7 +362,7 @@ mod tests {
|
||||
)]),
|
||||
);
|
||||
|
||||
assert_eq!(machine.import_room_keys(export, false, |_, _| {}).await?, keys);
|
||||
assert_eq!(machine.store().import_exported_room_keys(export, |_, _| {}).await?, keys);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1101,12 +1101,12 @@ mod tests {
|
||||
identities::{IdentityManager, LocalTrust, ReadOnlyDevice},
|
||||
olm::{Account, PrivateCrossSigningIdentity},
|
||||
session_manager::GroupSessionCache,
|
||||
store::{CryptoStoreWrapper, MemoryStore, PendingChanges, Store},
|
||||
types::events::room::encrypted::{EncryptedEvent, RoomEncryptedEventContent},
|
||||
store::{Changes, CryptoStoreWrapper, MemoryStore, PendingChanges, Store},
|
||||
types::events::room::encrypted::{
|
||||
EncryptedEvent, EncryptedToDeviceEvent, RoomEncryptedEventContent,
|
||||
},
|
||||
verification::VerificationMachine,
|
||||
};
|
||||
#[cfg(any(feature = "automatic-room-key-forwarding", feature = "backups_v1"))]
|
||||
use crate::{store::Changes, types::events::room::encrypted::EncryptedToDeviceEvent};
|
||||
|
||||
fn alice_id() -> &'static UserId {
|
||||
user_id!("@alice:example.org")
|
||||
@@ -1276,7 +1276,6 @@ mod tests {
|
||||
(alice_machine, group_session, bob_machine)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "automatic-room-key-forwarding", feature = "backups_v1"))]
|
||||
fn extract_content<'a>(
|
||||
recipient: &UserId,
|
||||
request: &'a crate::OutgoingRequest,
|
||||
@@ -1309,7 +1308,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "automatic-room-key-forwarding", feature = "backups_v1"))]
|
||||
fn request_to_event<C>(
|
||||
recipient: &UserId,
|
||||
sender: &UserId,
|
||||
@@ -1869,7 +1867,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(feature = "backups_v1")]
|
||||
async fn test_secret_broadcasting() {
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
use ruma::api::client::to_device::send_event_to_device::v3::Response as ToDeviceResponse;
|
||||
|
||||
@@ -823,7 +823,6 @@ impl ReadOnlyDevice {
|
||||
/// **Note**: Use this method with caution, the `canonical_json` needs to be
|
||||
/// correctly canonicalized and make sure that the object you are checking
|
||||
/// the signature for is allowed to be signed by a device.
|
||||
#[cfg(feature = "backups_v1")]
|
||||
pub(crate) fn has_signed_raw(
|
||||
&self,
|
||||
signatures: &Signatures,
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
#[cfg(feature = "backups_v1")]
|
||||
pub mod backups;
|
||||
mod ciphers;
|
||||
pub mod dehydrated_devices;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashSet},
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -54,9 +54,8 @@ use vodozemac::{
|
||||
Curve25519PublicKey, Ed25519Signature,
|
||||
};
|
||||
|
||||
#[cfg(feature = "backups_v1")]
|
||||
use crate::backups::BackupMachine;
|
||||
use crate::{
|
||||
backups::BackupMachine,
|
||||
dehydrated_devices::{DehydratedDevices, DehydrationError},
|
||||
error::{EventError, MegolmError, MegolmResult, OlmError, OlmResult},
|
||||
gossiping::GossipMachine,
|
||||
@@ -128,7 +127,6 @@ pub struct OlmMachineInner {
|
||||
/// of when a key query needs to be done and handling one.
|
||||
identity_manager: IdentityManager,
|
||||
/// A state machine that handles creating room key backups.
|
||||
#[cfg(feature = "backups_v1")]
|
||||
backup_machine: BackupMachine,
|
||||
}
|
||||
|
||||
@@ -202,7 +200,6 @@ impl OlmMachine {
|
||||
let session_manager =
|
||||
SessionManager::new(users_for_key_claim, key_request_machine.clone(), store.clone());
|
||||
|
||||
#[cfg(feature = "backups_v1")]
|
||||
let backup_machine = BackupMachine::new(store.clone(), None);
|
||||
|
||||
let inner = Arc::new(OlmMachineInner {
|
||||
@@ -215,7 +212,6 @@ impl OlmMachine {
|
||||
verification_machine,
|
||||
key_request_machine,
|
||||
identity_manager,
|
||||
#[cfg(feature = "backups_v1")]
|
||||
backup_machine,
|
||||
});
|
||||
|
||||
@@ -463,7 +459,6 @@ impl OlmMachine {
|
||||
self.inner.verification_machine.mark_request_as_sent(request_id);
|
||||
}
|
||||
IncomingResponse::KeysBackup(_) => {
|
||||
#[cfg(feature = "backups_v1")]
|
||||
self.inner.backup_machine.mark_request_as_sent(request_id).await?;
|
||||
}
|
||||
};
|
||||
@@ -1350,7 +1345,6 @@ impl OlmMachine {
|
||||
#[allow(unused_mut)]
|
||||
let mut secrets = identity.get_missing_secrets().await;
|
||||
|
||||
#[cfg(feature = "backups_v1")]
|
||||
if self.store().load_backup_keys().await?.decryption_key.is_none() {
|
||||
secrets.push(SecretName::RecoveryKey);
|
||||
}
|
||||
@@ -1749,77 +1743,17 @@ impl OlmMachine {
|
||||
/// machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
|
||||
/// # };
|
||||
/// ```
|
||||
#[deprecated(
|
||||
since = "0.7.0",
|
||||
note = "Use the OlmMachine::store::import_exported_room_keys method instead"
|
||||
)]
|
||||
pub async fn import_room_keys(
|
||||
&self,
|
||||
exported_keys: Vec<ExportedRoomKey>,
|
||||
#[allow(unused_variables)] from_backup: bool,
|
||||
from_backup: bool,
|
||||
progress_listener: impl Fn(usize, usize),
|
||||
) -> StoreResult<RoomKeyImportResult> {
|
||||
let mut sessions = Vec::new();
|
||||
|
||||
async fn new_session_better(
|
||||
session: &InboundGroupSession,
|
||||
old_session: Option<InboundGroupSession>,
|
||||
) -> bool {
|
||||
if let Some(old_session) = &old_session {
|
||||
session.compare(old_session).await == SessionOrdering::Better
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
let total_count = exported_keys.len();
|
||||
let mut keys = BTreeMap::new();
|
||||
|
||||
for (i, key) in exported_keys.into_iter().enumerate() {
|
||||
match InboundGroupSession::from_export(&key) {
|
||||
Ok(session) => {
|
||||
let old_session = self
|
||||
.inner
|
||||
.store
|
||||
.get_inbound_group_session(session.room_id(), session.session_id())
|
||||
.await?;
|
||||
|
||||
// Only import the session if we didn't have this session or
|
||||
// if it's a better version of the same session.
|
||||
if new_session_better(&session, old_session).await {
|
||||
#[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_base64())
|
||||
.or_insert_with(BTreeSet::new)
|
||||
.insert(session.session_id().to_owned());
|
||||
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
sender_key= key.sender_key.to_base64(),
|
||||
room_id = ?key.room_id,
|
||||
session_id = key.session_id,
|
||||
error = ?e,
|
||||
"Couldn't import a room key from a file export."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
progress_listener(i, total_count);
|
||||
}
|
||||
|
||||
let imported_count = sessions.len();
|
||||
|
||||
let changes = Changes { inbound_group_sessions: sessions, ..Default::default() };
|
||||
|
||||
self.store().save_changes(changes).await?;
|
||||
|
||||
info!(total_count, imported_count, room_keys = ?keys, "Successfully imported room keys");
|
||||
|
||||
Ok(RoomKeyImportResult::new(imported_count, total_count, keys))
|
||||
self.store().import_room_keys(exported_keys, from_backup, progress_listener).await
|
||||
}
|
||||
|
||||
/// Export the keys that match the given predicate.
|
||||
@@ -1950,7 +1884,6 @@ impl OlmMachine {
|
||||
///
|
||||
/// This state machine can be used to incrementally backup all room keys to
|
||||
/// the server.
|
||||
#[cfg(feature = "backups_v1")]
|
||||
pub fn backup_machine(&self) -> &BackupMachine {
|
||||
&self.inner.backup_machine
|
||||
}
|
||||
@@ -2872,11 +2805,7 @@ pub(crate) mod tests {
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
if cfg!(feature = "backups_v1") {
|
||||
assert_eq!(outgoing_to_device.len(), 4);
|
||||
} else {
|
||||
assert_eq!(outgoing_to_device.len(), 3);
|
||||
}
|
||||
assert_eq!(outgoing_to_device.len(), 4);
|
||||
|
||||
// The second time, as there are already in-flight requests, it should have no
|
||||
// effect.
|
||||
@@ -2893,25 +2822,21 @@ pub(crate) mod tests {
|
||||
|
||||
let should_query_secrets = alice.query_missing_secrets_from_other_sessions().await.unwrap();
|
||||
|
||||
if cfg!(feature = "backups_v1") {
|
||||
assert!(should_query_secrets);
|
||||
assert!(should_query_secrets);
|
||||
|
||||
let outgoing_to_device = alice
|
||||
.outgoing_requests()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|outgoing| match outgoing.request.as_ref() {
|
||||
OutgoingRequests::ToDeviceRequest(request) => {
|
||||
request.event_type.to_string() == "m.secret.request"
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.collect_vec();
|
||||
assert_eq!(outgoing_to_device.len(), 1);
|
||||
} else {
|
||||
assert!(!should_query_secrets);
|
||||
}
|
||||
let outgoing_to_device = alice
|
||||
.outgoing_requests()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|outgoing| match outgoing.request.as_ref() {
|
||||
OutgoingRequests::ToDeviceRequest(request) => {
|
||||
request.event_type.to_string() == "m.secret.request"
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.collect_vec();
|
||||
assert_eq!(outgoing_to_device.len(), 1);
|
||||
|
||||
// The second time, as there are already in-flight requests, it should have no
|
||||
// effect.
|
||||
|
||||
@@ -245,7 +245,6 @@ impl StaticAccountData {
|
||||
/// **Note**: Use this method with caution, the `canonical_json` needs to be
|
||||
/// correctly canonicalized and make sure that the object you are checking
|
||||
/// the signature for is allowed to be signed by our own device.
|
||||
#[cfg(any(test, feature = "backups_v1"))]
|
||||
pub fn has_signed_raw(
|
||||
&self,
|
||||
signatures: &crate::types::Signatures,
|
||||
|
||||
@@ -398,7 +398,6 @@ impl InboundGroupSession {
|
||||
|
||||
/// Export the inbound group session into a format that can be uploaded to
|
||||
/// the server as a backup.
|
||||
#[cfg(feature = "backups_v1")]
|
||||
pub async fn to_backup(&self) -> BackedUpRoomKey {
|
||||
self.export().await.into()
|
||||
}
|
||||
|
||||
@@ -97,6 +97,24 @@ pub struct ExportedRoomKey {
|
||||
pub forwarding_curve25519_key_chain: Vec<Curve25519PublicKey>,
|
||||
}
|
||||
|
||||
impl ExportedRoomKey {
|
||||
pub(crate) fn from_backed_up_room_key(
|
||||
room_id: OwnedRoomId,
|
||||
session_id: String,
|
||||
room_key: BackedUpRoomKey,
|
||||
) -> Self {
|
||||
Self {
|
||||
algorithm: room_key.algorithm,
|
||||
room_id,
|
||||
sender_key: room_key.sender_key,
|
||||
session_id,
|
||||
session_key: room_key.session_key,
|
||||
sender_claimed_keys: room_key.sender_claimed_keys,
|
||||
forwarding_curve25519_key_chain: room_key.forwarding_curve25519_key_chain,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A backed up version of an `InboundGroupSession`
|
||||
///
|
||||
/// This can be used to backup the `InboundGroupSession` to the server.
|
||||
|
||||
@@ -192,7 +192,6 @@ impl SignedJsonObject for CrossSigningKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "backups_v1")]
|
||||
impl SignedJsonObject for crate::types::MegolmV1AuthData {
|
||||
fn signatures(&self) -> &Signatures {
|
||||
&self.signatures
|
||||
|
||||
@@ -66,12 +66,12 @@ use crate::{
|
||||
user::UserIdentities, Device, ReadOnlyDevice, ReadOnlyUserIdentities, UserDevices,
|
||||
},
|
||||
olm::{
|
||||
Account, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
|
||||
Account, ExportedRoomKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
|
||||
PrivateCrossSigningIdentity, Session, StaticAccountData,
|
||||
},
|
||||
types::{events::room_key_withheld::RoomKeyWithheldEvent, EventEncryptionAlgorithm},
|
||||
verification::VerificationMachine,
|
||||
CrossSigningStatus, ReadOnlyOwnUserIdentity,
|
||||
CrossSigningStatus, ReadOnlyOwnUserIdentity, RoomKeyImportResult,
|
||||
};
|
||||
|
||||
pub mod caches;
|
||||
@@ -1142,16 +1142,12 @@ impl Store {
|
||||
self.inner.identity.lock().await.export_secret(secret_name).await
|
||||
}
|
||||
SecretName::RecoveryKey => {
|
||||
#[cfg(feature = "backups_v1")]
|
||||
if let Some(key) = self.load_backup_keys().await?.decryption_key {
|
||||
let exported = key.to_base64();
|
||||
Some(exported)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "backups_v1"))]
|
||||
None
|
||||
}
|
||||
name => {
|
||||
warn!(secret = ?name, "Unknown secret was requested");
|
||||
@@ -1470,6 +1466,112 @@ impl Store {
|
||||
pub fn secrets_stream(&self) -> impl Stream<Item = GossippedSecret> {
|
||||
self.inner.store.secrets_stream()
|
||||
}
|
||||
|
||||
pub(crate) async fn import_room_keys(
|
||||
&self,
|
||||
exported_keys: Vec<ExportedRoomKey>,
|
||||
from_backup: bool,
|
||||
progress_listener: impl Fn(usize, usize),
|
||||
) -> Result<RoomKeyImportResult> {
|
||||
let mut sessions = Vec::new();
|
||||
|
||||
async fn new_session_better(
|
||||
session: &InboundGroupSession,
|
||||
old_session: Option<InboundGroupSession>,
|
||||
) -> bool {
|
||||
if let Some(old_session) = &old_session {
|
||||
session.compare(old_session).await == SessionOrdering::Better
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
let total_count = exported_keys.len();
|
||||
let mut keys = BTreeMap::new();
|
||||
|
||||
for (i, key) in exported_keys.into_iter().enumerate() {
|
||||
match InboundGroupSession::from_export(&key) {
|
||||
Ok(session) => {
|
||||
let old_session = self
|
||||
.inner
|
||||
.store
|
||||
.get_inbound_group_session(session.room_id(), session.session_id())
|
||||
.await?;
|
||||
|
||||
// Only import the session if we didn't have this session or
|
||||
// if it's a better version of the same session.
|
||||
if new_session_better(&session, old_session).await {
|
||||
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_base64())
|
||||
.or_insert_with(BTreeSet::new)
|
||||
.insert(session.session_id().to_owned());
|
||||
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
sender_key= key.sender_key.to_base64(),
|
||||
room_id = ?key.room_id,
|
||||
session_id = key.session_id,
|
||||
error = ?e,
|
||||
"Couldn't import a room key from a file export."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
progress_listener(i, total_count);
|
||||
}
|
||||
|
||||
let imported_count = sessions.len();
|
||||
|
||||
let changes = Changes { inbound_group_sessions: sessions, ..Default::default() };
|
||||
|
||||
self.save_changes(changes).await?;
|
||||
|
||||
info!(total_count, imported_count, room_keys = ?keys, "Successfully imported room keys");
|
||||
|
||||
Ok(RoomKeyImportResult::new(imported_count, total_count, keys))
|
||||
}
|
||||
|
||||
/// Import the given room keys into our store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `exported_keys` - A list of previously exported keys that should be
|
||||
/// imported into our store. If we already have a better version of a key
|
||||
/// the key will *not* be imported.
|
||||
///
|
||||
/// Returns a tuple of numbers that represent the number of sessions that
|
||||
/// were imported and the total number of sessions that were found in the
|
||||
/// key export.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::io::Cursor;
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, decrypt_room_key_export};
|
||||
/// # use ruma::{device_id, user_id};
|
||||
/// # let alice = user_id!("@alice:example.org");
|
||||
/// # async {
|
||||
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
|
||||
/// # let export = Cursor::new("".to_owned());
|
||||
/// let exported_keys = decrypt_room_key_export(export, "1234").unwrap();
|
||||
/// machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
|
||||
/// # };
|
||||
/// ```
|
||||
pub async fn import_exported_room_keys(
|
||||
&self,
|
||||
exported_keys: Vec<ExportedRoomKey>,
|
||||
progress_listener: impl Fn(usize, usize),
|
||||
) -> Result<RoomKeyImportResult> {
|
||||
self.import_room_keys(exported_keys, false, progress_listener).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Store {
|
||||
|
||||
@@ -36,6 +36,12 @@ pub struct MegolmV1AuthData {
|
||||
extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
impl MegolmV1AuthData {
|
||||
pub fn new(public_key: Curve25519PublicKey, signatures: Signatures) -> Self {
|
||||
Self { public_key, signatures, extra: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Information pertaining to a room key backup. Can be used to upload a new
|
||||
/// backup version as defined in the [spec].
|
||||
///
|
||||
|
||||
@@ -80,7 +80,6 @@ impl MasterPubkey {
|
||||
/// **Note**: Use this method with caution, the `canonical_json` needs to be
|
||||
/// correctly canonicalized and make sure that the object you are checking
|
||||
/// the signature for is allowed to be signed by a master key.
|
||||
#[cfg(any(feature = "backups_v1", test))]
|
||||
pub(crate) fn has_signed_raw(
|
||||
&self,
|
||||
signatures: &Signatures,
|
||||
|
||||
@@ -612,7 +612,6 @@ impl IdentitiesBeingVerified {
|
||||
#[allow(unused_mut)]
|
||||
let mut secrets = self.private_identity.get_missing_secrets().await;
|
||||
|
||||
#[cfg(feature = "backups_v1")]
|
||||
if self.store.inner.load_backup_keys().await?.decryption_key.is_none() {
|
||||
secrets.push(ruma::events::secret::request::SecretName::RecoveryKey);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
|
||||
use eyeball::SharedObservable;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use imbl::Vector;
|
||||
use matrix_sdk::{
|
||||
deserialized_responses::SyncTimelineEvent, executor::spawn, sync::RoomUpdate, Room,
|
||||
@@ -205,6 +206,35 @@ impl TimelineBuilder {
|
||||
forwarded_room_key_handle,
|
||||
];
|
||||
|
||||
let room_key_from_backups_join_handle = {
|
||||
let inner = inner.clone();
|
||||
let room_id = inner.room().room_id();
|
||||
|
||||
let stream = client.encryption().backups().room_keys_for_room_stream(room_id);
|
||||
|
||||
spawn(async move {
|
||||
pin_mut!(stream);
|
||||
|
||||
while let Some(update) = stream.next().await {
|
||||
let room = inner.room();
|
||||
|
||||
match update {
|
||||
Ok(info) => {
|
||||
let mut session_ids = BTreeSet::new();
|
||||
|
||||
for set in info.into_values() {
|
||||
session_ids.extend(set);
|
||||
}
|
||||
|
||||
inner.retry_event_decryption(room, Some(session_ids)).await;
|
||||
}
|
||||
// We lagged, so retry every event.
|
||||
Err(_) => inner.retry_event_decryption(room, None).await,
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let (msg_sender, msg_receiver) = mpsc::channel(1);
|
||||
info!("Starting message-sending loop");
|
||||
spawn(send_queued_messages(inner.clone(), room.clone(), msg_receiver));
|
||||
@@ -220,6 +250,7 @@ impl TimelineBuilder {
|
||||
event_handler_handles: handles,
|
||||
room_update_join_handle,
|
||||
ignore_user_list_update_join_handle,
|
||||
room_key_from_backups_join_handle,
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -748,6 +748,7 @@ struct TimelineDropHandle {
|
||||
event_handler_handles: Vec<EventHandlerHandle>,
|
||||
room_update_join_handle: JoinHandle<()>,
|
||||
ignore_user_list_update_join_handle: JoinHandle<()>,
|
||||
room_key_from_backups_join_handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Drop for TimelineDropHandle {
|
||||
@@ -757,6 +758,7 @@ impl Drop for TimelineDropHandle {
|
||||
}
|
||||
self.room_update_join_handle.abort();
|
||||
self.ignore_user_list_update_join_handle.abort();
|
||||
self.room_key_from_backups_join_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ async fn retry_message_decryption() {
|
||||
let exported_keys = decrypt_room_key_export(Cursor::new(SESSION_KEY), "1234").unwrap();
|
||||
|
||||
let olm_machine = OlmMachine::new(own_user_id, "SomeDeviceId".into()).await;
|
||||
olm_machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
|
||||
olm_machine.store().import_exported_room_keys(exported_keys, |_, _| {}).await.unwrap();
|
||||
|
||||
timeline
|
||||
.inner
|
||||
@@ -201,7 +201,7 @@ async fn retry_edit_decryption() {
|
||||
|
||||
let own_user_id = user_id!("@example:morheus.localhost");
|
||||
let olm_machine = OlmMachine::new(own_user_id, "SomeDeviceId".into()).await;
|
||||
olm_machine.import_room_keys(keys, false, |_, _| {}).await.unwrap();
|
||||
olm_machine.store().import_exported_room_keys(keys, |_, _| {}).await.unwrap();
|
||||
|
||||
timeline
|
||||
.inner
|
||||
@@ -304,7 +304,7 @@ async fn retry_edit_and_more() {
|
||||
|
||||
let olm_machine = OlmMachine::new(user_id!("@jptest:matrix.org"), DEVICE_ID.into()).await;
|
||||
let keys = decrypt_room_key_export(Cursor::new(SESSION_KEY), "testing").unwrap();
|
||||
olm_machine.import_room_keys(keys, false, |_, _| {}).await.unwrap();
|
||||
olm_machine.store().import_exported_room_keys(keys, |_, _| {}).await.unwrap();
|
||||
|
||||
timeline
|
||||
.inner
|
||||
@@ -389,7 +389,7 @@ async fn retry_message_decryption_highlighted() {
|
||||
let exported_keys = decrypt_room_key_export(Cursor::new(SESSION_KEY), "1234").unwrap();
|
||||
|
||||
let olm_machine = OlmMachine::new(own_user_id, "SomeDeviceId".into()).await;
|
||||
olm_machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
|
||||
olm_machine.store().import_exported_room_keys(exported_keys, |_, _| {}).await.unwrap();
|
||||
|
||||
timeline
|
||||
.inner
|
||||
|
||||
@@ -438,7 +438,7 @@ async fn read_receipts_updates_on_message_decryption() {
|
||||
let exported_keys = decrypt_room_key_export(Cursor::new(SESSION_KEY), "1234").unwrap();
|
||||
|
||||
let olm_machine = OlmMachine::new(own_user_id, "SomeDeviceId".into()).await;
|
||||
olm_machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
|
||||
olm_machine.store().import_exported_room_keys(exported_keys, |_, _| {}).await.unwrap();
|
||||
|
||||
timeline
|
||||
.inner
|
||||
|
||||
@@ -33,6 +33,10 @@ Bug fixes:
|
||||
|
||||
Additions:
|
||||
|
||||
- Add secret storage support, the secret store can be opened using the
|
||||
`Client::encryption()::open_secret_store()` method, which allows you to import
|
||||
or export secrets from the account-data backed secret-store.
|
||||
|
||||
- Add `VerificationRequest::state` and `VerificationRequest::changes` to check
|
||||
and listen to changes in the state of the `VerificationRequest`. This removes
|
||||
the need to listen to individual matrix events once the `VerificationRequest`
|
||||
|
||||
@@ -62,6 +62,7 @@ docsrs = ["e2e-encryption", "sqlite", "sso-login", "qrcode", "image-proc"]
|
||||
[dependencies]
|
||||
anyhow = { workspace = true, optional = true }
|
||||
anymap2 = "0.13.0"
|
||||
aquamarine = "0.3.2"
|
||||
as_variant = { workspace = true }
|
||||
async-channel = "1.9.0"
|
||||
async-stream = { workspace = true }
|
||||
@@ -96,6 +97,7 @@ serde_json = { workspace = true }
|
||||
sha2 = { workspace = true, optional = true }
|
||||
tempfile = "3.3.0"
|
||||
thiserror = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tower = { version = "0.4.13", features = ["make"], optional = true }
|
||||
tracing = { workspace = true, features = ["attributes"] }
|
||||
url = "2.2.2"
|
||||
|
||||
@@ -464,7 +464,7 @@ impl ClientBuilder {
|
||||
oidc: OidcCtx::new(authentication_server_info, allow_insecure_oidc),
|
||||
});
|
||||
|
||||
let inner = Arc::new(ClientInner::new(
|
||||
let inner = ClientInner::new(
|
||||
auth_ctx,
|
||||
homeserver,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
@@ -475,7 +475,7 @@ impl ClientBuilder {
|
||||
self.respect_login_well_known,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
self.encryption_settings,
|
||||
));
|
||||
);
|
||||
|
||||
debug!("Done building the Client");
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ use crate::{
|
||||
authentication::{AuthCtx, AuthData, ReloadSessionCallback, SaveSessionCallback},
|
||||
config::RequestConfig,
|
||||
deduplicating_handler::DeduplicatingHandler,
|
||||
encryption::{backups::BackupClientState, recovery::RecoveryState},
|
||||
error::{HttpError, HttpResult},
|
||||
event_handler::{
|
||||
EventHandler, EventHandlerDropGuard, EventHandlerHandle, EventHandlerStore, SyncEvent,
|
||||
@@ -91,8 +92,10 @@ use crate::{
|
||||
|
||||
mod builder;
|
||||
pub(crate) mod futures;
|
||||
mod tasks;
|
||||
|
||||
pub use self::builder::{ClientBuildError, ClientBuilder};
|
||||
use self::tasks::{BackupUploadingTask, ClientTasks};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
type NotificationHandlerFut = Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||
@@ -142,9 +145,29 @@ pub struct Client {
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ClientLocks {
|
||||
/// Lock ensuring that only a single room may be marked as a DM at once.
|
||||
/// Look at the [`Room::mark_as_dm()`] method for a more detailed
|
||||
/// Look at the [`Account::mark_as_dm()`] method for a more detailed
|
||||
/// explanation.
|
||||
pub(crate) mark_as_dm_lock: Mutex<()>,
|
||||
/// Lock ensuring that only a single secret store is getting opened at the
|
||||
/// same time.
|
||||
///
|
||||
/// This is important so we don't accidentally create multiple different new
|
||||
/// default secret storage keys.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) open_secret_store_lock: Mutex<()>,
|
||||
/// Lock ensuring that we're only storing a single secret at a time.
|
||||
///
|
||||
/// Take a look at the [`SecretStore::put_secret`] method for a more
|
||||
/// detailed explanation.
|
||||
///
|
||||
/// [`SecretStore::put_secret`]: crate::encryption::secret_storage::SecretStore::put_secret
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) store_secret_lock: Mutex<()>,
|
||||
/// Lock ensuring that only one method at a time might modify our backup.
|
||||
pub(crate) backup_modify_lock: Mutex<()>,
|
||||
/// Lock ensuring that we're going to attempt to upload backups for a single
|
||||
/// requester.
|
||||
pub(crate) backup_upload_lock: Mutex<()>,
|
||||
/// Handler making sure we only have one group session sharing request in
|
||||
/// flight per room.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -202,6 +225,7 @@ pub(crate) struct ClientInner {
|
||||
/// to ensure that only a single call to a method happens at once or to
|
||||
/// deduplicate multiple calls to a method.
|
||||
locks: ClientLocks,
|
||||
pub(crate) tasks: StdMutex<ClientTasks>,
|
||||
pub(crate) typing_notice_times: StdRwLock<BTreeMap<OwnedRoomId, Instant>>,
|
||||
/// Event handlers. See `add_event_handler`.
|
||||
pub(crate) event_handlers: EventHandlerStore,
|
||||
@@ -220,6 +244,8 @@ pub(crate) struct ClientInner {
|
||||
/// End-to-end encryption settings.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) encryption_settings: EncryptionSettings,
|
||||
pub(crate) backups_state: BackupClientState,
|
||||
pub(crate) recovery_state: SharedObservable<RecoveryState>,
|
||||
}
|
||||
|
||||
impl ClientInner {
|
||||
@@ -238,14 +264,15 @@ impl ClientInner {
|
||||
server_versions: Option<Box<[MatrixVersion]>>,
|
||||
respect_login_well_known: bool,
|
||||
#[cfg(feature = "e2e-encryption")] encryption_settings: EncryptionSettings,
|
||||
) -> Self {
|
||||
Self {
|
||||
) -> Arc<Self> {
|
||||
let client = Self {
|
||||
homeserver: StdRwLock::new(homeserver),
|
||||
auth_ctx,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
sliding_sync_proxy: StdRwLock::new(sliding_sync_proxy),
|
||||
http_client,
|
||||
base_client,
|
||||
tasks: StdMutex::new(Default::default()),
|
||||
locks: Default::default(),
|
||||
server_versions: OnceCell::new_with(server_versions),
|
||||
typing_notice_times: Default::default(),
|
||||
@@ -256,7 +283,16 @@ impl ClientInner {
|
||||
sync_beat: event_listener::Event::new(),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
encryption_settings,
|
||||
}
|
||||
backups_state: Default::default(),
|
||||
recovery_state: Default::default(),
|
||||
};
|
||||
|
||||
let client = Arc::new(client);
|
||||
let weak_client = Arc::downgrade(&client);
|
||||
|
||||
client.tasks.lock().unwrap().upload_room_keys = Some(BackupUploadingTask::new(weak_client));
|
||||
|
||||
client
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,6 +515,13 @@ impl Client {
|
||||
Oidc::new(self.clone())
|
||||
}
|
||||
|
||||
pub async fn logout(&self) -> Result<()> {
|
||||
let request = ruma::api::client::session::logout::v3::Request::new();
|
||||
self.send(request, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register a handler for a specific event type.
|
||||
///
|
||||
/// The handler is a function or closure with one or more arguments. The
|
||||
@@ -892,6 +935,11 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn set_session_meta(&self, session_meta: SessionMeta) -> Result<()> {
|
||||
self.base_client().set_session_meta(session_meta).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Refresh the access token using the authentication API used to log into
|
||||
/// this session.
|
||||
///
|
||||
@@ -1910,7 +1958,7 @@ impl Client {
|
||||
/// Create a new specialized `Client` that can process notifications.
|
||||
pub async fn notification_client(&self) -> Result<Client> {
|
||||
let client = Client {
|
||||
inner: Arc::new(ClientInner::new(
|
||||
inner: ClientInner::new(
|
||||
self.inner.auth_ctx.clone(),
|
||||
self.homeserver(),
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
@@ -1921,7 +1969,7 @@ impl Client {
|
||||
self.inner.respect_login_well_known,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
self.inner.encryption_settings,
|
||||
)),
|
||||
),
|
||||
};
|
||||
|
||||
// Copy the parent's session meta into the child. This initializes the in-memory
|
||||
@@ -1932,7 +1980,7 @@ impl Client {
|
||||
// overwrite the session information shared with the parent too, and it
|
||||
// must be initialized at most once.
|
||||
if let Some(session) = self.session() {
|
||||
client.base_client().set_session_meta(session.into_meta()).await?;
|
||||
client.set_session_meta(session.into_meta()).await?;
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Weak;
|
||||
|
||||
use tokio::sync::mpsc::{self, UnboundedReceiver};
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use super::ClientInner;
|
||||
use crate::{
|
||||
encryption::backups::UploadState,
|
||||
executor::{spawn, JoinHandle},
|
||||
Client,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ClientTasks {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) upload_room_keys: Option<BackupUploadingTask>,
|
||||
pub(crate) auto_enable_backup_and_recovery: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) struct BackupUploadingTask {
|
||||
sender: mpsc::UnboundedSender<()>,
|
||||
join_handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
impl Drop for BackupUploadingTask {
|
||||
fn drop(&mut self) {
|
||||
self.join_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
impl BackupUploadingTask {
|
||||
pub fn new(client: Weak<ClientInner>) -> Self {
|
||||
let (sender, receiver) = mpsc::unbounded_channel();
|
||||
|
||||
let join_handle = spawn(async move {
|
||||
Self::listen(client, receiver).await;
|
||||
});
|
||||
|
||||
Self { sender, join_handle }
|
||||
}
|
||||
|
||||
pub fn trigger_upload(&self) {
|
||||
let _ = self.sender.send(());
|
||||
}
|
||||
|
||||
pub async fn listen(client: Weak<ClientInner>, mut receiver: UnboundedReceiver<()>) {
|
||||
while receiver.recv().await.is_some() {
|
||||
if let Some(client) = client.upgrade() {
|
||||
let client = Client { inner: client };
|
||||
|
||||
if let Err(e) = client.encryption().backups().backup_room_keys().await {
|
||||
client.inner.backups_state.upload_progress.set(UploadState::Error);
|
||||
warn!("Error backing up room keys {e:?}");
|
||||
}
|
||||
|
||||
client.inner.backups_state.upload_progress.set(UploadState::Idle);
|
||||
} else {
|
||||
trace!("Client got dropped, shutting down the task");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{future::IntoFuture, pin::Pin, time::Duration};
|
||||
|
||||
use futures_core::{Future, Stream};
|
||||
use futures_util::StreamExt;
|
||||
use thiserror::Error;
|
||||
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
|
||||
use tracing::trace;
|
||||
|
||||
use super::{Backups, ChannelObservable, UploadState};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Error)]
|
||||
pub enum SteadyStateError {
|
||||
#[error("The backup got disabled while waiting for the room keys to be uploaded.")]
|
||||
BackupDisabled,
|
||||
#[error("There was a connection error.")]
|
||||
Connection,
|
||||
#[error("We couldn't read status updates from the upload task quickly enough.")]
|
||||
Laged,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WaitForSteadyState<'a> {
|
||||
pub(super) backups: &'a Backups,
|
||||
pub(super) progress: ChannelObservable<UploadState>,
|
||||
pub(super) timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl<'a> WaitForSteadyState<'a> {
|
||||
pub fn subscribe_to_progress(
|
||||
&self,
|
||||
) -> impl Stream<Item = Result<UploadState, BroadcastStreamRecvError>> {
|
||||
self.progress.subscribe()
|
||||
}
|
||||
|
||||
pub fn with_delay(mut self, delay: Duration) -> Self {
|
||||
self.timeout = Some(delay);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFuture for WaitForSteadyState<'a> {
|
||||
type Output = Result<(), SteadyStateError>;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output + 'a>>>;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let Self { backups, timeout, progress } = self;
|
||||
|
||||
trace!("Creating a stream to wait for the steady state");
|
||||
|
||||
let mut stream = progress.subscribe();
|
||||
|
||||
let old_delay = if let Some(delay) = timeout {
|
||||
let mut lock = backups.client.inner.backups_state.upload_delay.write().unwrap();
|
||||
let old_delay = Some(lock.to_owned());
|
||||
|
||||
*lock = delay;
|
||||
|
||||
old_delay
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
trace!("Waiting for the upload steady state");
|
||||
|
||||
let ret = if backups.are_enabled().await {
|
||||
backups.maybe_trigger_backup();
|
||||
|
||||
let mut ret = Ok(());
|
||||
|
||||
// TODO: Do we want to be smart here and remember the count when we started
|
||||
// waiting and prevent the total from increasing, in case new room
|
||||
// keys arrive after we started waiting.
|
||||
while let Some(state) = stream.next().await {
|
||||
trace!(?state, "Update state while waiting for the backup steady state");
|
||||
|
||||
match state {
|
||||
Ok(UploadState::Done) => {
|
||||
ret = Ok(());
|
||||
break;
|
||||
}
|
||||
Ok(UploadState::Error) => {
|
||||
if backups.are_enabled().await {
|
||||
ret = Err(SteadyStateError::Connection);
|
||||
} else {
|
||||
ret = Err(SteadyStateError::BackupDisabled);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
ret = Err(SteadyStateError::Laged);
|
||||
break;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
} else {
|
||||
Err(SteadyStateError::BackupDisabled)
|
||||
};
|
||||
|
||||
if let Some(old_delay) = old_delay {
|
||||
let mut lock = backups.client.inner.backups_state.upload_delay.write().unwrap();
|
||||
*lock = old_delay;
|
||||
}
|
||||
|
||||
ret
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,11 +55,14 @@ use ruma::{
|
||||
DeviceId, OwnedDeviceId, OwnedUserId, TransactionId, UserId,
|
||||
};
|
||||
use tokio::sync::RwLockReadGuard;
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
use tracing::{debug, error, instrument, trace, warn};
|
||||
|
||||
use self::{
|
||||
backups::Backups,
|
||||
futures::PrepareEncryptedFile,
|
||||
identities::{DeviceUpdates, IdentityUpdates},
|
||||
recovery::Recovery,
|
||||
secret_storage::SecretStorage,
|
||||
};
|
||||
use crate::{
|
||||
attachment::{AttachmentInfo, Thumbnail},
|
||||
@@ -72,8 +75,11 @@ use crate::{
|
||||
Client, Error, Result, Room, TransmissionProgress,
|
||||
};
|
||||
|
||||
pub mod backups;
|
||||
pub mod futures;
|
||||
pub mod identities;
|
||||
pub mod recovery;
|
||||
pub mod secret_storage;
|
||||
pub mod verification;
|
||||
|
||||
pub use matrix_sdk_base::crypto::{
|
||||
@@ -97,6 +103,21 @@ pub struct EncryptionSettings {
|
||||
/// This requires to login with a username and password, or that MSC3967 is
|
||||
/// enabled on the server, as of 2023-10-20.
|
||||
pub auto_enable_cross_signing: bool,
|
||||
|
||||
/// Automatically download all room keys from the backup when the backup
|
||||
/// recovery key has been received. The backup recovery key can be received
|
||||
/// in two ways:
|
||||
///
|
||||
/// 1. Received as a `m.secret.send` to-device event, after a successful
|
||||
/// interactive verification.
|
||||
/// 2. Imported from secret storage using the
|
||||
/// [`SecretStore::import_secrets()`] method.
|
||||
///
|
||||
/// [`SecretStore::import_secrets()`]: crate::encryption::secret_storage::SecretStore::import_secrets
|
||||
pub auto_download_from_backup: bool,
|
||||
|
||||
/// Automatically create a backup version if no backup exists.
|
||||
pub auto_enable_backups: bool,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@@ -135,6 +156,7 @@ impl Client {
|
||||
|
||||
let response = self.send(request, None).await?;
|
||||
self.mark_request_as_sent(request_id, &response).await?;
|
||||
self.encryption().recovery().update_state_after_keys_query(&response).await;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
@@ -1034,7 +1056,7 @@ impl Encryption {
|
||||
/// );
|
||||
/// # anyhow::Ok(()) };
|
||||
/// ```
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(all(not(target_arch = "wasm32")))]
|
||||
pub async fn import_room_keys(
|
||||
&self,
|
||||
path: PathBuf,
|
||||
@@ -1052,7 +1074,26 @@ impl Encryption {
|
||||
let task = tokio::task::spawn_blocking(decrypt);
|
||||
let import = task.await.expect("Task join error")?;
|
||||
|
||||
Ok(olm.import_room_keys(import, false, |_, _| {}).await?)
|
||||
let ret = olm.store().import_exported_room_keys(import, |_, _| {}).await?;
|
||||
|
||||
self.backups().maybe_trigger_backup();
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Get the backups manager of the client.
|
||||
pub fn backups(&self) -> Backups {
|
||||
Backups { client: self.client.to_owned() }
|
||||
}
|
||||
|
||||
/// Get the secret storage manager of the client.
|
||||
pub fn secret_storage(&self) -> SecretStorage {
|
||||
SecretStorage { client: self.client.to_owned() }
|
||||
}
|
||||
|
||||
/// Get the recovery manager of the client.
|
||||
pub fn recovery(&self) -> Recovery {
|
||||
Recovery { client: self.client.to_owned() }
|
||||
}
|
||||
|
||||
/// Enables the crypto-store cross-process lock.
|
||||
@@ -1168,6 +1209,32 @@ impl Encryption {
|
||||
let olm_machine = olm_machine.as_ref().ok_or(Error::AuthenticationRequired)?;
|
||||
Ok(olm_machine.uploaded_key_count().await?)
|
||||
}
|
||||
|
||||
/// Enables automated backup and recovery.
|
||||
pub async fn enable_backups_and_recovery(&self) {
|
||||
let mut tasks = self.client.inner.tasks.lock().unwrap();
|
||||
|
||||
let this = self.clone();
|
||||
tasks.auto_enable_backup_and_recovery = Some(tokio::spawn(async move {
|
||||
if let Err(e) = this.backups().setup_and_resume().await {
|
||||
error!("Couldn't setup and resume backups {e:?}");
|
||||
}
|
||||
if let Err(e) = this.recovery().setup().await {
|
||||
warn!("Couldn't auto enable recovery {e:?}");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Waits for the backup and recovery enabling to finish, if requested with
|
||||
/// [`Self::enable_backups_and_recovery`].
|
||||
pub async fn wait_for_backups_and_recovery(&self) {
|
||||
let task = self.client.inner.tasks.lock().unwrap().auto_enable_backup_and_recovery.take();
|
||||
if let Some(task) = task {
|
||||
if let Err(err) = task.await {
|
||||
warn!("error when initializing backups and recovery: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::{future::IntoFuture, pin::Pin};
|
||||
|
||||
use futures_core::{Future, Stream};
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk_base::crypto::store::RoomKeyCounts;
|
||||
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use super::Recovery;
|
||||
use crate::encryption::{
|
||||
backups::{ChannelObservable, UploadState},
|
||||
secret_storage::{Result, SecretStore},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Zeroize, ZeroizeOnDrop)]
|
||||
pub enum EnableProgress {
|
||||
CreatingBackup,
|
||||
CreatingRecoveryKey,
|
||||
#[zeroize(skip)]
|
||||
BackingUp(RoomKeyCounts),
|
||||
Done {
|
||||
recovery_key: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for EnableProgress {
|
||||
fn default() -> Self {
|
||||
Self::CreatingBackup
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Enable<'a> {
|
||||
pub(super) recovery: &'a Recovery,
|
||||
pub(super) progress: ChannelObservable<EnableProgress>,
|
||||
pub(super) wait_for_backups_upload: bool,
|
||||
pub(super) passphrase: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> Enable<'a> {
|
||||
pub(super) fn new(recovery: &'a Recovery) -> Self {
|
||||
Self {
|
||||
recovery,
|
||||
progress: Default::default(),
|
||||
wait_for_backups_upload: false,
|
||||
passphrase: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe_to_progress(
|
||||
&self,
|
||||
) -> impl Stream<Item = Result<EnableProgress, BroadcastStreamRecvError>> {
|
||||
self.progress.subscribe()
|
||||
}
|
||||
|
||||
pub fn wait_for_backups_to_upload(mut self) -> Self {
|
||||
self.wait_for_backups_upload = true;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_passphrase(mut self, passphrase: &'a str) -> Self {
|
||||
self.passphrase = Some(passphrase);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFuture for Enable<'a> {
|
||||
type Output = Result<String>;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'a>>;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let Self { recovery, progress, wait_for_backups_upload, passphrase } = self;
|
||||
|
||||
if !recovery.client.encryption().backups().are_enabled().await {
|
||||
progress.set(EnableProgress::CreatingBackup);
|
||||
recovery.mark_backup_as_enabled().await?;
|
||||
recovery.client.encryption().backups().create().await?;
|
||||
}
|
||||
|
||||
progress.set(EnableProgress::CreatingRecoveryKey);
|
||||
|
||||
let secret_storage = recovery.client.encryption().secret_storage();
|
||||
|
||||
let create_store = if let Some(passphrase) = passphrase {
|
||||
secret_storage.create_secret_store().with_passphrase(passphrase)
|
||||
} else {
|
||||
secret_storage.create_secret_store()
|
||||
};
|
||||
|
||||
let store: SecretStore = create_store.await?;
|
||||
|
||||
if wait_for_backups_upload {
|
||||
let backups = recovery.client.encryption().backups();
|
||||
let upload_future = backups.wait_for_steady_state();
|
||||
let upload_progress = upload_future.subscribe_to_progress();
|
||||
|
||||
let progress_task = matrix_sdk_common::executor::spawn({
|
||||
let progress = progress.clone();
|
||||
async move {
|
||||
pin_mut!(upload_progress);
|
||||
|
||||
while let Some(update) = upload_progress.next().await {
|
||||
match update {
|
||||
Ok(UploadState::Uploading(count)) => {
|
||||
progress.set(EnableProgress::BackingUp(count));
|
||||
}
|
||||
Ok(UploadState::Done) => break,
|
||||
Ok(UploadState::Error) => break,
|
||||
Err(_) => break,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(_) = upload_future.await {
|
||||
// TODO: What do we want to do here?
|
||||
todo!()
|
||||
}
|
||||
|
||||
progress_task.abort();
|
||||
} else {
|
||||
recovery.client.encryption().backups().maybe_trigger_backup();
|
||||
}
|
||||
|
||||
let key = store.secret_storage_key();
|
||||
|
||||
progress.set(EnableProgress::Done { recovery_key: key });
|
||||
recovery.update_recovery_state().await?;
|
||||
|
||||
Ok(store.secret_storage_key())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Reset<'a> {
|
||||
pub(super) recovery: &'a Recovery,
|
||||
pub(super) passphrase: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> Reset<'a> {
|
||||
pub(super) fn new(recovery: &'a Recovery) -> Self {
|
||||
Self { recovery, passphrase: None }
|
||||
}
|
||||
|
||||
pub fn with_passphrase(mut self, passphrase: &'a str) -> Self {
|
||||
self.passphrase = Some(passphrase);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFuture for Reset<'a> {
|
||||
type Output = Result<String>;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'a>>;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let Self { recovery, passphrase } = self;
|
||||
|
||||
let secret_storage = recovery.client.encryption().secret_storage();
|
||||
|
||||
let create_store = if let Some(passphrase) = passphrase {
|
||||
secret_storage.create_secret_store().with_passphrase(passphrase)
|
||||
} else {
|
||||
secret_storage.create_secret_store()
|
||||
};
|
||||
|
||||
let store: SecretStore = create_store.await?;
|
||||
recovery.update_recovery_state().await?;
|
||||
|
||||
Ok(store.secret_storage_key())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RecoverAndReset<'a> {
|
||||
pub(super) recovery: &'a Recovery,
|
||||
pub(super) old_recovery_key: &'a str,
|
||||
pub(super) passphrase: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> RecoverAndReset<'a> {
|
||||
pub(super) fn new(recovery: &'a Recovery, old_recovery_key: &'a str) -> Self {
|
||||
Self { recovery, old_recovery_key, passphrase: None }
|
||||
}
|
||||
|
||||
pub fn with_passphrase(mut self, passphrase: &'a str) -> Self {
|
||||
self.passphrase = Some(passphrase);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFuture for RecoverAndReset<'a> {
|
||||
type Output = Result<String>;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'a>>;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let Self { recovery, old_recovery_key, passphrase } = self;
|
||||
|
||||
recovery.recover(old_recovery_key).await?;
|
||||
|
||||
let reset = if let Some(passphrase) = passphrase {
|
||||
recovery.reset_key().with_passphrase(passphrase)
|
||||
} else {
|
||||
recovery.reset_key()
|
||||
};
|
||||
|
||||
reset.await
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
//! The recovery module
|
||||
//!
|
||||
//! The recovery module attempts to provide a unified and simplified view over
|
||||
//! the secret storage and backup subsystems.
|
||||
//!
|
||||
//! Dear spec connosieuers, recovery here is not the same as the [`Recovery
|
||||
//! key`] mentioned in the spec. The recovery key from the spec is solely about
|
||||
//! backups, while the term recovery in this file includes both the backups
|
||||
//! and the secret storage mechanism. The recovery key mentioned in this file is
|
||||
//! the secret storage key.
|
||||
//!
|
||||
//! [`Recovery key`]: https://spec.matrix.org/v1.8/client-server-api/#recovery-key
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use futures_core::Stream;
|
||||
use ruma::{
|
||||
api::client::keys::get_keys,
|
||||
events::{
|
||||
secret::send::ToDeviceSecretSendEvent,
|
||||
secret_storage::default_key::SecretStorageDefaultKeyEvent, EventContent,
|
||||
GlobalAccountDataEventType,
|
||||
},
|
||||
exports::ruma_macros::EventContent,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{encryption::secret_storage::Result, Client};
|
||||
|
||||
mod futures;
|
||||
|
||||
pub use futures::{Enable, EnableProgress, RecoverAndReset, Reset};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub enum RecoveryState {
|
||||
/// We didn't yet inform ourselves about the state of things.
|
||||
#[default]
|
||||
Unknown,
|
||||
/// Secret storage is setup and we have all the secrets locally.
|
||||
Enabled,
|
||||
/// No default secret storage key exists or it is disabled explicitly using
|
||||
/// the account data event.
|
||||
Disabled,
|
||||
/// Secret storage is setup but we're missing some secrets.
|
||||
Incomplete,
|
||||
}
|
||||
|
||||
// TODO: This should likely be in the matrix-sdk-ui crate.
|
||||
|
||||
/// A hack to allow the `m.secret_storage.default_key` event to be "deleted".
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
|
||||
#[ruma_event(type = "m.secret_storage.default_key", kind = GlobalAccountData)]
|
||||
struct SecretStorageDisabledContent {}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
|
||||
#[ruma_event(type = "m.org.matrix.custom.backup_disabled", kind = GlobalAccountData)]
|
||||
struct BackupDisabledContent {
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl BackupDisabledContent {
|
||||
fn event_type() -> GlobalAccountDataEventType {
|
||||
// This is dumb, there's got to be a better way to get to the event type?
|
||||
Self { disabled: false }.event_type()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Recovery {
|
||||
pub(super) client: Client,
|
||||
}
|
||||
|
||||
impl Recovery {
|
||||
pub fn state(&self) -> RecoveryState {
|
||||
self.client.inner.recovery_state.get()
|
||||
}
|
||||
|
||||
pub fn state_stream(&self) -> impl Stream<Item = RecoveryState> {
|
||||
self.client.inner.recovery_state.subscribe_reset()
|
||||
}
|
||||
|
||||
/// Enable secret storage *and* backups.
|
||||
pub fn enable(&self) -> Enable<'_> {
|
||||
Enable::new(self)
|
||||
}
|
||||
|
||||
/// Enable backups only.
|
||||
pub async fn enable_backup(&self) -> Result<()> {
|
||||
self.mark_backup_as_enabled().await?;
|
||||
self.client.encryption().backups().create().await?;
|
||||
self.client.encryption().backups().maybe_trigger_backup();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disable recovery completely.
|
||||
///
|
||||
/// This method will disable the uploading of room keys to a backup, delete
|
||||
/// the currently active backup version, and remove the default secret
|
||||
/// storage key. It will not delete the `m.secret_storage.default_key`
|
||||
/// global account data even since that's not possible, but it will set it
|
||||
/// to an invalid event. Can someone explain me why a key/value store
|
||||
/// doesn't have a `DELETE` method?
|
||||
pub async fn disable(&self) -> Result<()> {
|
||||
self.client.encryption().backups().disable().await?;
|
||||
// Why oh why, can't we delete account data events?
|
||||
self.client.account().set_account_data(SecretStorageDisabledContent {}).await?;
|
||||
self.client.account().set_account_data(BackupDisabledContent { disabled: true }).await?;
|
||||
// TODO: Do we want to "delete" the known secrets as well?
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reset the recovery key.
|
||||
///
|
||||
/// This will rotate the secret storage key and re-upload all the secrets to
|
||||
/// the [`SecretStore`].
|
||||
///
|
||||
/// [`SecretStore`]: crate::encryption::secret_storage::SecretStore
|
||||
pub fn reset_key(&self) -> Reset<'_> {
|
||||
Reset::new(self)
|
||||
}
|
||||
|
||||
/// Reset the recovery key but first import all the secrets from foobar.
|
||||
pub fn recover_and_reset<'a>(&'a self, old_key: &'a str) -> RecoverAndReset<'_> {
|
||||
RecoverAndReset::new(self, old_key)
|
||||
}
|
||||
|
||||
/// What the fuck is this supposed to do if not fetch the secrets from
|
||||
/// secret storage? How is that different from the initial restore stuff
|
||||
/// from secret storage flow?
|
||||
pub async fn recover(&self, recovery_key: &str) -> Result<()> {
|
||||
let store =
|
||||
self.client.encryption().secret_storage().open_secret_store(recovery_key).await?;
|
||||
|
||||
store.import_secrets().await?;
|
||||
self.update_recovery_state().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn is_recovery_enabled(&self) -> Result<bool> {
|
||||
// TODO: Should this take backups into consideration?
|
||||
Ok(self.client.encryption().secret_storage().is_enabled().await?)
|
||||
}
|
||||
|
||||
/// Is this device the last device the user has.
|
||||
pub async fn are_we_the_last_man_standing(&self) -> Result<bool> {
|
||||
let olm_machine = self.client.olm_machine().await;
|
||||
let olm_machine = olm_machine.as_ref().ok_or(crate::Error::NoOlmMachine)?;
|
||||
let user_id = olm_machine.user_id();
|
||||
|
||||
self.client.encryption().ensure_initial_key_query().await?;
|
||||
|
||||
let devices = self.client.encryption().get_user_devices(user_id).await?;
|
||||
|
||||
Ok(devices.devices().count() == 1)
|
||||
}
|
||||
|
||||
async fn all_known_secrets_available(&self) -> Result<bool> {
|
||||
let cross_signing_complete = self
|
||||
.client
|
||||
.encryption()
|
||||
.cross_signing_status()
|
||||
.await
|
||||
.map(|status| status.is_complete())
|
||||
.unwrap_or_default();
|
||||
|
||||
if self.are_backups_disabled().await? {
|
||||
Ok(cross_signing_complete)
|
||||
} else {
|
||||
Ok(self.client.encryption().backups().are_enabled().await && cross_signing_complete)
|
||||
}
|
||||
}
|
||||
|
||||
async fn should_auto_enable_backups(&self) -> Result<bool> {
|
||||
// If we didn't already enable backups, we don't see a backup version on the
|
||||
// server and finally if backups have not been marked to be explicitly
|
||||
// disabled, then we can automatically enable them.
|
||||
Ok(self.client.inner.encryption_settings.auto_enable_backups
|
||||
&& !self.client.encryption().backups().are_enabled().await
|
||||
&& !self.client.encryption().backups().exists_on_server().await?
|
||||
&& !self.are_backups_disabled().await?)
|
||||
}
|
||||
|
||||
pub(crate) async fn setup(&self) -> Result<()> {
|
||||
info!("Setting up account data listeners and trying to setup recovery");
|
||||
|
||||
self.update_recovery_state().await?;
|
||||
|
||||
if self.should_auto_enable_backups().await? {
|
||||
self.enable_backup().await?;
|
||||
}
|
||||
|
||||
self.client.add_event_handler(Self::default_key_event_handler);
|
||||
self.client.add_event_handler(Self::secret_send_event_handler);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn are_backups_disabled(&self) -> Result<bool> {
|
||||
Ok(self
|
||||
.client
|
||||
.account()
|
||||
.fetch_account_data(BackupDisabledContent::event_type())
|
||||
.await?
|
||||
.map(|event| {
|
||||
event
|
||||
.deserialize_as::<BackupDisabledContent>()
|
||||
.map(|event| event.disabled)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.unwrap_or(false))
|
||||
}
|
||||
|
||||
async fn mark_backup_as_enabled(&self) -> Result<()> {
|
||||
self.client.account().set_account_data(BackupDisabledContent { disabled: false }).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_recovery_state(&self) -> Result<RecoveryState> {
|
||||
Ok(if self.is_recovery_enabled().await? {
|
||||
if self.all_known_secrets_available().await? {
|
||||
RecoveryState::Enabled
|
||||
} else {
|
||||
RecoveryState::Incomplete
|
||||
}
|
||||
} else {
|
||||
RecoveryState::Disabled
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_recovery_state(&self) -> Result<()> {
|
||||
let new_state = self.check_recovery_state().await?;
|
||||
self.client.inner.recovery_state.set(new_state);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn secret_send_event_handler(_: ToDeviceSecretSendEvent, client: Client) {
|
||||
if let Err(e) = client.encryption().recovery().update_recovery_state().await {
|
||||
error!(
|
||||
"Coulnd't update the recovery state after the receival of a secret send \
|
||||
event {e:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn default_key_event_handler(_: SecretStorageDefaultKeyEvent, client: Client) {
|
||||
if let Err(e) = client.encryption().recovery().update_recovery_state().await {
|
||||
error!(
|
||||
"Coulnd't update the recovery state after the receival of a new \
|
||||
default_key event {e:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn update_state_after_backup_disabling(&self) {
|
||||
// TODO: This is quite ugly, backups shouldn't depend on recovery,
|
||||
// recovery should listen to the backup state change.
|
||||
if let Err(e) = self.update_recovery_state().await {
|
||||
error!("Coulnd't update the recovery state after backups were disabled {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn update_state_after_keys_query(&self, response: &get_keys::v3::Response) {
|
||||
if let Some(user_id) = self.client.user_id() {
|
||||
if response.master_keys.contains_key(user_id) {
|
||||
// TODO: This is unnecessarily expensive, we could let the crypto crate notify
|
||||
// us that our private keys got erased... Butt, the OlmMachine
|
||||
// gets recreated and... You know the drill by now...
|
||||
if let Err(e) = self.update_recovery_state().await {
|
||||
error!(
|
||||
"Couldn't update the recovery state after a /keys/query updated our \
|
||||
user identity {e:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{future::IntoFuture, pin::Pin};
|
||||
|
||||
use futures_core::Future;
|
||||
use matrix_sdk_base::crypto::secret_storage::SecretStorageKey;
|
||||
use ruma::events::secret_storage::default_key::SecretStorageDefaultKeyEventContent;
|
||||
|
||||
use super::{Result, SecretStorage, SecretStore};
|
||||
|
||||
/// Future returned by [`SecretStorage::create_secret_store()`].
|
||||
#[derive(Debug)]
|
||||
pub struct CreateStore<'a> {
|
||||
pub(super) secret_storage: &'a SecretStorage,
|
||||
pub(super) passphrase: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> CreateStore<'a> {
|
||||
/// Set the passphrase for the new [`SecretStore`].
|
||||
///
|
||||
/// See the documentation for the [`SecretStorage::create_secret_store()`]
|
||||
/// method for more info.
|
||||
pub fn with_passphrase(mut self, passphrase: &'a str) -> Self {
|
||||
self.passphrase = Some(passphrase);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFuture for CreateStore<'a> {
|
||||
type Output = Result<SecretStore>;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'a>>;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
let Self { secret_storage, passphrase } = self;
|
||||
|
||||
Box::pin(async move {
|
||||
// Prevent multiple simultaneous calls to this method.
|
||||
//
|
||||
// See the documentation for the lock in the `store_secret` method for more
|
||||
// info.
|
||||
let client_copy = secret_storage.client.to_owned();
|
||||
let _guard = client_copy.locks().open_secret_store_lock.lock().await;
|
||||
|
||||
let new_key = if let Some(passphrase) = passphrase {
|
||||
SecretStorageKey::new_from_passphrase(passphrase)
|
||||
} else {
|
||||
SecretStorageKey::new()
|
||||
};
|
||||
|
||||
let content = new_key.event_content().to_owned();
|
||||
|
||||
secret_storage.client.account().set_account_data(content).await?;
|
||||
|
||||
let store = SecretStore { client: secret_storage.client.to_owned(), key: new_key };
|
||||
store.export_secrets().await?;
|
||||
|
||||
let default_key_content =
|
||||
SecretStorageDefaultKeyEventContent::new(store.key.key_id().to_owned());
|
||||
|
||||
store.client.account().set_account_data(default_key_content).await?;
|
||||
|
||||
Ok(store)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
//! Secret Storage Support
|
||||
//!
|
||||
//! This submodule provides essential functionality for secret storage in
|
||||
//! compliance with the [Matrix protocol specification][spec].
|
||||
//!
|
||||
//! Secret storage is a critical component that provides an encrypted
|
||||
//! key/value storage system. It leverages [account data] events stored on the
|
||||
//! Matrix homeserver to ensure secure and private storage of sensitive
|
||||
//! information.
|
||||
//!
|
||||
//! For detailed information and usage guidelines, refer to the documentation of
|
||||
//! the [`SecretStore`] struct.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use matrix_sdk::Client;
|
||||
//! # use url::Url;
|
||||
//! # async {
|
||||
//! # let homeserver = Url::parse("http://example.com")?;
|
||||
//! # let client = Client::new(homeserver).await?;
|
||||
//! use ruma::events::secret::request::SecretName;
|
||||
//!
|
||||
//! // Open the store.
|
||||
//! let secret_store = client
|
||||
//! .encryption()
|
||||
//! .secret_storage()
|
||||
//! .open_secret_store("It's a secret to everybody")
|
||||
//! .await?;
|
||||
//!
|
||||
//! // Import the secrets.
|
||||
//! secret_store.import_secrets().await?;
|
||||
//!
|
||||
//! // Our own device should now be verified.
|
||||
//! let device = client
|
||||
//! .encryption()
|
||||
//! .get_own_device()
|
||||
//! .await?
|
||||
//! .expect("We should be able to retrieve our own device");
|
||||
//!
|
||||
//! assert!(device.is_cross_signed_by_owner());
|
||||
//!
|
||||
//! # anyhow::Ok(()) };
|
||||
//! ```
|
||||
//!
|
||||
//! [spec]: https://spec.matrix.org/v1.8/client-server-api/#secret-storage
|
||||
//! [account data]: https://spec.matrix.org/v1.8/client-server-api/#client-config
|
||||
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
use matrix_sdk_base::crypto::{
|
||||
secret_storage::{DecodeError, MacError, SecretStorageKey},
|
||||
CryptoStoreError, SecretImportError,
|
||||
};
|
||||
use ruma::events::{
|
||||
secret_storage::{
|
||||
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
|
||||
},
|
||||
EventContentFromType, GlobalAccountDataEventType,
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::identities::ManualVerifyError;
|
||||
use crate::Client;
|
||||
|
||||
mod futures;
|
||||
mod secret_store;
|
||||
|
||||
pub use futures::CreateStore;
|
||||
pub use secret_store::SecretStore;
|
||||
|
||||
/// Convenicence type alias for the secret-storage specific results.
|
||||
pub type Result<T, E = SecretStorageError> = std::result::Result<T, E>;
|
||||
|
||||
/// Error type for the secret-storage subsystem.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SecretStorageError {
|
||||
/// A typical SDK error.
|
||||
#[error(transparent)]
|
||||
Sdk(#[from] crate::Error),
|
||||
|
||||
/// Error when deserializing account data events.
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
/// The secret storage key could not have been decoded or verified
|
||||
/// successfully.
|
||||
#[error(transparent)]
|
||||
SecretStorageKey(#[from] DecodeError),
|
||||
|
||||
/// The secret store could not be opened because info about the
|
||||
/// secret-storage key could not have been found in the account data of
|
||||
/// the user.
|
||||
#[error(
|
||||
"The info about the secret key could not have been found in the account data of the user"
|
||||
)]
|
||||
MissingKeyInfo {
|
||||
/// The key ID of the default key. Will be set to the key ID in the
|
||||
/// `m.secret_storage.default_key` event. If the
|
||||
/// `m.secret_storage.default_key` does not exits, will be
|
||||
/// `None`.
|
||||
key_id: Option<String>,
|
||||
},
|
||||
|
||||
/// A secret could not have been imported from the secret store into the
|
||||
/// local store.
|
||||
#[error(transparent)]
|
||||
SecretImportError(#[from] SecretImportError),
|
||||
|
||||
/// A general storage error.
|
||||
#[error(transparent)]
|
||||
Storage(#[from] CryptoStoreError),
|
||||
|
||||
/// An error happened while trying to mark our own device as verified after
|
||||
/// the private cross-signing keys have been imported.
|
||||
#[error(transparent)]
|
||||
Verification(#[from] ManualVerifyError),
|
||||
|
||||
/// Error describing a decryption failure of a secret.
|
||||
#[error(transparent)]
|
||||
Decryption(#[from] DecryptionError),
|
||||
}
|
||||
|
||||
/// Error type describing decryption failures of the secret-storage system.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DecryptionError {
|
||||
/// The secret could not have been decrypted.
|
||||
#[error("Could not decrypt the secret using the secret storage key, invalid MAC.")]
|
||||
Mac(#[from] MacError),
|
||||
|
||||
/// Could not decode the secret, the secret is not valid UTF-8.
|
||||
#[error("Could not decode the secret, the secret is not valid UTF-8")]
|
||||
Utf8(#[from] FromUtf8Error),
|
||||
}
|
||||
|
||||
/// A high-level API to manage secret storage.
|
||||
///
|
||||
/// To get this, use [`Client::encryption()::secret_storage()`].
|
||||
#[derive(Debug)]
|
||||
pub struct SecretStorage {
|
||||
pub(super) client: Client,
|
||||
}
|
||||
|
||||
impl SecretStorage {
|
||||
/// Open the [`SecretStore`] with the given `key`.
|
||||
///
|
||||
/// The `secret_storage_key` can be a passphrase or a Base58 encoded secret
|
||||
/// storage key.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk::Client;
|
||||
/// # use url::Url;
|
||||
/// # async {
|
||||
/// # let homeserver = Url::parse("http://example.com")?;
|
||||
/// # let client = Client::new(homeserver).await?;
|
||||
/// use ruma::events::secret::request::SecretName;
|
||||
///
|
||||
/// let secret_store = client
|
||||
/// .encryption()
|
||||
/// .secret_storage()
|
||||
/// .open_secret_store("It's a secret to everybody")
|
||||
/// .await?;
|
||||
///
|
||||
/// let my_secret = "Top secret secret";
|
||||
/// let my_secret_name = "m.treasure";
|
||||
///
|
||||
/// secret_store.put_secret(my_secret_name, my_secret);
|
||||
///
|
||||
/// # anyhow::Ok(()) };
|
||||
/// ```
|
||||
pub async fn open_secret_store(&self, secret_storage_key: &str) -> Result<SecretStore> {
|
||||
let maybe_default_key_id = self
|
||||
.client
|
||||
.account()
|
||||
.fetch_account_data(GlobalAccountDataEventType::SecretStorageDefaultKey)
|
||||
.await?;
|
||||
|
||||
if let Some(default_key_id) = maybe_default_key_id {
|
||||
let default_key_id =
|
||||
default_key_id.deserialize_as::<SecretStorageDefaultKeyEventContent>()?;
|
||||
|
||||
let event_type =
|
||||
GlobalAccountDataEventType::SecretStorageKey(default_key_id.key_id.to_owned());
|
||||
let secret_key =
|
||||
self.client.account().fetch_account_data(event_type.to_owned()).await?;
|
||||
|
||||
if let Some(secret_key_content) = secret_key {
|
||||
let event_type = event_type.to_string();
|
||||
let secret_key_content = to_raw_value(&secret_key_content)?;
|
||||
|
||||
let secret_key_content =
|
||||
SecretStorageKeyEventContent::from_parts(&event_type, &secret_key_content)?;
|
||||
|
||||
let key =
|
||||
SecretStorageKey::from_account_data(secret_storage_key, secret_key_content)?;
|
||||
|
||||
Ok(SecretStore { client: self.client.to_owned(), key })
|
||||
} else {
|
||||
Err(SecretStorageError::MissingKeyInfo { key_id: Some(default_key_id.key_id) })
|
||||
}
|
||||
} else {
|
||||
Err(SecretStorageError::MissingKeyInfo { key_id: None })
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`SecretStore`].
|
||||
///
|
||||
/// The [`SecretStore`] will be protected by a randomly generated key, or
|
||||
/// optionally a passphrase can be provided as well.
|
||||
///
|
||||
/// In both cases, whether a passphrase was provided or not, the key to open
|
||||
/// the [`SecretStore`] can be obtained using the
|
||||
/// [`SecretStore::secret_storage_key()`] method.
|
||||
///
|
||||
/// *Note*: This method will set the new secret storage key as the default
|
||||
/// key in the `m.secret_storage.default_key` event. All the known secrets
|
||||
/// will be re-encrypted and uploaded to the homeserver as well. This
|
||||
/// includes the following secrets:
|
||||
///
|
||||
/// - `m.cross_signing.master`: The master cross-signing key.
|
||||
/// - `m.cross_signing.self_signing`: The self-signing cross-signing key.
|
||||
/// - `m.cross_signing.user_signing`: The user-signing cross-signing key.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk::Client;
|
||||
/// # use url::Url;
|
||||
/// # async {
|
||||
/// # let homeserver = Url::parse("http://example.com")?;
|
||||
/// # let client = Client::new(homeserver).await?;
|
||||
/// use ruma::events::secret::request::SecretName;
|
||||
///
|
||||
/// let secret_store = client
|
||||
/// .encryption()
|
||||
/// .secret_storage()
|
||||
/// .create_secret_store()
|
||||
/// .await?;
|
||||
///
|
||||
/// let my_secret = "Top secret secret";
|
||||
/// let my_secret_name = SecretName::from("m.treasure");
|
||||
///
|
||||
/// secret_store.put_secret(my_secret_name, my_secret);
|
||||
///
|
||||
/// let secret_storage_key = secret_store.secret_storage_key();
|
||||
///
|
||||
/// println!("Your secret storage key is {secret_storage_key}, save it somewhere safe.");
|
||||
///
|
||||
/// # anyhow::Ok(()) };
|
||||
/// ```
|
||||
pub fn create_secret_store(&self) -> CreateStore<'_> {
|
||||
CreateStore { secret_storage: self, passphrase: None }
|
||||
}
|
||||
|
||||
/// Is secret storage set up for this user?
|
||||
pub async fn is_enabled(&self) -> crate::Result<bool> {
|
||||
if let Some(content) = self
|
||||
.client
|
||||
.account()
|
||||
.fetch_account_data(GlobalAccountDataEventType::SecretStorageDefaultKey)
|
||||
.await?
|
||||
{
|
||||
// Since we can't delete account data events, we're going to treat
|
||||
// deserialization failures as secret storage being disabled.
|
||||
Ok(content.deserialize_as::<SecretStorageDefaultKeyEventContent>().is_ok())
|
||||
} else {
|
||||
// No account data event found, must be disabled.
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use matrix_sdk_base::crypto::{secret_storage::SecretStorageKey, CrossSigningKeyExport};
|
||||
use ruma::{
|
||||
events::{
|
||||
secret::request::SecretName, secret_storage::secret::SecretEventContent,
|
||||
GlobalAccountDataEventType,
|
||||
},
|
||||
serde::Raw,
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use tracing::{
|
||||
error,
|
||||
field::{debug, display},
|
||||
info, instrument, warn, Span,
|
||||
};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::{DecryptionError, Result};
|
||||
use crate::Client;
|
||||
|
||||
#[cfg_attr(doc, aquamarine::aquamarine)]
|
||||
/// Secure key/value storage for Matrix users.
|
||||
///
|
||||
/// The `SecretStore` struct encapsulates the secret storage mechanism for
|
||||
/// Matrix users, as it is specified in the [Matrix specification].
|
||||
///
|
||||
/// This specialized storage is tied to the user's Matrix account and serves as
|
||||
/// an encrypted key/value store, backed by [account data] residing on the
|
||||
/// homeserver. Any secrets uploaded to the homeserver using the
|
||||
/// [`SecretStore::put_secret()`] method are automatically encrypted by the
|
||||
/// [`SecretStore`].
|
||||
///
|
||||
/// [`SecretStore`] enables you to safely manage and access sensitive
|
||||
/// information while ensuring that it remains protected from unauthorized
|
||||
/// access. It plays a crucial role in maintaining the privacy and security of a
|
||||
/// Matrix user's data.
|
||||
///
|
||||
/// **Data Flow Overview:**
|
||||
/// ```mermaid
|
||||
/// flowchart LR
|
||||
/// subgraph Client
|
||||
/// SecretStore
|
||||
/// end
|
||||
/// subgraph Homeserver
|
||||
/// data[Account Data]
|
||||
/// end
|
||||
/// SecretStore <== Encrypted ==> data
|
||||
/// ```
|
||||
///
|
||||
/// **Note**: It's important to emphasize that the `SecretStore` should not be
|
||||
/// used for storing large volumes of data due to its nature as a key/value
|
||||
/// store for sensitive information.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk::Client;
|
||||
/// # use url::Url;
|
||||
/// # async {
|
||||
/// # let homeserver = Url::parse("http://example.com")?;
|
||||
/// # let client = Client::new(homeserver).await?;
|
||||
/// use ruma::events::secret::request::SecretName;
|
||||
///
|
||||
/// let secret_store = client
|
||||
/// .encryption()
|
||||
/// .secret_storage()
|
||||
/// .open_secret_store("It's a secret to everybody")
|
||||
/// .await?;
|
||||
///
|
||||
/// let my_secret = "Top secret secret";
|
||||
/// let my_secret_name = SecretName::from("m.treasure");
|
||||
///
|
||||
/// secret_store.put_secret(my_secret_name, my_secret);
|
||||
///
|
||||
/// # anyhow::Ok(()) };
|
||||
/// ```
|
||||
///
|
||||
/// [Matrix specification]: https://spec.matrix.org/v1.8/client-server-api/#secret-storage
|
||||
/// [account data]: https://spec.matrix.org/v1.8/client-server-api/#client-config
|
||||
pub struct SecretStore {
|
||||
pub(super) client: Client,
|
||||
pub(super) key: SecretStorageKey,
|
||||
}
|
||||
|
||||
impl SecretStore {
|
||||
/// Export the [`SecretStorageKey`] of this [`SecretStore`] as a
|
||||
/// base58-encoded string as defined in the [spec].
|
||||
///
|
||||
/// *Note*: This returns a copy of the private key material of the
|
||||
/// [`SecretStorageKey`] as a string. The caller needs to ensure that this
|
||||
/// string is zeroized.
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/v1.8/client-server-api/#key-representation
|
||||
pub fn secret_storage_key(&self) -> String {
|
||||
self.key.to_base58()
|
||||
}
|
||||
|
||||
/// Retrieve a secret from the homeserver's account data
|
||||
///
|
||||
/// This method allows you to retrieve a secret from the account data stored
|
||||
/// on the Matrix homeserver.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `secret_name`: The name of the secret. The provided `secret_name`
|
||||
/// serves as the event type for the associated account data event.
|
||||
///
|
||||
/// The `retrieve_secret` method enables you to access and decrypt secrets
|
||||
/// previously stored in the user's account data on the homeserver. You can
|
||||
/// use the `secret_name` parameter to specify the desired secret to
|
||||
/// retrieve.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk::Client;
|
||||
/// # use url::Url;
|
||||
/// # async {
|
||||
/// # let homeserver = Url::parse("http://example.com")?;
|
||||
/// # let client = Client::new(homeserver).await?;
|
||||
/// use ruma::events::secret::request::SecretName;
|
||||
///
|
||||
/// let secret_store = client
|
||||
/// .encryption()
|
||||
/// .secret_storage()
|
||||
/// .open_secret_store("It's a secret to everybody")
|
||||
/// .await?;
|
||||
///
|
||||
/// let my_secret_name = SecretName::from("m.treasure");
|
||||
///
|
||||
/// let secret = secret_store.get_secret(my_secret_name).await?;
|
||||
///
|
||||
/// # anyhow::Ok(()) };
|
||||
/// ```
|
||||
pub async fn get_secret(&self, secret_name: impl Into<SecretName>) -> Result<Option<String>> {
|
||||
let secret_name = secret_name.into();
|
||||
let event_type = GlobalAccountDataEventType::from(secret_name.to_owned());
|
||||
|
||||
if let Some(secret_content) = self.client.account().fetch_account_data(event_type).await? {
|
||||
let mut secret_content = secret_content.deserialize_as::<SecretEventContent>()?;
|
||||
|
||||
// The `SecretEventContent` contains a map from the secret storage key ID to the
|
||||
// ciphertext. Let's try to find a secret which was encrypted using our
|
||||
// [`SecretStorageKey`].
|
||||
if let Some(secret_content) = secret_content.encrypted.remove(self.key.key_id()) {
|
||||
// We found a secret we should be able to decrypt, let's try to do so.
|
||||
let decrypted = self
|
||||
.key
|
||||
.decrypt(&secret_content.try_into()?, &secret_name)
|
||||
.map_err(DecryptionError::from)?;
|
||||
|
||||
let secret = String::from_utf8(decrypted).map_err(DecryptionError::from)?;
|
||||
|
||||
Ok(Some(secret))
|
||||
} else {
|
||||
// We did not find a secret which was encrypted using our [`SecretStorageKey`],
|
||||
// no need to try to decrypt.
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Store a secret in the homeserver's account data
|
||||
///
|
||||
/// This method allows you to securely store a secret on the Matrix
|
||||
/// homeserver as an encrypted account data event.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `secret_name`: The name of the secret. The provided `secret_name`
|
||||
/// serves as the event type for the account data event on the homeserver.
|
||||
///
|
||||
/// - `secret`: The secret to be stored on the homeserver. The secret is
|
||||
/// encrypted before being stored, ensuring its confidentiality and
|
||||
/// integrity.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk::Client;
|
||||
/// # use url::Url;
|
||||
/// # async {
|
||||
/// # let homeserver = Url::parse("http://example.com")?;
|
||||
/// # let client = Client::new(homeserver).await?;
|
||||
/// use ruma::events::secret::request::SecretName;
|
||||
///
|
||||
/// let secret_store = client
|
||||
/// .encryption()
|
||||
/// .secret_storage()
|
||||
/// .open_secret_store("It's a secret to everybody")
|
||||
/// .await?;
|
||||
///
|
||||
/// let my_secret = "Top secret secret";
|
||||
/// let my_secret_name = SecretName::from("m.treasure");
|
||||
///
|
||||
/// secret_store.put_secret(my_secret_name, my_secret);
|
||||
///
|
||||
/// # anyhow::Ok(()) };
|
||||
/// ```
|
||||
pub async fn put_secret(&self, secret_name: impl Into<SecretName>, secret: &str) -> Result<()> {
|
||||
// This function does a read/update/store of an account data event stored on the
|
||||
// homeserver. We first fetch the existing account data event, the event
|
||||
// contains a map which gets updated by this method, finally we upload the
|
||||
// modified event.
|
||||
//
|
||||
// To prevent multiple calls to this method trying to update a secret at the
|
||||
// same time, and thus trampling on each other we introduce a lock which
|
||||
// acts as a semaphore.
|
||||
//
|
||||
// Technically there's a low chance of this happening since we're not storing
|
||||
// many secrets and the bigger problem is that another client might be
|
||||
// doing this as well and the server doesn't have a mechanism to protect against
|
||||
// this.
|
||||
//
|
||||
// We could make this lock be per `secret_name` but this is not a performance
|
||||
// critical method.
|
||||
let _guard = self.client.locks().store_secret_lock.lock().await;
|
||||
|
||||
let secret_name = secret_name.into();
|
||||
let event_type = GlobalAccountDataEventType::from(secret_name.to_owned());
|
||||
|
||||
// Get the existing account data event or create a new empty one.
|
||||
let mut secret_content = if let Some(secret_content) =
|
||||
self.client.account().fetch_account_data(event_type.to_owned()).await?
|
||||
{
|
||||
secret_content
|
||||
.deserialize_as::<SecretEventContent>()
|
||||
.unwrap_or_else(|_| SecretEventContent::new(Default::default()))
|
||||
} else {
|
||||
SecretEventContent::new(Default::default())
|
||||
};
|
||||
|
||||
// Encrypt the secret.
|
||||
let secret = secret.as_bytes().to_vec();
|
||||
let encrypted_secret = self.key.encrypt(secret, &secret_name);
|
||||
|
||||
// Insert the encrypted secret into the account data event.
|
||||
secret_content.encrypted.insert(self.key.key_id().to_owned(), encrypted_secret.into());
|
||||
let secret_content = Raw::from_json(to_raw_value(&secret_content)?);
|
||||
|
||||
// Upload the modified account data event, now that the new secret has been
|
||||
// inserted.
|
||||
self.client.account().set_account_data_raw(event_type, secret_content).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all the well-known private parts/keys of the [`OwnUserIdentity`] as
|
||||
/// a [`CrossSigningKeyExport`].
|
||||
///
|
||||
/// The export can be imported into the [`OlmMachine`] using
|
||||
/// [`OlmMachine::import_cross_signing_keys()`].
|
||||
async fn get_cross_signing_keys(&self) -> Result<CrossSigningKeyExport> {
|
||||
let mut export = CrossSigningKeyExport::default();
|
||||
|
||||
export.master_key = self.get_secret(SecretName::CrossSigningMasterKey).await?;
|
||||
export.self_signing_key = self.get_secret(SecretName::CrossSigningSelfSigningKey).await?;
|
||||
export.user_signing_key = self.get_secret(SecretName::CrossSigningUserSigningKey).await?;
|
||||
|
||||
Ok(export)
|
||||
}
|
||||
|
||||
async fn put_cross_signing_keys(&self, export: CrossSigningKeyExport) -> Result<()> {
|
||||
if let Some(master_key) = &export.master_key {
|
||||
self.put_secret(SecretName::CrossSigningMasterKey, master_key).await?;
|
||||
}
|
||||
|
||||
if let Some(user_signing_key) = &export.user_signing_key {
|
||||
self.put_secret(SecretName::CrossSigningUserSigningKey, user_signing_key).await?;
|
||||
}
|
||||
|
||||
if let Some(self_signing_key) = &export.self_signing_key {
|
||||
self.put_secret(SecretName::CrossSigningSelfSigningKey, self_signing_key).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn maybe_enable_backups(&self) -> Result<()> {
|
||||
if let Some(mut secret) = self.get_secret(SecretName::RecoveryKey).await? {
|
||||
let ret = self.client.encryption().backups().maybe_enable_backups(&secret).await;
|
||||
|
||||
if let Err(e) = &ret {
|
||||
warn!("Could not enable backups from secret storage: {e:?}");
|
||||
}
|
||||
|
||||
secret.zeroize();
|
||||
|
||||
Ok(ret.map(|_| ())?)
|
||||
} else {
|
||||
info!("No backup recovery key found.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve and store well-known secrets locally
|
||||
///
|
||||
/// This method retrieves and stores all well-known secrets from the account
|
||||
/// data on the Matrix homeserver to enhance local security and identity
|
||||
/// verification.
|
||||
///
|
||||
/// The following secrets are retrieved by this method:
|
||||
///
|
||||
/// - `m.cross_signing.master`: The master cross-signing key.
|
||||
/// - `m.cross_signing.self_signing`: The self-signing cross-signing key.
|
||||
/// - `m.cross_signing.user_signing`: The user-signing cross-signing key.
|
||||
/// - `m.megolm_backup.v1`: The backup recovery key.
|
||||
///
|
||||
/// If the `m.cross_signing.self_signing` key is successfully imported, it
|
||||
/// is used to sign our own [`Device`], marking it as verified. This step is
|
||||
/// establishes trust in your own device's identity.
|
||||
///
|
||||
/// By invoking this method, you ensure that your device has access to
|
||||
/// the necessary secrets for device and identity verification.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk::Client;
|
||||
/// # use url::Url;
|
||||
/// # async {
|
||||
/// # let homeserver = Url::parse("http://example.com")?;
|
||||
/// # let client = Client::new(homeserver).await?;
|
||||
/// use ruma::events::secret::request::SecretName;
|
||||
///
|
||||
/// let secret_store = client
|
||||
/// .encryption()
|
||||
/// .secret_storage()
|
||||
/// .open_secret_store("It's a secret to everybody")
|
||||
/// .await?;
|
||||
///
|
||||
/// secret_store.import_secrets().await?;
|
||||
///
|
||||
/// let status = client
|
||||
/// .encryption()
|
||||
/// .cross_signing_status()
|
||||
/// .await
|
||||
/// .expect("We should be able to check out cross-signing status");
|
||||
///
|
||||
/// println!("Cross-signing status {status:?}");
|
||||
///
|
||||
/// # anyhow::Ok(()) };
|
||||
/// ```
|
||||
///
|
||||
/// [`Device`]: crate::encryption::identities::Device
|
||||
#[instrument(fields(user_id, device_id, cross_signing_status))]
|
||||
pub async fn import_secrets(&self) -> Result<()> {
|
||||
let olm_machine = self.client.olm_machine().await;
|
||||
let olm_machine = olm_machine.as_ref().ok_or(crate::Error::NoOlmMachine)?;
|
||||
|
||||
Span::current()
|
||||
.record("user_id", display(olm_machine.user_id()))
|
||||
.record("device_id", display(olm_machine.device_id()));
|
||||
|
||||
info!("Fetching the private cross-signing keys from the secret store");
|
||||
|
||||
// Get all our private cross-signing keys from the secret store.
|
||||
let export = self.get_cross_signing_keys().await?;
|
||||
|
||||
info!(cross_signing_keys = ?export, "Received the cross signing keys from the server");
|
||||
|
||||
// We need to ensure that we have the public parts of the cross-signing keys,
|
||||
// those are represented as the `OwnUserIdentity` struct. The public
|
||||
// parts from the server are compared to the public parts re-derived from the
|
||||
// private parts. We will only import the private parts of the cross-signing
|
||||
// keys if they match to the public parts, otherwise we would risk
|
||||
// importing some stale cross-signing keys leftover in the secret store.
|
||||
let (request_id, request) = olm_machine.query_keys_for_users([olm_machine.user_id()]);
|
||||
self.client.keys_query(&request_id, request.device_keys).await?;
|
||||
|
||||
// Let's now try to import our private cross-signing keys.
|
||||
let status = olm_machine.import_cross_signing_keys(export).await?;
|
||||
|
||||
Span::current().record("cross_signing_status", debug(&status));
|
||||
|
||||
info!("Done importing the cross signing keys");
|
||||
|
||||
if status.has_self_signing {
|
||||
info!("Successfully imported the self-signing key, attempting to sign our own device");
|
||||
|
||||
// Now that we successfully imported them, the self-signing key can be used to
|
||||
// verify our own device so other devices and user identities trust
|
||||
// it if the trust our user identity.
|
||||
if let Some(own_device) = self.client.encryption().get_own_device().await? {
|
||||
own_device.verify().await?;
|
||||
|
||||
// Another /keys/query request to ensure that the signatures we uploaded using
|
||||
// `own_device.verify()` are attached to the `Device` we have in storage.
|
||||
let (request_id, request) =
|
||||
olm_machine.query_keys_for_users([olm_machine.user_id()]);
|
||||
self.client.keys_query(&request_id, request.device_keys).await?;
|
||||
|
||||
info!("Successfully signed our own device, the device is now verified");
|
||||
} else {
|
||||
error!("Couldn't find our own device in the store");
|
||||
}
|
||||
}
|
||||
|
||||
self.maybe_enable_backups().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn export_secrets(&self) -> Result<()> {
|
||||
let olm_machine = self.client.olm_machine().await;
|
||||
let olm_machine = olm_machine.as_ref().ok_or(crate::Error::NoOlmMachine)?;
|
||||
|
||||
if let Some(cross_signing_keys) = olm_machine.export_cross_signing_keys().await? {
|
||||
self.put_cross_signing_keys(cross_signing_keys).await?;
|
||||
}
|
||||
|
||||
let backup_keys = olm_machine.backup_machine().get_backup_keys().await?;
|
||||
|
||||
if let Some(backup_recovery_key) = backup_keys.decryption_key {
|
||||
let mut key = backup_recovery_key.to_base64();
|
||||
self.put_secret(SecretName::RecoveryKey, &key).await?;
|
||||
|
||||
key.zeroize();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for SecretStore {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("SecretStore").field("key", &self.key).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
@@ -825,7 +825,10 @@ impl MatrixAuth {
|
||||
|
||||
async fn set_session(&self, session: MatrixSession) -> Result<()> {
|
||||
self.set_session_tokens(session.tokens);
|
||||
self.client.base_client().set_session_meta(session.meta).await?;
|
||||
self.client.set_session_meta(session.meta).await?;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
self.client.encryption().enable_backups_and_recovery().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -712,7 +712,7 @@ impl Oidc {
|
||||
authorization_data: Default::default(),
|
||||
};
|
||||
|
||||
self.client.base_client().set_session_meta(meta).await?;
|
||||
self.client.set_session_meta(meta).await?;
|
||||
self.deferred_enable_cross_process_refresh_lock().await?;
|
||||
|
||||
self.client
|
||||
@@ -755,6 +755,8 @@ impl Oidc {
|
||||
}
|
||||
}
|
||||
|
||||
self.client.encryption().enable_backups_and_recovery().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -911,7 +913,7 @@ impl Oidc {
|
||||
device_id: whoami_res.device_id.ok_or(OidcError::MissingDeviceId)?,
|
||||
};
|
||||
|
||||
self.client.base_client().set_session_meta(session).await.map_err(crate::Error::from)?;
|
||||
self.client.set_session_meta(session).await.map_err(crate::Error::from)?;
|
||||
// At this point the Olm machine has been set up.
|
||||
|
||||
// Enable the cross-process lock for refreshes, if needs be.
|
||||
@@ -952,6 +954,8 @@ impl Oidc {
|
||||
}
|
||||
}
|
||||
|
||||
self.client.encryption().enable_backups_and_recovery().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
mod backups;
|
||||
mod secret_storage;
|
||||
mod verification;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,664 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk::{
|
||||
encryption::secret_storage::SecretStorageError,
|
||||
matrix_auth::{MatrixSession, MatrixSessionTokens},
|
||||
};
|
||||
use matrix_sdk_base::SessionMeta;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
device_id,
|
||||
events::{
|
||||
secret::request::SecretName,
|
||||
secret_storage::{
|
||||
default_key::SecretStorageDefaultKeyEventContent, secret::SecretEventContent,
|
||||
},
|
||||
},
|
||||
user_id, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
use wiremock::{
|
||||
matchers::{header, method, path, path_regex},
|
||||
Mock, MockServer, ResponseTemplate,
|
||||
};
|
||||
|
||||
use crate::{logged_in_client, no_retry_test_client};
|
||||
|
||||
const SECRET_STORE_KEY: &str = "EsTj 3yST y93F SLpB jJsz eAXc 2XzA ygD3 w69H fGaN TKBj jXEd";
|
||||
|
||||
async fn mock_secret_store_key(
|
||||
server: &MockServer,
|
||||
user_id: &UserId,
|
||||
key_id: &str,
|
||||
iv: &str,
|
||||
mac: &str,
|
||||
) {
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key"
|
||||
)))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"key": key_id,
|
||||
})))
|
||||
.expect(1..)
|
||||
.named("default_key account data GET")
|
||||
.mount(server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.key.{key_id}"
|
||||
)))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
|
||||
"iv": iv,
|
||||
"mac": mac,
|
||||
})))
|
||||
.expect(1..)
|
||||
.named("m.direct account data GET")
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn secret_store_create_default_key() {
|
||||
let (client, server) = logged_in_client().await;
|
||||
|
||||
let user_id = client.user_id().expect("We should know our user ID by now");
|
||||
|
||||
let key_id: Arc<Mutex<Option<String>>> = Mutex::new(None).into();
|
||||
|
||||
let put_new_key_matcher = {
|
||||
let key_id = key_id.to_owned();
|
||||
|
||||
move |request: &wiremock::Request| {
|
||||
let path_segments =
|
||||
request.url.path_segments().expect("The URL should be able to be a base");
|
||||
|
||||
let key_id_segment = path_segments
|
||||
.last()
|
||||
.expect("The path should have a key ID as the last segment")
|
||||
.to_owned();
|
||||
|
||||
*key_id.lock().unwrap() = Some(key_id_segment);
|
||||
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
let put_new_key_id_matcher = move |request: &wiremock::Request| {
|
||||
let key_id = key_id.lock().unwrap().take().expect("We should know our new key ID by now");
|
||||
|
||||
let content: SecretStorageDefaultKeyEventContent =
|
||||
request.body_json().expect("The content should be a default key event content");
|
||||
|
||||
assert_eq!(
|
||||
key_id,
|
||||
format!("m.secret_storage.key.{}", content.key_id),
|
||||
"The key ID of the key we created should be the same as the key ID we're marking as the default one"
|
||||
);
|
||||
|
||||
true
|
||||
};
|
||||
|
||||
Mock::given(method("PUT"))
|
||||
.and(path_regex(format!(
|
||||
r"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.key.[A-Za-z0-9]"
|
||||
)))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.and(put_new_key_matcher)
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
|
||||
.expect(1)
|
||||
.named("m.direct account data GET")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("PUT"))
|
||||
.and(path(format!(
|
||||
"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key"
|
||||
)))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.and(put_new_key_id_matcher)
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
|
||||
.expect(1)
|
||||
.named("default_key account data GET")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let _ = client
|
||||
.encryption()
|
||||
.secret_storage()
|
||||
.create_secret_store()
|
||||
.await
|
||||
.expect("We should be able to create a new secret store");
|
||||
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn secret_store_missing_key_info() {
|
||||
let (client, server) = logged_in_client().await;
|
||||
|
||||
let user_id = client.user_id().expect("We should know our user ID by now");
|
||||
let key_id = "bmur2d9ypPUH1msSwCxQOJkuKRmJI55e";
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key"
|
||||
)))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"key": key_id
|
||||
})))
|
||||
.expect(1)
|
||||
.named("default_key account data GET")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.key.{key_id}"
|
||||
)))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(404).set_body_json(json!({
|
||||
"errcode": "M_NOT_FOUND",
|
||||
"error": "Account data not found"
|
||||
})))
|
||||
.expect(1)
|
||||
.named("m.direct account data GET")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let ret = client.encryption().secret_storage().open_secret_store(SECRET_STORE_KEY).await;
|
||||
|
||||
let found_key_id = assert_matches!(
|
||||
ret,
|
||||
Err(SecretStorageError::MissingKeyInfo { key_id: Some(key_id) }) => key_id,
|
||||
"We should report that the key info for the default key is missing"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
key_id, found_key_id,
|
||||
"The key ID in the error should match to the key ID of the reported default key"
|
||||
);
|
||||
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn secret_store_not_setup() {
|
||||
let (client, server) = logged_in_client().await;
|
||||
|
||||
let user_id = client.user_id().expect("We should know our user ID by now");
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key"
|
||||
)))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(404).set_body_json(json!({
|
||||
"errcode": "M_NOT_FOUND",
|
||||
"error": "Account data not found"
|
||||
})))
|
||||
.expect(1)
|
||||
.named("default_key account data GET")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let ret = client.encryption().secret_storage().open_secret_store(SECRET_STORE_KEY).await;
|
||||
|
||||
assert_matches!(
|
||||
ret,
|
||||
Err(SecretStorageError::MissingKeyInfo { key_id: None }),
|
||||
"We should report that the key info for the default key is missing"
|
||||
);
|
||||
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn secret_store_opening() {
|
||||
let (client, server) = logged_in_client().await;
|
||||
|
||||
mock_secret_store_key(
|
||||
&server,
|
||||
client.user_id().unwrap(),
|
||||
"bmur2d9ypPUH1msSwCxQOJkuKRmJI55e",
|
||||
"xv5b6/p3ExEw++wTyfSHEg==",
|
||||
"ujBBbXahnTAMkmPUX2/0+VTfUh63pGyVRuBcDMgmJC8=",
|
||||
)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("_matrix/client/r0/user/@example:localhost/account_data/m.cross_signing.master"))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"encrypted": {
|
||||
"bmur2d9ypPUH1msSwCxQOJkuKRmJI55e": {
|
||||
"ciphertext": "lCRSSA1lChONEXj/8RyogsgAa8ouQwYDnLr4XBCheRikrZykLRzPCx3doCE=",
|
||||
"iv": "bdfCwu+ECYgZ/jWTkGrQ/A==",
|
||||
"mac": "NXeV1dZaOe2JLvQ6Hh6tFto7AgFFdaQnY0l9pruwdtE="
|
||||
}
|
||||
}
|
||||
})))
|
||||
.expect(1..)
|
||||
.named("m.direct account data GET")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let secret_store = client
|
||||
.encryption()
|
||||
.secret_storage()
|
||||
.open_secret_store(SECRET_STORE_KEY)
|
||||
.await
|
||||
.expect("We should be able to open our secret store");
|
||||
|
||||
let secret = secret_store
|
||||
.get_secret(SecretName::CrossSigningMasterKey)
|
||||
.await
|
||||
.expect("We should be able to retrieve a secret from the secret store")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(secret, "VcY1+LNyV6aSMne8mvi4lc/q+JiCHe+m6+hKLtgLP30=");
|
||||
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn set_in_secret_store() {
|
||||
let (client, server) = logged_in_client().await;
|
||||
|
||||
mock_secret_store_key(
|
||||
&server,
|
||||
client.user_id().unwrap(),
|
||||
"bmur2d9ypPUH1msSwCxQOJkuKRmJI55e",
|
||||
"xv5b6/p3ExEw++wTyfSHEg==",
|
||||
"ujBBbXahnTAMkmPUX2/0+VTfUh63pGyVRuBcDMgmJC8=",
|
||||
)
|
||||
.await;
|
||||
|
||||
let secret_store = client
|
||||
.encryption()
|
||||
.secret_storage()
|
||||
.open_secret_store(SECRET_STORE_KEY)
|
||||
.await
|
||||
.expect("We should be able to open our secret store");
|
||||
|
||||
let uploaded_content: Arc<Mutex<Option<SecretEventContent>>> = Mutex::new(None).into();
|
||||
|
||||
{
|
||||
// This mock is scoped because, at first we don't have a `foo` event in our
|
||||
// account data. Only when we call `secret_store.set_secret()` will we
|
||||
// have one, and a different mock will be required for the next GET request.
|
||||
let _guard = Mock::given(method("GET"))
|
||||
.and(path("_matrix/client/r0/user/@example:localhost/account_data/foo"))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(404).set_body_json(json!({
|
||||
"errcode": "M_NOT_FOUND",
|
||||
"error": "Account data not found"
|
||||
})))
|
||||
.expect(1)
|
||||
.named("foo account data GET")
|
||||
.mount_as_scoped(&server)
|
||||
.await;
|
||||
|
||||
// Create a custom matcher to extract the body so we can put it into the
|
||||
// response for the next GET /account_data request.
|
||||
let put_matcher = {
|
||||
let uploaded_content = uploaded_content.to_owned();
|
||||
|
||||
move |request: &wiremock::Request| {
|
||||
let content: SecretEventContent =
|
||||
request.body_json().expect("The request body should be a SecretEventContent");
|
||||
|
||||
*uploaded_content.lock().unwrap() = Some(content);
|
||||
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
Mock::given(method("PUT"))
|
||||
.and(path("_matrix/client/r0/user/@example:localhost/account_data/foo"))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.and(put_matcher)
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
|
||||
.expect(1)
|
||||
.named("foo account data PUT")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
secret_store
|
||||
.put_secret("foo", "It's a secret to everybody")
|
||||
.await
|
||||
.expect("We should be able to store a secret to the secret store");
|
||||
}
|
||||
|
||||
let uploaded_content = uploaded_content
|
||||
.lock()
|
||||
.unwrap()
|
||||
.take()
|
||||
.expect("The secret content should have been uploaded");
|
||||
|
||||
let uploaded_content = serde_json::to_value(uploaded_content).unwrap();
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("_matrix/client/r0/user/@example:localhost/account_data/foo"))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(uploaded_content))
|
||||
.expect(1)
|
||||
.named("foo account data GET")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let secret = secret_store
|
||||
.get_secret("foo")
|
||||
.await
|
||||
.expect("We should be able to retrieve a secret from the secret store")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(secret, "It's a secret to everybody");
|
||||
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn restore_cross_signing_from_secret_store() {
|
||||
let user_id = user_id!("@example:morpheus.localhost");
|
||||
|
||||
let session = MatrixSession {
|
||||
meta: SessionMeta {
|
||||
user_id: user_id!("@example:morpheus.localhost").to_owned(),
|
||||
device_id: device_id!("DEVICEID").to_owned(),
|
||||
},
|
||||
tokens: MatrixSessionTokens { access_token: "1234".to_owned(), refresh_token: None },
|
||||
};
|
||||
let (client, server) = no_retry_test_client().await;
|
||||
client.restore_session(session).await.unwrap();
|
||||
|
||||
mock_secret_store_key(
|
||||
&server,
|
||||
user_id,
|
||||
"bmur2d9ypPUH1msSwCxQOJkuKRmJI55e",
|
||||
"xv5b6/p3ExEw++wTyfSHEg==",
|
||||
"ujBBbXahnTAMkmPUX2/0+VTfUh63pGyVRuBcDMgmJC8=",
|
||||
)
|
||||
.await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("_matrix/client/r0/keys/query"))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"master_keys": {
|
||||
"@example:morpheus.localhost": {
|
||||
"keys": {
|
||||
"ed25519:fKJdf5b3ga1hshUT5obkBteMNTtLkfy8qh4h5/XLNew": "fKJdf5b3ga1hshUT5obkBteMNTtLkfy8qh4h5/XLNew"
|
||||
},
|
||||
"signatures": {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:QLEYYETKXR": "2UCY7IS+5NFdzGGPgyovI2+uHM13pbrsAJIUp0daUCzJdl6hvp5rK/6L5AgyxiVAhbK/XT4lDDJeSymeJAs7Cg"
|
||||
}
|
||||
},
|
||||
"usage": [
|
||||
"master"
|
||||
],
|
||||
"user_id": "@example:morpheus.localhost"
|
||||
}
|
||||
},
|
||||
"self_signing_keys": {
|
||||
"@example:morpheus.localhost": {
|
||||
"keys": {
|
||||
"ed25519:vsEoa0HHxc8hmlWgoohTQBGWcCRh1BFoTzI8HNIxg5Q": "vsEoa0HHxc8hmlWgoohTQBGWcCRh1BFoTzI8HNIxg5Q"
|
||||
},
|
||||
"signatures": {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:fKJdf5b3ga1hshUT5obkBteMNTtLkfy8qh4h5/XLNew": "YFvSUX40E81EO8jibgn+kklMFkqfGTDPcENsq6UEepMY9UUOp9iredNCA/NR2INlfq4gkQhMJk2QRGZ31pQZCg"
|
||||
}
|
||||
},
|
||||
"usage": [
|
||||
"self_signing"
|
||||
],
|
||||
"user_id": "@example:morpheus.localhost"
|
||||
}
|
||||
},
|
||||
"user_signing_keys": {
|
||||
"@example:morpheus.localhost": {
|
||||
"keys": {
|
||||
"ed25519:IGfFj6KhzqBWxiMm6/e+Pu/x/5NV6a7iEe8AKhuRiq0": "IGfFj6KhzqBWxiMm6/e+Pu/x/5NV6a7iEe8AKhuRiq0"
|
||||
},
|
||||
"signatures": {
|
||||
"@example:morpheus.localhost": {
|
||||
"ed25519:fKJdf5b3ga1hshUT5obkBteMNTtLkfy8qh4h5/XLNew": "+b9IdTCC7WezR/XJX+K/wI7DukcTOfBukyy9iyZJP3SFZCNWVRmMG1Kuc8iQg8txSNY5NP0M3PI/Srv9nHZjDQ"
|
||||
}
|
||||
},
|
||||
"usage": [
|
||||
"user_signing"
|
||||
],
|
||||
"user_id": "@example:morpheus.localhost"
|
||||
}
|
||||
}
|
||||
})))
|
||||
.expect(2)
|
||||
.named("/keys/query POST")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("_matrix/client/r0/user/@example:morpheus.localhost/account_data/m.cross_signing.master"))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"encrypted": {
|
||||
"bmur2d9ypPUH1msSwCxQOJkuKRmJI55e": {
|
||||
"ciphertext": "lCRSSA1lChONEXj/8RyogsgAa8ouQwYDnLr4XBCheRikrZykLRzPCx3doCE=",
|
||||
"iv": "bdfCwu+ECYgZ/jWTkGrQ/A==",
|
||||
"mac": "NXeV1dZaOe2JLvQ6Hh6tFto7AgFFdaQnY0l9pruwdtE="
|
||||
}
|
||||
}
|
||||
})))
|
||||
.expect(1)
|
||||
.named("m.direct account data GET")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(
|
||||
"_matrix/client/r0/user/@example:morpheus.localhost/account_data/m.cross_signing.self_signing",
|
||||
))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"encrypted": {
|
||||
"bmur2d9ypPUH1msSwCxQOJkuKRmJI55e": {
|
||||
"ciphertext": "+B9WD02IvtQ8S4OaquhuYEZAx20xvz0oTN7r2VM9VOBxmlOyi+KkkWOvLAo=",
|
||||
"iv": "3BCaKGCaSMkg1x9WnTqUmw==",
|
||||
"mac": "xQEDxQbPH0bYeZUFC3wYJh0lsLkP2amcFGdaZ3VdfQg="
|
||||
}
|
||||
}
|
||||
})))
|
||||
.expect(1)
|
||||
.named("m.direct account data GET")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(
|
||||
"_matrix/client/r0/user/@example:morpheus.localhost/account_data/m.cross_signing.user_signing",
|
||||
))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"encrypted": {
|
||||
"bmur2d9ypPUH1msSwCxQOJkuKRmJI55e": {
|
||||
"ciphertext": "atqNy5IDzYRkRC+lkKoflwsyHkd0dr4UeoViwJdUzexiq0M8h1i8JMkADNg=",
|
||||
"iv": "bjb1V2n9YmA8j31Z9muMqQ==",
|
||||
"mac": "vusvNuV8Kkq50VxtC78oioofVBurnTTVEhiRyZkfu/4="
|
||||
}
|
||||
}
|
||||
})))
|
||||
.expect(1)
|
||||
.named("m.direct account data GET")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(
|
||||
"_matrix/client/r0/user/@example:morpheus.localhost/account_data/m.megolm_backup.v1",
|
||||
))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(404).set_body_json(json!({
|
||||
"errcode": "M_NOT_FOUND",
|
||||
"error": "Account data not found"
|
||||
})))
|
||||
.expect(1)
|
||||
.named("m.megolm_backup.v1 account data GET")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("_matrix/client/unstable/keys/signatures/upload"))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"failures": {}
|
||||
})))
|
||||
.expect(1)
|
||||
.named("signatures upload POST")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let secret_store = client
|
||||
.encryption()
|
||||
.secret_storage()
|
||||
.open_secret_store(SECRET_STORE_KEY)
|
||||
.await
|
||||
.expect("We should be able to open our secret store");
|
||||
|
||||
let status = client
|
||||
.encryption()
|
||||
.cross_signing_status()
|
||||
.await
|
||||
.expect("We should be able to check our cross-signing status");
|
||||
|
||||
assert!(!status.has_master, "Initially we should not have access to our cross signing key");
|
||||
|
||||
secret_store
|
||||
.import_secrets()
|
||||
.await
|
||||
.expect("We should be able to import all our known secrets from 4S");
|
||||
|
||||
let status = client
|
||||
.encryption()
|
||||
.cross_signing_status()
|
||||
.await
|
||||
.expect("We should be able to check our cross-signing status");
|
||||
|
||||
assert!(
|
||||
status.is_complete(),
|
||||
"We should have access to our cross signing key after the import"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SECRET_STORE_KEY,
|
||||
secret_store.secret_storage_key(),
|
||||
"We should be able to retrieve the secret storage key from the store",
|
||||
);
|
||||
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn is_secret_storage_enabled() {
|
||||
let user_id = user_id!("@example:morpheus.localhost");
|
||||
|
||||
let session = MatrixSession {
|
||||
meta: SessionMeta {
|
||||
user_id: user_id!("@example:morpheus.localhost").to_owned(),
|
||||
device_id: device_id!("DEVICEID").to_owned(),
|
||||
},
|
||||
tokens: MatrixSessionTokens { access_token: "1234".to_owned(), refresh_token: None },
|
||||
};
|
||||
let (client, server) = no_retry_test_client().await;
|
||||
client.restore_session(session).await.unwrap();
|
||||
|
||||
{
|
||||
let _scope = Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key"
|
||||
)))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(404).set_body_json(json!({
|
||||
"errcode": "M_NOT_FOUND",
|
||||
"error": "Account data not found"
|
||||
})))
|
||||
.expect(1)
|
||||
.named("default_key account data GET")
|
||||
.mount_as_scoped(&server)
|
||||
.await;
|
||||
|
||||
let enabled = client
|
||||
.encryption()
|
||||
.secret_storage()
|
||||
.is_enabled()
|
||||
.await
|
||||
.expect("We should be able to check if secret storage is enabled");
|
||||
|
||||
assert!(
|
||||
!enabled,
|
||||
"If we didn't find the default key account data event, we should assume that \
|
||||
secret storage is disabled."
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let _scope = Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key"
|
||||
)))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
|
||||
.expect(1)
|
||||
.named("default_key account data GET")
|
||||
.mount_as_scoped(&server)
|
||||
.await;
|
||||
|
||||
let enabled = client
|
||||
.encryption()
|
||||
.secret_storage()
|
||||
.is_enabled()
|
||||
.await
|
||||
.expect("We should be able to check if secret storage is enabled");
|
||||
|
||||
assert!(
|
||||
!enabled,
|
||||
"If deserialization of the default key account data event failed, we should assume \
|
||||
that secret storage is disabled"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let _scope = Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key"
|
||||
)))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"key": "some_key_id",
|
||||
})))
|
||||
.expect(1)
|
||||
.named("default_key account data GET")
|
||||
.mount_as_scoped(&server)
|
||||
.await;
|
||||
|
||||
let enabled = client
|
||||
.encryption()
|
||||
.secret_storage()
|
||||
.is_enabled()
|
||||
.await
|
||||
.expect("We should be able to check if secret storage is enabled");
|
||||
|
||||
assert!(
|
||||
enabled,
|
||||
"If there is a default key event and deserialization did not fail, we're assuming \
|
||||
that secret storage is enabled"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -439,6 +439,7 @@ async fn test_login_with_cross_signing_bootstrapping() {
|
||||
.server_versions([MatrixVersion::V1_0])
|
||||
.with_encryption_settings(matrix_sdk::encryption::EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
..Default::default()
|
||||
})
|
||||
.request_config(RequestConfig::new().disable_retry())
|
||||
.build()
|
||||
@@ -489,6 +490,7 @@ async fn test_login_with_cross_signing_bootstrapping() {
|
||||
.server_versions([MatrixVersion::V1_0])
|
||||
.with_encryption_settings(matrix_sdk::encryption::EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
..Default::default()
|
||||
})
|
||||
.request_config(RequestConfig::new().disable_retry())
|
||||
.build()
|
||||
@@ -562,6 +564,7 @@ async fn test_login_doesnt_fail_if_cross_signing_bootstrapping_failed() {
|
||||
.server_versions([MatrixVersion::V1_0])
|
||||
.with_encryption_settings(matrix_sdk::encryption::EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
..Default::default()
|
||||
})
|
||||
.request_config(RequestConfig::new().disable_retry())
|
||||
.build()
|
||||
@@ -680,6 +683,7 @@ async fn test_login_with_cross_signing_bootstrapping_already_bootstrapped() {
|
||||
let client = builder
|
||||
.with_encryption_settings(matrix_sdk::encryption::EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
..Default::default()
|
||||
})
|
||||
.request_config(RequestConfig::new().disable_retry())
|
||||
.build()
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "example-backups"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "example-backups"
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
|
||||
clap = { version = "4.0.15", features = ["derive"] }
|
||||
futures-util = "0.3.24"
|
||||
tracing-subscriber = "0.3.16"
|
||||
url = "2.3.1"
|
||||
# when copy-pasting this, please use a git dependency or make sure that you
|
||||
# have copied the example as it was at the time of the release you use.
|
||||
matrix-sdk = { path = "../../crates/matrix-sdk" }
|
||||
@@ -0,0 +1,121 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
encryption::{backups::BackupState, secret_storage::SecretStore},
|
||||
Client,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
/// A command line example showcasing how to resume backups by importing the
|
||||
/// backup key from secret storage.
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
/// The homeserver to connect to.
|
||||
#[clap(value_parser)]
|
||||
homeserver: Url,
|
||||
|
||||
/// The user ID that should be used to restore the session.
|
||||
#[clap(value_parser)]
|
||||
user_name: String,
|
||||
|
||||
/// The password that should be used for the login.
|
||||
#[clap(value_parser)]
|
||||
password: String,
|
||||
|
||||
/// Set the proxy that should be used for the connection.
|
||||
#[clap(short, long)]
|
||||
proxy: Option<Url>,
|
||||
|
||||
/// Enable verbose logging output.
|
||||
#[clap(short, long, action)]
|
||||
verbose: bool,
|
||||
|
||||
/// The secret storage key, this key will be used to open the secret-store.
|
||||
#[clap(long, action)]
|
||||
secret_store_key: String,
|
||||
}
|
||||
|
||||
async fn import_known_secrets(client: &Client, secret_store: SecretStore) -> Result<()> {
|
||||
secret_store.import_secrets().await?;
|
||||
|
||||
let status = client
|
||||
.encryption()
|
||||
.cross_signing_status()
|
||||
.await
|
||||
.expect("We should be able to get our cross-signing status");
|
||||
|
||||
if status.is_complete() {
|
||||
println!("Successfully imported all the cross-signing keys");
|
||||
} else {
|
||||
eprintln!("Couldn't import all the cross-signing keys: {status:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn login(cli: &Cli) -> Result<Client> {
|
||||
let builder = Client::builder().homeserver_url(&cli.homeserver);
|
||||
|
||||
let builder = if let Some(proxy) = &cli.proxy { builder.proxy(proxy) } else { builder };
|
||||
|
||||
let client = builder.build().await?;
|
||||
|
||||
client
|
||||
.matrix_auth()
|
||||
.login_username(&cli.user_name, &cli.password)
|
||||
.initial_device_display_name("rust-sdk")
|
||||
.await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn listen_for_backup_state_changes(client: Client) {
|
||||
let stream = client.encryption().backups().state_stream();
|
||||
pin_mut!(stream);
|
||||
|
||||
while let Some(state) = stream.next().await {
|
||||
let Ok(state) = state else { panic!("Error while receiving backup state updates") };
|
||||
|
||||
match state {
|
||||
BackupState::Unknown => (),
|
||||
BackupState::Enabling => println!("Trying to enable backups"),
|
||||
BackupState::Resuming => println!("Trying to resume backups"),
|
||||
BackupState::Enabled => println!("Backups have been successfully enabled"),
|
||||
BackupState::Downloading => println!("Downloading the room keys from the backup"),
|
||||
BackupState::Disabling => println!("Disabling the backup"),
|
||||
BackupState::Disabled => println!("Backup has been disabled"),
|
||||
BackupState::Creating => println!("Trying to create a new backup"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.verbose {
|
||||
tracing_subscriber::fmt::init();
|
||||
}
|
||||
|
||||
let client = login(&cli).await?;
|
||||
|
||||
client.sync_once(Default::default()).await?;
|
||||
|
||||
let secret_store =
|
||||
client.encryption().secret_storage().open_secret_store(&cli.secret_store_key).await?;
|
||||
|
||||
let _task = tokio::spawn({
|
||||
let client = client.to_owned();
|
||||
async move { listen_for_backup_state_changes(client.to_owned()).await }
|
||||
});
|
||||
|
||||
import_known_secrets(&client, secret_store).await?;
|
||||
|
||||
loop {
|
||||
if let Err(e) = client.sync(SyncSettings::new()).await {
|
||||
eprintln!("Error syncing: {e:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,13 @@ use clap::Parser;
|
||||
use futures_util::stream::StreamExt;
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
encryption::verification::{
|
||||
format_emojis, Emoji, SasState, SasVerification, Verification, VerificationRequest,
|
||||
VerificationRequestState,
|
||||
encryption::{
|
||||
recovery::RecoveryState,
|
||||
verification::{
|
||||
format_emojis, Emoji, SasState, SasVerification, Verification, VerificationRequest,
|
||||
VerificationRequestState,
|
||||
},
|
||||
EncryptionSettings,
|
||||
},
|
||||
ruma::{
|
||||
events::{
|
||||
@@ -143,9 +147,24 @@ async fn sync(client: Client) -> matrix_sdk::Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
client.sync(SyncSettings::new()).await?;
|
||||
loop {
|
||||
if let Err(e) = client.sync(SyncSettings::new()).await {
|
||||
eprintln!("Error syncing, what the fuck is going on with this synapse {e:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
async fn listen_for_recovery_state_changes(client: Client) {
|
||||
let mut stream = client.encryption().recovery().state_stream();
|
||||
|
||||
while let Some(state) = stream.next().await {
|
||||
match state {
|
||||
RecoveryState::Unknown => println!("The recovery state is now unknown"),
|
||||
RecoveryState::Enabled => println!("Recovery is now enabled"),
|
||||
RecoveryState::Disabled => println!("Recovery is now disabled"),
|
||||
RecoveryState::Incomplete => println!("Recovery is now incomplete"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -172,7 +191,15 @@ struct Cli {
|
||||
}
|
||||
|
||||
async fn login(cli: Cli) -> Result<Client> {
|
||||
let builder = Client::builder().homeserver_url(cli.homeserver);
|
||||
let encryption_settings = EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
auto_download_from_backup: true,
|
||||
auto_enable_backups: true,
|
||||
};
|
||||
|
||||
let builder = Client::builder()
|
||||
.homeserver_url(cli.homeserver)
|
||||
.with_encryption_settings(encryption_settings);
|
||||
|
||||
let builder = if let Some(proxy) = cli.proxy { builder.proxy(proxy) } else { builder };
|
||||
|
||||
@@ -197,6 +224,11 @@ async fn main() -> Result<()> {
|
||||
|
||||
let client = login(cli).await?;
|
||||
|
||||
let _recovery_state_task = tokio::spawn({
|
||||
let client = client.to_owned();
|
||||
async move { listen_for_recovery_state_changes(client.to_owned()).await }
|
||||
});
|
||||
|
||||
sync(client).await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "example-encryption-recovery"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "example-encryption-recovery"
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread", "io-std"] }
|
||||
clap = { version = "4.0.15", features = ["derive"] }
|
||||
futures-util = "0.3.24"
|
||||
tracing-subscriber = "0.3.16"
|
||||
url = "2.3.1"
|
||||
zeroize = "1.6.0"
|
||||
# when copy-pasting this, please use a git dependency or make sure that you
|
||||
# have copied the example as it was at the time of the release you use.
|
||||
matrix-sdk = { path = "../../crates/matrix-sdk" }
|
||||
@@ -0,0 +1,312 @@
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
encryption::{backups::BackupState, recovery::RecoveryState, EncryptionSettings},
|
||||
Client,
|
||||
};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use url::Url;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// A command line example showcasing how the encryption recovery works in the
|
||||
/// Matrix Rust SDK.
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
/// The homeserver to connect to.
|
||||
#[clap(value_parser)]
|
||||
homeserver: Url,
|
||||
|
||||
/// The user ID that should be used to restore the session.
|
||||
#[clap(value_parser)]
|
||||
user_name: String,
|
||||
|
||||
/// The password that should be used for the login.
|
||||
#[clap(value_parser)]
|
||||
password: String,
|
||||
|
||||
/// Set the proxy that should be used for the connection.
|
||||
#[clap(short, long)]
|
||||
proxy: Option<Url>,
|
||||
|
||||
/// Enable verbose logging output.
|
||||
#[clap(short, long, action)]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(no_binary_name = true)]
|
||||
enum Command {
|
||||
/// Disable backups and recovery, whatever the later means.
|
||||
Disable,
|
||||
/// Enable backups, don't wait for the backup to be done.
|
||||
EnableBackup,
|
||||
/// Change the recovery key, we generate a new one and present the base58
|
||||
/// string to the user.
|
||||
ChangeRecoveryKey {
|
||||
/// The passphrase, which can be used to recover, in addition to the
|
||||
/// recovery key.
|
||||
#[clap(long, action)]
|
||||
passphrase: Option<String>,
|
||||
},
|
||||
/// Logout, if recovery isn't enabled, ask the user if they want to do so
|
||||
/// now.
|
||||
Logout,
|
||||
/// Logout without setting up recovery, still wait for room keys to be
|
||||
/// uploaded.
|
||||
LogoutNoRecovery,
|
||||
Recover {
|
||||
/// The recovery key, AKA the secret storage key, this key will be used
|
||||
/// to open the secret-store. Not to be confused with the
|
||||
/// Recovery key from the spec.
|
||||
#[clap(long, action)]
|
||||
recovery_key: String,
|
||||
},
|
||||
}
|
||||
|
||||
async fn recover(client: &Client, recovery_key: &str) -> Result<()> {
|
||||
// You see how this name isn't really fitting?
|
||||
client.encryption().recovery().recover(recovery_key).await?;
|
||||
|
||||
let status = client
|
||||
.encryption()
|
||||
.cross_signing_status()
|
||||
.await
|
||||
.expect("We should be able to get our cross-signing status");
|
||||
|
||||
if status.is_complete() {
|
||||
println!("Successfully imported all the cross-signing keys");
|
||||
} else {
|
||||
eprintln!("Couldn't import all the cross-signing keys: {status:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn login(cli: &Cli) -> Result<Client> {
|
||||
let encryption_settings = EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
auto_download_from_backup: true,
|
||||
auto_enable_backups: true,
|
||||
};
|
||||
|
||||
let builder = Client::builder()
|
||||
.homeserver_url(&cli.homeserver)
|
||||
.with_encryption_settings(encryption_settings);
|
||||
|
||||
let builder = if let Some(proxy) = &cli.proxy { builder.proxy(proxy) } else { builder };
|
||||
|
||||
let client = builder.build().await?;
|
||||
|
||||
client
|
||||
.matrix_auth()
|
||||
.login_username(&cli.user_name, &cli.password)
|
||||
.initial_device_display_name("rust-sdk")
|
||||
.await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn listen_for_backup_state_changes(client: Client) {
|
||||
let mut stream = client.encryption().backups().state_stream();
|
||||
|
||||
while let Some(state) = stream.next().await {
|
||||
let Ok(state) = state else {
|
||||
panic!("Receive error while waiting for updates to the backup state")
|
||||
};
|
||||
|
||||
match state {
|
||||
BackupState::Unknown => (),
|
||||
BackupState::Enabling => println!("Trying to enable backups"),
|
||||
BackupState::Resuming => println!("Trying to resume backups"),
|
||||
BackupState::Enabled => println!("Successfully enabled backups"),
|
||||
BackupState::Downloading => println!("Downloading the room keys from the backup"),
|
||||
BackupState::Disabling => println!("Disabling the backup"),
|
||||
BackupState::Disabled => println!("Backup successfully disabled"),
|
||||
BackupState::Creating => println!("Trying to create a new backup"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_for_recovery_state_changes(client: Client) {
|
||||
let mut stream = client.encryption().recovery().state_stream();
|
||||
|
||||
while let Some(state) = stream.next().await {
|
||||
match state {
|
||||
RecoveryState::Unknown => println!("The recovery state is now unknown"),
|
||||
RecoveryState::Enabled => println!("Recovery is now enabled"),
|
||||
RecoveryState::Disabled => println!("Recovery is now disabled"),
|
||||
RecoveryState::Incomplete => println!("Recovery is now incomplete"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn logout(client: &Client) -> Result<()> {
|
||||
let recovery = client.encryption().recovery();
|
||||
|
||||
if recovery.are_we_the_last_man_standing().await? {
|
||||
let enable_recovery = recovery.enable().wait_for_backups_to_upload();
|
||||
|
||||
let mut progress = enable_recovery.subscribe_to_progress();
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
while let Some(update) = progress.next().await {
|
||||
println!("Hello world {update:?}");
|
||||
}
|
||||
});
|
||||
|
||||
let recovery_key = enable_recovery.await?;
|
||||
println!("Successfully created a recovery key: `{recovery_key}`.");
|
||||
|
||||
task.abort();
|
||||
}
|
||||
|
||||
client.logout().await?;
|
||||
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
async fn logout_no_recovery(client: &Client) -> Result<()> {
|
||||
let backups = client.encryption().backups();
|
||||
|
||||
let wait = backups.wait_for_steady_state();
|
||||
let mut progress = wait.subscribe_to_progress();
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
while let Some(update) = progress.next().await {
|
||||
println!("Hello world {update:?}");
|
||||
std::io::stdout().flush().expect("Unable to write to stdout");
|
||||
}
|
||||
});
|
||||
|
||||
println!("Waiting for the room keys to upload.");
|
||||
|
||||
if let Err(e) = wait.await {
|
||||
println!("Error waiting for room keys to upload {e:?}");
|
||||
}
|
||||
|
||||
task.abort();
|
||||
|
||||
client.logout().await?;
|
||||
println!("Successfully logged out. Exiting...");
|
||||
std::io::stdout().flush().expect("Unable to write to stdout");
|
||||
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
async fn enable_backup(client: &Client) -> Result<()> {
|
||||
let recovery = client.encryption().recovery();
|
||||
let enable_backup = recovery.enable_backup();
|
||||
|
||||
enable_backup.await?;
|
||||
|
||||
println!("Successfully enabled backups.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn disable(client: &Client) -> Result<()> {
|
||||
client.encryption().recovery().disable().await?;
|
||||
|
||||
println!("Successfully disable recovery.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reset_key(client: &Client, passphrase: Option<&str>) -> Result<()> {
|
||||
// TODO: Check if we should be able to reset the key and then ask for the
|
||||
// existing one if we're not supposed to.
|
||||
let mut recovery_key = if let Some(passphrase) = passphrase {
|
||||
client.encryption().recovery().reset_key().with_passphrase(passphrase).await?
|
||||
} else {
|
||||
client.encryption().recovery().reset_key().await?
|
||||
};
|
||||
|
||||
println!("Successfully changed the recovery key, new key: `{recovery_key}`.");
|
||||
recovery_key.zeroize();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_command(client: &Client, command: Command) -> Result<()> {
|
||||
// TODO: Convert this into a proper REPL, perhaps even a status at the top would
|
||||
// be nice.
|
||||
match command {
|
||||
Command::Disable => disable(client).await,
|
||||
Command::EnableBackup => enable_backup(client).await,
|
||||
Command::ChangeRecoveryKey { mut passphrase } => {
|
||||
let ret = reset_key(client, passphrase.as_deref()).await;
|
||||
passphrase.zeroize();
|
||||
ret
|
||||
}
|
||||
Command::Recover { mut recovery_key } => {
|
||||
let ret = recover(client, &recovery_key).await;
|
||||
recovery_key.zeroize();
|
||||
|
||||
ret
|
||||
}
|
||||
Command::Logout => logout(client).await,
|
||||
Command::LogoutNoRecovery => logout_no_recovery(client).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_command(client: Client) -> Result<()> {
|
||||
let stdin = tokio::io::stdin();
|
||||
let reader = BufReader::new(stdin);
|
||||
|
||||
let mut lines = reader.lines();
|
||||
|
||||
while let Some(line) = lines.next_line().await? {
|
||||
let line = line.split_whitespace();
|
||||
|
||||
match Command::try_parse_from(line) {
|
||||
Ok(command) => run_command(&client, command).await?,
|
||||
Err(e) => println!("{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn sync(client: Client) {
|
||||
loop {
|
||||
if let Err(e) = client.sync(SyncSettings::new()).await {
|
||||
eprintln!("Error syncing, what the fuck is going on with this synapse {e:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.verbose {
|
||||
tracing_subscriber::fmt::init();
|
||||
}
|
||||
|
||||
let client = login(&cli).await?;
|
||||
|
||||
client.sync_once(Default::default()).await?;
|
||||
|
||||
let _task = tokio::spawn({
|
||||
let client = client.to_owned();
|
||||
async move { listen_for_backup_state_changes(client.to_owned()).await }
|
||||
});
|
||||
|
||||
let _recovery_state_task = tokio::spawn({
|
||||
let client = client.to_owned();
|
||||
async move { listen_for_recovery_state_changes(client.to_owned()).await }
|
||||
});
|
||||
|
||||
let _sync_task = tokio::spawn({
|
||||
let client = client.to_owned();
|
||||
async move { sync(client).await }
|
||||
});
|
||||
|
||||
get_command(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "example-secret-storage"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "example-secret-storage"
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
|
||||
clap = { version = "4.0.15", features = ["derive"] }
|
||||
futures-util = "0.3.24"
|
||||
tracing-subscriber = "0.3.16"
|
||||
url = "2.3.1"
|
||||
# when copy-pasting this, please use a git dependency or make sure that you
|
||||
# have copied the example as it was at the time of the release you use.
|
||||
matrix-sdk = { path = "../../crates/matrix-sdk" }
|
||||
@@ -0,0 +1,148 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use matrix_sdk::{
|
||||
encryption::secret_storage::SecretStore,
|
||||
matrix_auth::{MatrixSession, MatrixSessionTokens},
|
||||
ruma::{events::secret::request::SecretName, OwnedDeviceId, OwnedUserId},
|
||||
AuthSession, Client, SessionMeta,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
/// A command line example showcasing how the secret storage support works in
|
||||
/// the Matrix Rust SDK.
|
||||
///
|
||||
/// Secret storage is an account data backed encrypted key/value store. You can
|
||||
/// put or get secrets from the store.
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
/// The homeserver to connect to.
|
||||
#[clap(value_parser)]
|
||||
homeserver: Url,
|
||||
|
||||
/// The user ID that should be used to restore the session.
|
||||
#[clap(value_parser)]
|
||||
user_id: OwnedUserId,
|
||||
|
||||
/// The user name that should be used for the login.
|
||||
#[clap(value_parser)]
|
||||
device_id: OwnedDeviceId,
|
||||
|
||||
/// The password that should be used for the login.
|
||||
#[clap(value_parser)]
|
||||
access_token: String,
|
||||
|
||||
/// Set the proxy that should be used for the connection.
|
||||
#[clap(short, long)]
|
||||
proxy: Option<Url>,
|
||||
|
||||
/// Enable verbose logging output.
|
||||
#[clap(short, long, action)]
|
||||
verbose: bool,
|
||||
|
||||
/// The secret storage key, this key will be used to open the secret-store.
|
||||
#[clap(long, action)]
|
||||
secret_store_key: String,
|
||||
|
||||
/// The sub-command to run.
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Commands {
|
||||
/// Retrieve a secret from the homeserver.
|
||||
GetSecret { secret_name: SecretName },
|
||||
/// Upload a secret to the homeserver.
|
||||
SetSecret { secret_name: SecretName, secret: String },
|
||||
/// Import all known and specced secrets from the secret store into the
|
||||
/// local database.
|
||||
///
|
||||
/// **Note**: This command won't strictly do the right thing, as we are
|
||||
/// reusing a device ID and access token from a different device. It will
|
||||
/// import the secrets correctly, but it will sign device keys which don't
|
||||
/// belong to the provided device ID.
|
||||
ImportKnownSecrets,
|
||||
}
|
||||
|
||||
async fn get_secret(secret_store: SecretStore, secret_name: SecretName) -> Result<()> {
|
||||
let secret = secret_store.get_secret(secret_name.to_owned()).await?;
|
||||
|
||||
if let Some(secret) = secret {
|
||||
println!("Secret: {secret}");
|
||||
} else {
|
||||
println!("No secret with the name {secret_name} found")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_secret(
|
||||
secret_store: SecretStore,
|
||||
secret_name: SecretName,
|
||||
secret: &str,
|
||||
) -> Result<()> {
|
||||
secret_store.put_secret(secret_name.to_owned(), secret).await?;
|
||||
|
||||
println!("Secret {secret_name} was successfully encrypted and stored on the homeserver");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn import_known_secrets(client: Client, secret_store: SecretStore) -> Result<()> {
|
||||
secret_store.import_secrets().await?;
|
||||
|
||||
let status = client
|
||||
.encryption()
|
||||
.cross_signing_status()
|
||||
.await
|
||||
.expect("We should be able to get our cross-signing status");
|
||||
|
||||
if status.is_complete() {
|
||||
println!("Successfully imported all the cross-signing keys");
|
||||
} else {
|
||||
eprintln!("Couldn't import all the cross-signing keys: {status:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn restore_client(cli: &Cli) -> Result<Client> {
|
||||
let builder = Client::builder().homeserver_url(&cli.homeserver);
|
||||
|
||||
let builder = if let Some(proxy) = cli.proxy.as_ref() { builder.proxy(proxy) } else { builder };
|
||||
let client = builder.build().await?;
|
||||
|
||||
// TODO: We should be able to get the device id from `/whoami`.
|
||||
let session = AuthSession::Matrix(MatrixSession {
|
||||
meta: SessionMeta { user_id: cli.user_id.to_owned(), device_id: cli.device_id.to_owned() },
|
||||
tokens: MatrixSessionTokens {
|
||||
access_token: cli.access_token.to_owned(),
|
||||
refresh_token: None,
|
||||
},
|
||||
});
|
||||
|
||||
client.restore_session(session).await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.verbose {
|
||||
tracing_subscriber::fmt::init();
|
||||
}
|
||||
|
||||
let client = restore_client(&cli).await?;
|
||||
let secret_store =
|
||||
client.encryption().secret_storage().open_secret_store(&cli.secret_store_key).await?;
|
||||
|
||||
match cli.command {
|
||||
Commands::GetSecret { secret_name } => get_secret(secret_store, secret_name).await,
|
||||
Commands::SetSecret { secret_name, secret } => {
|
||||
set_secret(secret_store, secret_name, &secret).await
|
||||
}
|
||||
Commands::ImportKnownSecrets => import_known_secrets(client, secret_store).await,
|
||||
}
|
||||
}
|
||||
+3
-8
@@ -239,15 +239,10 @@ fn run_feature_tests(cmd: Option<FeatureSet>) -> Result<()> {
|
||||
}
|
||||
|
||||
fn run_crypto_tests() -> Result<()> {
|
||||
cmd!(
|
||||
"rustup run stable cargo clippy -p matrix-sdk-crypto --features=backups_v1 -- -D warnings"
|
||||
)
|
||||
.run()?;
|
||||
cmd!("rustup run stable cargo clippy -p matrix-sdk-crypto -- -D warnings").run()?;
|
||||
cmd!("rustup run stable cargo nextest run -p matrix-sdk-crypto --no-default-features --features testing").run()?;
|
||||
cmd!("rustup run stable cargo nextest run -p matrix-sdk-crypto --features=backups_v1,testing")
|
||||
.run()?;
|
||||
cmd!("rustup run stable cargo test --doc -p matrix-sdk-crypto --features=backups_v1,testing")
|
||||
.run()?;
|
||||
cmd!("rustup run stable cargo nextest run -p matrix-sdk-crypto --features=testing").run()?;
|
||||
cmd!("rustup run stable cargo test --doc -p matrix-sdk-crypto --features=testing").run()?;
|
||||
cmd!(
|
||||
"rustup run stable cargo clippy -p matrix-sdk-crypto --features=experimental-algorithms -- -D warnings"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user