Compare commits

...

28 Commits

Author SHA1 Message Date
Benjamin Bouvier f6501e585a crypto: put the enabling of backup and recovery in the background (#2795) 2023-10-31 12:38:50 +01:00
Benjamin Bouvier 0aed05a7d4 crypto: make sure OIDC is properly set up before sending any request (#2794) 2023-10-31 12:38:50 +01:00
Damir Jelić 0f722f6eb8 Display the recovery state in the emoji example 2023-10-31 12:38:50 +01:00
Damir Jelić ea94975adc Add a logout method to the client 2023-10-31 12:38:50 +01:00
Damir Jelić 6d69a19524 Add an example for the recovery stuff 2023-10-31 12:38:50 +01:00
Damir Jelić 73fe6603ab Expose the recovery thing in the bindings 2023-10-31 12:38:50 +01:00
Damir Jelić e6a15831c3 Recovery support 2023-10-31 12:35:44 +01:00
Damir Jelić 2ad554a66a Add an example for the room key backup support 2023-10-31 12:32:19 +01:00
Damir Jelić 1642ac7934 Test that we can decrypt after importing the room keys from backup 2023-10-31 12:32:19 +01:00
Damir Jelić d03277a0bb More tests for the backup support 2023-10-31 12:32:19 +01:00
Damir Jelić 816e3f5ee5 Add a hack so the timeline retries to decrypt if we receive room keys from backup 2023-10-31 12:32:19 +01:00
Damir Jelić caf27caca0 Fetch the backup recovery key when we import all known secrets 2023-10-31 12:32:19 +01:00
Damir Jelić 8453495c69 Add client tasks for background tasks in the client 2023-10-31 12:32:17 +01:00
Damir Jelić 747c00c265 Try to resume backups if we restore the client 2023-10-31 12:29:53 +01:00
Damir Jelić 4fda5e5ff0 Backup support 2023-10-31 12:29:51 +01:00
Damir Jelić 8096dc3275 Add a higher level method to sign backup versions 2023-10-31 12:26:43 +01:00
Damir Jelić ddad613db5 Add a helper to more easily upload info about a new backup decryption key 2023-10-31 12:26:43 +01:00
Damir Jelić ec8dc91b25 Implement PartialEq for the backup decryption key 2023-10-31 12:26:43 +01:00
Damir Jelić cb55d94d00 Add some higher level methods to decrypt backed up room keys 2023-10-31 12:26:43 +01:00
Damir Jelić b263c14e0f Add a method to create an ExportedRoomKey from a BackedUpRoomKey 2023-10-31 12:26:43 +01:00
Damir Jelić 4596372869 Add a getter for the backup auth data to the BackupDecryptionKey 2023-10-31 12:26:43 +01:00
Damir Jelić 6a916b4e52 Use the better Signatures type 2023-10-31 12:26:43 +01:00
Damir Jelić 5ba540a883 Enable backups in the crypto crate by default 2023-10-31 12:26:41 +01:00
Damir Jelić 8d39a1d9b9 Add a backup specific method to import room keys 2023-10-31 12:24:28 +01:00
Damir Jelić 49c6e2cb92 Bump Ruma 2023-10-31 12:24:28 +01:00
Damir Jelić 38e10dbd38 Add an example for the secret storage support 2023-10-31 12:21:13 +01:00
Damir Jelić ee06e1d5ba Add a test for the secret storage support in the main crate 2023-10-31 12:21:13 +01:00
Damir Jelić 16dda959e5 Add secret storage support to the main SDK crate 2023-10-31 12:21:12 +01:00
60 changed files with 6033 additions and 197 deletions
Generated
+87 -20
View File
@@ -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
View File
@@ -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" }
+1 -1
View File
@@ -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))?;
+1 -1
View File
@@ -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 }
+7
View File
@@ -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);
+5
View File
@@ -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,
}
+297
View File
@@ -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?)
}
}
+6
View File
@@ -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)
+2
View File
@@ -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},
+5
View File
@@ -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.
+2 -3
View File
@@ -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)]
+80 -2
View File
@@ -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,
-1
View File
@@ -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;
+23 -98
View File
@@ -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
+108 -6
View File
@@ -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);
}
+32 -1
View File
@@ -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,
}),
};
+2
View File
@@ -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
+4
View File
@@ -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`
+2
View File
@@ -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"
+2 -2
View File
@@ -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");
+55 -7
View File
@@ -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)
+80
View File
@@ -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
+70 -3
View File
@@ -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()
}
}
+4 -1
View File
@@ -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(())
}
+6 -2
View File
@@ -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()
+20
View File
@@ -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" }
+121
View File
@@ -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:?}")
}
}
}
+38 -6
View File
@@ -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(())
+21
View File
@@ -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" }
+312
View File
@@ -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(())
}
+20
View File
@@ -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" }
+148
View File
@@ -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
View File
@@ -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"
)