From 6d69a19524471c8a280f981da1a2b13530b1ecdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Sun, 22 Oct 2023 19:24:44 +0200 Subject: [PATCH] Add an example for the recovery stuff --- Cargo.lock | 14 + examples/encryption-recovery/Cargo.toml | 21 ++ examples/encryption-recovery/src/main.rs | 312 +++++++++++++++++++++++ 3 files changed, 347 insertions(+) create mode 100644 examples/encryption-recovery/Cargo.toml create mode 100644 examples/encryption-recovery/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index a6051b25d..f4e085632 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1594,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" diff --git a/examples/encryption-recovery/Cargo.toml b/examples/encryption-recovery/Cargo.toml new file mode 100644 index 000000000..bd1def008 --- /dev/null +++ b/examples/encryption-recovery/Cargo.toml @@ -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" } diff --git a/examples/encryption-recovery/src/main.rs b/examples/encryption-recovery/src/main.rs new file mode 100644 index 000000000..9b9719cf6 --- /dev/null +++ b/examples/encryption-recovery/src/main.rs @@ -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, + + /// 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, + }, + /// 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 { + 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(()) +}