From 2327658e8cc580ff20a14f16b79a1830e8dcd38c Mon Sep 17 00:00:00 2001 From: Ulyssa Date: Thu, 28 Mar 2024 20:58:34 -0700 Subject: [PATCH] Add commands for importing and exporting room keys (#233) --- docs/iamb.1 | 14 +++++++++++-- src/base.rs | 21 +++++++++++++++++++- src/commands.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 32 ++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 3 deletions(-) diff --git a/docs/iamb.1 b/docs/iamb.1 index cd88e71..cba3044 100644 --- a/docs/iamb.1 +++ b/docs/iamb.1 @@ -61,12 +61,22 @@ Log out of View a list of joined rooms. .It Sy ":spaces" View a list of joined spaces. -.It Sy ":verify" -View a list of ongoing E2EE verifications. .It Sy ":welcome" View the startup Welcome window. .El +.Sh "E2EE COMMANDS" +.Bl -tag -width Ds +.It Sy ":keys export [path] [passphrase]" +Export and encrypt keys to +.Pa path . +.It Sy ":keys import [path] [passphrase]" +Import and decrypt keys from +.Pa path . +.It Sy ":verify" +View a list of ongoing E2EE verifications. +.El + .Sh "MESSAGE COMMANDS" .Bl -tag -width Ds .It Sy ":download" diff --git a/src/base.rs b/src/base.rs index 7cb8137..fef5b11 100644 --- a/src/base.rs +++ b/src/base.rs @@ -420,6 +420,15 @@ pub enum HomeserverAction { Logout(String, bool), } +/// An action performed against the user's room keys. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum KeysAction { + /// Export room keys to a file, encrypted with a passphrase. + Export(String, String), + /// Import room keys from a file, encrypted with a passphrase. + Import(String, String), +} + /// An action that the main program loop should. /// /// See [the commands module][super::commands] for where these are usually created. @@ -428,6 +437,9 @@ pub enum IambAction { /// Perform an action against the homeserver. Homeserver(HomeserverAction), + /// Perform an action over room keys. + Keys(KeysAction), + /// Perform an action on the currently selected message. Message(MessageAction), @@ -485,6 +497,7 @@ impl ApplicationAction for IambAction { fn is_edit_sequence(&self, _: &EditContext) -> SequenceStatus { match self { IambAction::Homeserver(..) => SequenceStatus::Break, + IambAction::Keys(..) => SequenceStatus::Break, IambAction::Message(..) => SequenceStatus::Break, IambAction::Room(..) => SequenceStatus::Break, IambAction::OpenLink(..) => SequenceStatus::Break, @@ -498,6 +511,7 @@ impl ApplicationAction for IambAction { fn is_last_action(&self, _: &EditContext) -> SequenceStatus { match self { IambAction::Homeserver(..) => SequenceStatus::Atom, + IambAction::Keys(..) => SequenceStatus::Atom, IambAction::Message(..) => SequenceStatus::Atom, IambAction::OpenLink(..) => SequenceStatus::Atom, IambAction::Room(..) => SequenceStatus::Atom, @@ -511,6 +525,7 @@ impl ApplicationAction for IambAction { fn is_last_selection(&self, _: &EditContext) -> SequenceStatus { match self { IambAction::Homeserver(..) => SequenceStatus::Ignore, + IambAction::Keys(..) => SequenceStatus::Ignore, IambAction::Message(..) => SequenceStatus::Ignore, IambAction::Room(..) => SequenceStatus::Ignore, IambAction::OpenLink(..) => SequenceStatus::Ignore, @@ -526,6 +541,7 @@ impl ApplicationAction for IambAction { IambAction::Homeserver(..) => false, IambAction::Message(..) => false, IambAction::Room(..) => false, + IambAction::Keys(..) => false, IambAction::Send(..) => false, IambAction::OpenLink(..) => false, IambAction::ToggleScrollbackFocus => false, @@ -585,6 +601,9 @@ pub enum IambError { #[error("Cryptographic storage error: {0}")] CryptoStore(#[from] matrix_sdk::encryption::CryptoStoreError), + #[error("Failed to import room keys: {0}")] + FailedKeyImport(#[from] matrix_sdk::encryption::RoomKeyImportError), + /// A failure related to the cryptographic store. #[error("Cannot export keys from sled: {0}")] UpgradeSled(#[from] crate::sled_export::SledMigrationError), @@ -1767,7 +1786,7 @@ fn complete_cmdarg( match cmd.name.as_str() { "cancel" | "dms" | "edit" | "redact" | "reply" => vec![], "members" | "rooms" | "spaces" | "welcome" => vec![], - "download" | "open" | "upload" => complete_path(text, cursor), + "download" | "keys" | "open" | "upload" => complete_path(text, cursor), "react" | "unreact" => complete_emoji(text, cursor, store), "invite" => complete_users(text, cursor, store), diff --git a/src/commands.rs b/src/commands.rs index 9eefe5f..d6db847 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -19,6 +19,7 @@ use crate::base::{ HomeserverAction, IambAction, IambId, + KeysAction, MessageAction, ProgramCommand, ProgramCommands, @@ -102,6 +103,29 @@ fn iamb_invite(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { return Ok(step); } +fn iamb_keys(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + let mut args = desc.arg.strings()?; + + if args.len() != 3 { + return Err(CommandError::InvalidArgument); + } + + let act = args.remove(0); + let path = args.remove(0); + let passphrase = args.remove(0); + + let act = match act.as_str() { + "export" => KeysAction::Export(path, passphrase), + "import" => KeysAction::Import(path, passphrase), + _ => return Err(CommandError::InvalidArgument), + }; + + let vact = IambAction::Keys(act); + let step = CommandStep::Continue(vact.into(), ctx.context.clone()); + + return Ok(step); +} + fn iamb_verify(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { let mut args = desc.arg.strings()?; @@ -523,6 +547,7 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) { f: iamb_invite, }); cmds.add_command(ProgramCommand { name: "join".into(), aliases: vec![], f: iamb_join }); + cmds.add_command(ProgramCommand { name: "keys".into(), aliases: vec![], f: iamb_keys }); cmds.add_command(ProgramCommand { name: "leave".into(), aliases: vec![], @@ -959,4 +984,31 @@ mod tests { let res = cmds.input_cmd("redact Removed Removed", ctx.clone()); assert_eq!(res, Err(CommandError::InvalidArgument)); } + + #[test] + fn test_cmd_keys() { + let mut cmds = setup_commands(); + let ctx = EditContext::default(); + + let res = cmds.input_cmd("keys import /a/b/c pword", ctx.clone()).unwrap(); + let act = IambAction::Keys(KeysAction::Import("/a/b/c".into(), "pword".into())); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds.input_cmd("keys export /a/b/c pword", ctx.clone()).unwrap(); + let act = IambAction::Keys(KeysAction::Export("/a/b/c".into(), "pword".into())); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + // Invalid invocations. + let res = cmds.input_cmd("keys", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + + let res = cmds.input_cmd("keys import", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + + let res = cmds.input_cmd("keys import foo", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + + let res = cmds.input_cmd("keys import foo bar baz", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + } } diff --git a/src/main.rs b/src/main.rs index 1782c80..937196a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,6 +87,7 @@ use crate::{ IambId, IambInfo, IambResult, + KeysAction, ProgramAction, ProgramContext, ProgramStore, @@ -529,6 +530,7 @@ impl Application { None }, + IambAction::Keys(act) => self.keys_command(act, ctx, store).await?, IambAction::Message(act) => { self.screen.current_window_mut()?.message_command(act, ctx, store).await? }, @@ -603,6 +605,36 @@ impl Application { } } + async fn keys_command( + &mut self, + action: KeysAction, + _: ProgramContext, + store: &mut ProgramStore, + ) -> IambResult { + let encryption = store.application.worker.client.encryption(); + + match action { + KeysAction::Export(path, passphrase) => { + encryption + .export_room_keys(path.into(), &passphrase, |_| true) + .await + .map_err(IambError::from)?; + + Ok(Some("Successfully exported room keys".into())) + }, + KeysAction::Import(path, passphrase) => { + let res = encryption + .import_room_keys(path.into(), &passphrase) + .await + .map_err(IambError::from)?; + + let msg = format!("Imported {} of {} keys", res.imported_count, res.total_count); + + Ok(Some(msg.into())) + }, + } + } + fn handle_info(&mut self, info: InfoMessage) { match info { InfoMessage::Message(info) => {