diff --git a/Cargo.lock b/Cargo.lock index d5783d79..58aabdc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3457,6 +3457,7 @@ dependencies = [ "itertools 0.14.0", "js_option", "matrix-sdk-common", + "matrix-sdk-qrcode", "pbkdf2", "rand 0.10.1", "rmp-serde", @@ -3508,6 +3509,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "matrix-sdk-qrcode" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc199f424cd31ad8a63717965779f6ecc877b3ecebe31db8030ef3be8200b0cd" +dependencies = [ + "byteorder", + "qrcode", + "ruma", + "thiserror 2.0.18", + "vodozemac", +] + [[package]] name = "matrix-sdk-sqlite" version = "0.18.0" @@ -4566,6 +4580,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" + [[package]] name = "quantette" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index 9cb3e413..9c1493dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ version = "0.18.0" [dependencies.matrix-sdk] version = "0.18.0" default-features = false -features = ["e2e-encryption", "socks", "sqlite", "sso-login"] +features = ["e2e-encryption", "socks", "sqlite", "sso-login", "qrcode"] [dependencies.matrix-sdk-crypto] version = "0.18.0" diff --git a/docs/iamb.1 b/docs/iamb.1 index 3f75ae73..e86cbbfa 100644 --- a/docs/iamb.1 +++ b/docs/iamb.1 @@ -84,6 +84,8 @@ Import and decrypt keys from View a list of ongoing E2EE verifications. .It Sy ":verify accept [key]" Accept a verification request. +.It Sy ":verify emoji [key]" +Transition a request to use interactive emoji verification. .It Sy ":verify cancel [key]" Cancel an in-progress verification. .It Sy ":verify confirm [key]" diff --git a/src/base.rs b/src/base.rs index c74cedda..66a58cf5 100644 --- a/src/base.rs +++ b/src/base.rs @@ -12,6 +12,7 @@ use std::time::{Duration, Instant}; use emojis::Emoji; +use matrix_sdk::encryption::verification::VerificationRequest; use ratatui::{ buffer::Buffer, layout::{Alignment, Rect}, @@ -31,7 +32,6 @@ use url::Url; use matrix_sdk::{ RoomState as MatrixRoomState, - encryption::verification::SasVerification, room::Room as MatrixRoom, ruma::{ EventId, @@ -139,6 +139,9 @@ pub enum VerifyAction { /// Reject an in-progress verification due to mismatched Emoji. Mismatch, + + /// Start an interactive (SAS) emoji verification + Emoji, } /// An action taken against the currently selected message. @@ -1839,7 +1842,8 @@ pub struct ChatStore { pub presences: CompletionMap, /// In-progress and completed verifications. - pub verifications: CompletionMap, + /// The map key is the `flow_id`. + pub verifications: CompletionMap, /// Settings for the current profile loaded from config file. pub settings: ApplicationSettings, @@ -1927,13 +1931,6 @@ impl ChatStore { pub fn set_room_name(&mut self, room_id: &RoomId, name: &str) { self.rooms.get_or_default(room_id.to_owned()).name = name.to_string().into(); } - - /// Insert a new E2EE verification. - pub fn insert_sas(&mut self, sas: SasVerification) { - let key = format!("{}/{}", sas.other_user_id(), sas.other_device().device_id()); - - self.verifications.insert(key, sas); - } } impl ApplicationStore for ChatStore {} diff --git a/src/commands.rs b/src/commands.rs index 5db66deb..c9baa513 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -211,6 +211,7 @@ fn iamb_verify(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { "cancel" => VerifyAction::Cancel, "confirm" => VerifyAction::Confirm, "mismatch" => VerifyAction::Mismatch, + "emoji" => VerifyAction::Emoji, "request" => { let iact = IambAction::VerifyRequest(args.remove(1)); let step = CommandStep::Continue(iact.into(), ctx.context.clone()); diff --git a/src/completions.rs b/src/completions.rs index c429c6a4..f9c6c828 100644 --- a/src/completions.rs +++ b/src/completions.rs @@ -373,7 +373,14 @@ fn complete_iamb_keys( /// Tab completion for `:verify` fn complete_iamb_verify(args: Vec, store: &ChatStore) -> Vec { - let subcmds = ["request", "accept", "confirm", "cancel", "missmatch"]; + let subcmds = [ + "request", + "accept", + "confirm", + "cancel", + "missmatch", + "emoji", + ]; match args.len() { 1 => complete_choices(&args[0], &subcmds), 2 if args[0] == "request" => complete_users(&args[1], store), diff --git a/src/main.rs b/src/main.rs index f9856e04..f13228a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::{Duration, Instant}; use clap::{CommandFactory, Parser}; -use matrix_sdk::ruma::OwnedUserId; +use matrix_sdk::ruma::UserId; use matrix_sdk::ruma::api::error::ErrorKind; use matrix_sdk::ruma::profile::{ProfileFieldName, ProfileFieldValue}; use matrix_sdk_crypto::encrypt_room_key_export; @@ -87,6 +87,7 @@ mod worker; #[cfg(test)] mod tests; +mod verifications; use crate::{ base::{ @@ -621,19 +622,15 @@ impl Application { None }, - IambAction::Verify(act, user_dev) => { - if let Some(sas) = store.application.verifications.get(&user_dev) { - self.worker.verify(act, sas.clone())? - } else { - return Err(IambError::InvalidVerificationId(user_dev).into()); - } + IambAction::Verify(act, flow_id) => { + return verifications::iamb_verify(act, flow_id, store).await; }, IambAction::VerifyRequest(user_id) => { - if let Ok(user_id) = OwnedUserId::try_from(user_id.as_str()) { - self.worker.verify_request(user_id)? - } else { + let Ok(user_id) = <&UserId>::try_from(user_id.as_str()) else { return Err(IambError::InvalidUserId(user_id).into()); - } + }; + + return verifications::iamb_verify_request(user_id, store).await; }, }; diff --git a/src/verifications.rs b/src/verifications.rs new file mode 100644 index 00000000..77b99b63 --- /dev/null +++ b/src/verifications.rs @@ -0,0 +1,226 @@ +use matrix_sdk::Client; +use matrix_sdk::encryption::verification::{ + Verification, + VerificationRequest, + VerificationRequestState, +}; +use matrix_sdk::ruma::events::key::verification::VerificationMethod; +use matrix_sdk::ruma::{OwnedDeviceId, OwnedUserId, UserId}; +use modalkit::errors::UIError; +use modalkit::prelude::{EditInfo, InfoMessage}; + +use crate::base::{AsyncProgramStore, IambError, IambResult, ProgramStore, VerifyAction}; + +const SUPPORTED_METHODS: [VerificationMethod; 3] = [ + // Emoji verification + VerificationMethod::SasV1, + // QR Code verification + VerificationMethod::ReciprocateV1, + VerificationMethod::QrCodeShowV1, +]; + +async fn maybe_autostart(request: &VerificationRequest) -> Result<(), matrix_sdk::Error> { + if let Some(theirs) = request.their_supported_methods() { + if theirs.contains(&VerificationMethod::QrCodeScanV1) && + theirs.contains(&VerificationMethod::ReciprocateV1) + { + // Generate a QR code to show. This doesn't actually mean we select + // this flow. + request.generate_qr_code().await?; + } else if theirs.contains(&VerificationMethod::SasV1) { + // We only have one method in common and don't need to query the + // user. + request.start_sas().await?; + } + } + + Ok(()) +} + +pub async fn handle_request( + flow_id: String, + other_user_id: OwnedUserId, + other_device_id: OwnedDeviceId, + client: Client, + store: AsyncProgramStore, +) { + let own_user_id = client.user_id().unwrap(); + let own_device_id = client.device_id().unwrap(); + if other_user_id == own_user_id && other_device_id == own_device_id { + tracing::debug!("ignoring the verification request we sent"); + return; + } + + let Some(request) = client + .encryption() + .get_verification_request(&other_user_id, &flow_id) + .await + else { + tracing::warn!("couldn't find verification request in crypto store"); + return; + }; + + tracing::debug!("received a verification request"); + + store.lock().await.application.verifications.insert(flow_id, request); +} + +pub async fn handle_ready( + flow_id: String, + other_user_id: OwnedUserId, + client: Client, + store: AsyncProgramStore, +) { + let Some(request) = client + .encryption() + .get_verification_request(&other_user_id, &flow_id) + .await + else { + tracing::warn!("couldn't find verification request in crypto store"); + return; + }; + + if let Err(err) = maybe_autostart(&request).await { + tracing::warn!("unable to start verification process: {err}"); + } + + // Insert the request in case we missed it. Not sure if this is needed. + // Might happen with room verification requests if the client is restarted. + store.lock().await.application.verifications.insert(flow_id, request); +} + +pub async fn handle_start(flow_id: String, other_user_id: OwnedUserId, client: Client) { + match client.encryption().get_verification(&other_user_id, &flow_id).await { + Some(Verification::SasV1(sas)) => { + tracing::debug!("accepting SAS verification flow"); + if let Err(err) = sas.accept().await { + tracing::warn!("unable to accept SAS verification flow: {err}"); + } + }, + Some(_) => { + tracing::info!("ignoring verification start with unsupported method"); + }, + None => { + tracing::warn!("couldn't find verification request in crypto store"); + }, + } +} + +pub async fn iamb_verify( + act: VerifyAction, + flow_id: String, + store: &ProgramStore, +) -> IambResult { + let Some(request) = store.application.verifications.get(&flow_id) else { + return Err(IambError::InvalidVerificationId(flow_id).into()); + }; + + match act { + VerifyAction::Accept => { + if request.their_supported_methods().is_none_or(|theirs| { + !(theirs.contains(&VerificationMethod::SasV1) || + theirs.contains(&VerificationMethod::QrCodeScanV1) && + theirs.contains(&VerificationMethod::ReciprocateV1)) + }) { + let msg = "We don't have any verification methods in common."; + let err = UIError::Failure(msg.into()); + return Err(err); + } + + request + .accept_with_methods(SUPPORTED_METHODS.into()) + .await + .map_err(IambError::from)?; + + maybe_autostart(request).await.map_err(IambError::from)?; + + Ok(Some(InfoMessage::from("Accepted verification request"))) + }, + VerifyAction::Emoji => { + if request + .their_supported_methods() + .is_none_or(|theirs| !theirs.contains(&VerificationMethod::SasV1)) + { + let msg = "The other party doesn't support emoji verification."; + let err = UIError::Failure(msg.into()); + return Err(err); + } + + if request.start_sas().await.map_err(IambError::from)?.is_some() { + Ok(Some(InfoMessage::from("Verification started"))) + } else { + let msg = "Can't start interactive verification at this point."; + let err = UIError::Failure(msg.into()); + Err(err) + } + }, + VerifyAction::Cancel => { + request.cancel().await.map_err(IambError::from)?; + Ok(Some(InfoMessage::from("Cancelled verification"))) + }, + VerifyAction::Confirm => { + match request.state() { + VerificationRequestState::Transitioned { + verification: Verification::SasV1(sas), + } if sas.can_be_presented() => { + sas.confirm().await.map_err(IambError::from)?; + }, + VerificationRequestState::Transitioned { verification: Verification::QrV1(qr) } + if qr.has_been_scanned() => + { + qr.confirm().await.map_err(IambError::from)?; + }, + _ => { + let msg = "Can only confirm in-progress verifications!"; + let err = UIError::Failure(msg.into()); + return Err(err); + }, + } + + Ok(Some(InfoMessage::from("Confirmed verification"))) + }, + VerifyAction::Mismatch => { + match request.state() { + VerificationRequestState::Transitioned { + verification: Verification::SasV1(sas), + } if sas.can_be_presented() => { + sas.mismatch().await.map_err(IambError::from)?; + }, + VerificationRequestState::Transitioned { verification: Verification::QrV1(qr) } + if qr.has_been_scanned() => + { + qr.cancel().await.map_err(IambError::from)?; + }, + _ => { + let msg = "Can only reject in-progress verifications!"; + let err = UIError::Failure(msg.into()); + return Err(err); + }, + } + + Ok(Some(InfoMessage::from("Rejected verification"))) + }, + } +} + +pub async fn iamb_verify_request( + user_id: &UserId, + store: &mut ProgramStore, +) -> IambResult { + let enc = store.application.worker.client.encryption(); + + let Some(identity) = enc.get_user_identity(user_id).await.map_err(IambError::from)? else { + let msg = format!("Could not find identity information for {user_id}"); + let err = UIError::Failure(msg); + return Err(err); + }; + + let request = identity.request_verification_with_methods(SUPPORTED_METHODS.into()); + let request = request.await.map_err(IambError::from)?; + + let flow_id = request.flow_id().to_owned(); + store.application.verifications.insert(flow_id, request); + + let info = format!("Sent verification request to {user_id}"); + Ok(Some(InfoMessage::from(info))) +} diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 53d5d3ce..458d00d1 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -6,7 +6,7 @@ //! Additionally, some of the iamb commands delegate behaviour to the current UI element. For //! example, [sending messages][crate::base::SendAction] delegate to the [room window][RoomState], //! where we have the message bar and room ID easily accessible and resettable. -use std::cmp::{Ord, Ordering, PartialOrd}; +use std::cmp::{Ord, Ordering}; use std::fmt::{self, Display}; use std::ops::Deref; use std::sync::Arc; @@ -14,7 +14,6 @@ use std::time::{Duration, Instant}; use matrix_sdk::{ RoomState as MatrixRoomState, - encryption::verification::{SasVerification, format_emojis}, room::{Room as MatrixRoom, RoomMember}, ruma::{ OwnedRoomAliasId, @@ -80,11 +79,14 @@ use crate::base::{ }; use crate::windows::room::room_command; -use self::{room::RoomState, welcome::WelcomeState}; +use self::room::RoomState; +use self::verify::VerifyItem; +use self::welcome::WelcomeState; use crate::message::MessageTimeStamp; use feruca::Collator; pub mod room; +pub mod verify; pub mod welcome; type MatrixRoomInfo = Arc<(MatrixRoom, Option)>; @@ -107,7 +109,7 @@ fn bold_spans(s: &str) -> Line<'_> { } #[inline] -fn selected_style(selected: bool) -> Style { +pub fn selected_style(selected: bool) -> Style { if selected { Style::default().add_modifier(StyleModifier::REVERSED) } else { @@ -765,12 +767,20 @@ impl WindowOps for IambWindow { .render(area, buf, state); }, IambWindow::VerifyList(state) => { - let verifications = &store.application.verifications; - let mut items = verifications.iter().map(VerifyItem::from).collect::>(); + let mut items = store + .application + .verifications + .iter() + .map(|(_, req)| VerifyItem::new(req.to_owned())) + .collect::>(); // Sort the active verifications towards the top. items.sort(); + if let Some(item) = items.first_mut() { + item.show_help(); + } + state.set(items); state.set_ignorecase(store.application.settings.tunables.ignorecase); @@ -1430,208 +1440,6 @@ impl Promptable for SpaceItem { } } -#[derive(Clone)] -pub struct VerifyItem { - user_dev: String, - sasv1: SasVerification, -} - -impl VerifyItem { - fn new(user_dev: String, sasv1: SasVerification) -> Self { - VerifyItem { user_dev, sasv1 } - } - - fn show_item(&self) -> String { - let state = if self.sasv1.is_done() { - "done" - } else if self.sasv1.is_cancelled() { - "cancelled" - } else if self.sasv1.emoji().is_some() { - "accepted" - } else { - "not accepted" - }; - - if self.sasv1.is_self_verification() { - let device = self.sasv1.other_device(); - - if let Some(display_name) = device.display_name() { - format!("Device verification with {display_name} ({state})") - } else { - format!("Device verification with device {} ({})", device.device_id(), state) - } - } else { - format!("User Verification with {} ({})", self.sasv1.other_user_id(), state) - } - } -} - -impl PartialEq for VerifyItem { - fn eq(&self, other: &Self) -> bool { - self.user_dev == other.user_dev - } -} - -impl Eq for VerifyItem {} - -impl Ord for VerifyItem { - fn cmp(&self, other: &Self) -> Ordering { - fn state_val(sas: &SasVerification) -> usize { - if sas.is_done() { - return 3; - } else if sas.is_cancelled() { - return 2; - } else { - return 1; - } - } - - fn device_val(sas: &SasVerification) -> usize { - if sas.is_self_verification() { - return 1; - } else { - return 2; - } - } - - let state1 = state_val(&self.sasv1); - let state2 = state_val(&other.sasv1); - - let dev1 = device_val(&self.sasv1); - let dev2 = device_val(&other.sasv1); - - let scmp = state1.cmp(&state2); - let dcmp = dev1.cmp(&dev2); - - scmp.then(dcmp).then_with(|| { - let did1 = self.sasv1.other_device().device_id(); - let did2 = other.sasv1.other_device().device_id(); - - did1.cmp(did2) - }) - } -} - -impl PartialOrd for VerifyItem { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl From<(&String, &SasVerification)> for VerifyItem { - fn from((user_dev, sasv1): (&String, &SasVerification)) -> Self { - VerifyItem::new(user_dev.clone(), sasv1.clone()) - } -} - -impl Display for VerifyItem { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.sasv1.is_done() { - return Ok(()); - } - - if self.sasv1.is_cancelled() { - write!(f, ":verify request {}", self.sasv1.other_user_id()) - } else if self.sasv1.emoji().is_some() { - write!(f, ":verify confirm {}", self.user_dev) - } else { - write!(f, ":verify accept {}", self.user_dev) - } - } -} - -impl ListItem for VerifyItem { - fn show( - &self, - selected: bool, - _: &ViewportContext, - _: &mut ProgramStore, - ) -> Text<'_> { - let mut lines = vec![]; - - let bold = Style::default().add_modifier(StyleModifier::BOLD); - let item = Span::styled(self.show_item(), selected_style(selected)); - lines.push(Line::from(item)); - - if self.sasv1.is_done() { - // Print nothing. - } else if self.sasv1.is_cancelled() { - if let Some(info) = self.sasv1.cancel_info() { - lines.push(Line::from(format!(" Cancelled: {}", info.reason()))); - lines.push(Line::from("")); - } - - lines.push(Line::from(" You can start a new verification request with:")); - } else if let Some(emoji) = self.sasv1.emoji() { - lines.push(Line::from( - " Both devices should see the following Emoji sequence:".to_string(), - )); - lines.push(Line::from("")); - - for line in format_emojis(emoji).lines() { - lines.push(Line::from(format!(" {line}"))); - } - - lines.push(Line::from("")); - lines.push(Line::from(" If they don't match, run:")); - lines.push(Line::from("")); - lines.push(Line::from(Span::styled( - format!(":verify mismatch {}", self.user_dev), - bold, - ))); - lines.push(Line::from("")); - lines.push(Line::from(" If everything looks right, you can confirm with:")); - } else { - lines.push(Line::from(" To accept this request, run:")); - } - - let cmd = self.to_string(); - - if !cmd.is_empty() { - lines.push(Line::from("")); - lines.push(Line::from(vec![Span::from(" "), Span::styled(cmd, bold)])); - lines.push(Line::from("")); - lines.push(Line::from(vec![ - Span::from("You can copy the above command with "), - Span::styled("yy", bold), - Span::from(" and then execute it with "), - Span::styled("@\"", bold), - ])); - } - - Text::from(lines) - } - - fn get_word(&self) -> Option { - None - } -} - -impl Promptable for VerifyItem { - fn prompt( - &mut self, - act: &PromptAction, - _: &ProgramContext, - _: &mut ProgramStore, - ) -> EditResult, IambInfo> { - match act { - PromptAction::Submit => Ok(vec![]), - PromptAction::Abort(_) => { - let msg = "Cannot abort entry inside a list"; - let err = EditError::Failure(msg.into()); - - Err(err) - }, - PromptAction::Recall(..) => { - let msg = "Cannot recall history inside a list"; - let err = EditError::Failure(msg.into()); - - Err(err) - }, - } - } -} - #[derive(Clone)] pub struct MemberItem { member: RoomMember, diff --git a/src/windows/verify.rs b/src/windows/verify.rs new file mode 100644 index 00000000..b6f96d31 --- /dev/null +++ b/src/windows/verify.rs @@ -0,0 +1,366 @@ +use std::cmp::Ordering; +use std::fmt; + +use matrix_sdk::encryption::verification::{ + Verification, + VerificationRequest, + VerificationRequestState, +}; +use matrix_sdk::ruma::events::key::verification::VerificationMethod; +use matrix_sdk_crypto::matrix_sdk_qrcode::qrcode::render::unicode::Dense1x2; +use matrix_sdk_crypto::{QrVerificationState, SasState, format_emojis}; + +use modalkit::actions::{PromptAction, Promptable}; +use modalkit::errors::{EditError, EditResult}; +use modalkit::prelude::ViewportContext; +use modalkit_ratatui::list::{ListCursor, ListItem}; +use ratatui::style::{Color, Modifier as StyleModifier, Style}; +use ratatui::text::{Line, Span, Text}; + +use crate::base::{IambInfo, ProgramAction, ProgramContext, ProgramStore}; + +const BLACK_ON_WHITE: Style = Style::new().fg(Color::Black).bg(Color::White); + +#[derive(Clone)] +pub struct VerifyItem { + request: VerificationRequest, + show_help: bool, +} + +impl VerifyItem { + pub fn new(request: VerificationRequest) -> Self { + Self { request, show_help: false } + } + + pub fn show_help(&mut self) { + self.show_help = true; + } +} + +impl PartialEq for VerifyItem { + fn eq(&self, other: &Self) -> bool { + self.request.flow_id() == other.request.flow_id() + } +} + +impl Eq for VerifyItem {} + +impl Ord for VerifyItem { + fn cmp(&self, other: &Self) -> Ordering { + fn state_val(req: &VerificationRequest) -> usize { + // 0: running + // 1: ready + // 2: requests for this session + // 3: all others + // 4: canceled + // 5: done + match req.state() { + VerificationRequestState::Requested { .. } => 2, + VerificationRequestState::Ready { .. } => 1, + VerificationRequestState::Transitioned { + verification: Verification::SasV1(sas), + } => { + match sas.state() { + SasState::KeysExchanged { emojis: Some(_), .. } => 0, + SasState::Done { .. } => 5, + SasState::Cancelled(_) => 4, + _ => 3, + } + }, + VerificationRequestState::Transitioned { verification: Verification::QrV1(qr) } => { + match qr.state() { + QrVerificationState::Started => 1, + QrVerificationState::Scanned => 0, + QrVerificationState::Done { .. } => 5, + QrVerificationState::Cancelled(_) => 4, + _ => 3, + } + }, + VerificationRequestState::Done => 5, + VerificationRequestState::Cancelled(_) => 4, + _ => 3, + } + } + + fn device_val(req: &VerificationRequest) -> usize { + if req.is_self_verification() { 1 } else { 2 } + } + + let state1 = state_val(&self.request); + let state2 = state_val(&other.request); + + let dev1 = device_val(&self.request); + let dev2 = device_val(&other.request); + + let scmp = state1.cmp(&state2); + let dcmp = dev1.cmp(&dev2); + + scmp.then(dcmp).then_with(|| { + let did1 = self.request.flow_id(); + let did2 = other.request.flow_id(); + + did1.cmp(did2) + }) + } +} + +impl PartialOrd for VerifyItem { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl fmt::Display for VerifyItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.request.state() { + VerificationRequestState::Requested { .. } => { + write!(f, ":verify accept {}", self.request.flow_id()) + }, + VerificationRequestState::Ready { their_methods, .. } + if their_methods.contains(&VerificationMethod::SasV1) => + { + write!(f, ":verify emoji {}", self.request.flow_id()) + }, + VerificationRequestState::Transitioned { verification: Verification::SasV1(sas) } => { + match sas.state() { + SasState::KeysExchanged { emojis: Some(_), .. } => { + write!(f, ":verify confirm {}", self.request.flow_id()) + }, + _ => Ok(()), + } + }, + VerificationRequestState::Transitioned { verification: Verification::QrV1(qr) } => { + match qr.state() { + QrVerificationState::Started => { + write!(f, ":verify emoji {}", self.request.flow_id()) + }, + QrVerificationState::Scanned => { + write!(f, ":verify confirm {}", self.request.flow_id()) + }, + _ => Ok(()), + } + }, + _ => Ok(()), + } + } +} + +impl ListItem for VerifyItem { + fn show( + &self, + selected: bool, + _: &ViewportContext, + store: &mut ProgramStore, + ) -> Text<'_> { + let mut lines = vec![]; + let bold = Style::default().add_modifier(StyleModifier::BOLD); + let selected_bold = super::selected_style(selected).add_modifier(StyleModifier::BOLD); + let selected = super::selected_style(selected); + + let mut other_device = None; + let state = match self.request.state() { + _ if self.request.is_passive() => "completed with other device", + VerificationRequestState::Created { .. } => "request sent", + VerificationRequestState::Requested { their_methods, other_device_data } => { + other_device = Some(other_device_data); + + if their_methods.contains(&VerificationMethod::SasV1) { + lines.push(Line::from(" To accept this request, run:")); + "requested" + } else { + "no methods in common" + } + }, + VerificationRequestState::Ready { other_device_data, their_methods, .. } => { + // This state should only be temporary since we either generate a qr code or + // directly start SAS verification. + + other_device = Some(other_device_data); + + if their_methods.contains(&VerificationMethod::SasV1) { + lines.push(Line::from(" To start interactive verification, run:")); + } + + "ready" + }, + VerificationRequestState::Transitioned { verification: Verification::SasV1(sas) } => { + other_device = Some(sas.other_device().to_owned()); + + match sas.state() { + SasState::Created { .. } | + SasState::Started { .. } | + SasState::Accepted { .. } => "starting", + SasState::KeysExchanged { emojis: Some(emojis), .. } => { + lines.push(Line::from( + " Both devices should see the following Emoji sequence:".to_string(), + )); + lines.push(Line::from("")); + + for line in format_emojis(emojis.emojis).lines() { + lines.push(Line::from(format!(" {line}"))); + } + + lines.push(Line::from("")); + lines.push(Line::from(" If they don't match, run:")); + lines.push(Line::from("")); + lines.push(Line::from(Span::styled( + format!(" :verify mismatch {}", self.request.flow_id()), + bold, + ))); + lines.push(Line::from("")); + lines.push(Line::from( + " If everything looks right, you can confirm with:", + )); + "running" + }, + SasState::KeysExchanged { emojis: None, .. } => "unsupported method", + SasState::Confirmed => "waiting for response", + SasState::Done { .. } => "done", + SasState::Cancelled(info) => { + lines.push(Line::from(format!(" Cancelled: {}", info.reason()))); + "cancelled" + }, + } + }, + VerificationRequestState::Transitioned { verification: Verification::QrV1(qr) } => { + other_device = Some(qr.other_device().to_owned()); + + match qr.state() { + QrVerificationState::Started => { + if let Ok(qrcode) = qr.to_qr_code() { + let rendered = qrcode.render::().build(); + lines.extend( + rendered + .lines() + .map(|line| Line::styled(line.to_owned(), BLACK_ON_WHITE)), + ); + + lines.push(Line::from(" Scan this QR code with the other device.")); + lines.push(Line::from( + " To alternativly start interactive verification, run:", + )); + } else { + lines.push(Line::from(" To start interactive verification, run:")); + } + + "ready" + }, + QrVerificationState::Scanned => { + lines.push(Line::from( + " Check whether the other device shows a successful verification." + .to_string(), + )); + lines.push(Line::from("")); + lines.push(Line::from(" If it shows an error, run:")); + lines.push(Line::from("")); + lines.push(Line::from(Span::styled( + format!(" :verify mismatch {}", self.request.flow_id()), + bold, + ))); + lines.push(Line::from("")); + lines.push(Line::from( + " If everything looks right, you can confirm with:", + )); + "running" + }, + QrVerificationState::Confirmed => "waiting for response", + QrVerificationState::Reciprocated => { + tracing::error!( + "reached unreachable state of having scanned a verification QR code" + ); + "inconsistent state" + }, + QrVerificationState::Done { .. } => "done", + QrVerificationState::Cancelled(info) => { + lines.push(Line::from(format!(" Cancelled: {}", info.reason()))); + "cancelled" + }, + } + }, + VerificationRequestState::Transitioned { .. } => "unsupported method", + VerificationRequestState::Done => "done", + VerificationRequestState::Cancelled(info) => { + lines.push(Line::from(format!(" Cancelled: {}", info.reason()))); + "cancelled" + }, + }; + + let line = if self.request.is_self_verification() { + if let Some(device) = other_device { + if let Some(display_name) = device.display_name() { + vec![ + Span::styled("Device verification with ", selected), + Span::styled(display_name.to_owned(), selected_bold), + Span::styled(format!(" ({state})"), selected), + ] + } else { + vec![ + Span::styled("Device verification with ", selected), + Span::styled(device.device_id().to_string(), selected_bold), + Span::styled(format!(" ({state})"), selected), + ] + } + } else { + vec![Span::styled( + format!("Device verification with any own device ({state})"), + selected, + )] + } + } else { + let color = store.application.settings.get_user_color(self.request.other_user_id()); + vec![ + Span::styled("User verification with ", selected), + Span::styled(self.request.other_user_id().as_str(), selected_bold.patch(color)), + Span::styled(format!(" ({state})"), selected), + ] + }; + lines.insert(0, line.into()); + + let cmd = self.to_string(); + + if !cmd.is_empty() { + lines.push(Line::from("")); + lines.push(Line::from(vec![Span::from(" "), Span::styled(cmd, bold)])); + if self.show_help { + lines.push(Line::from("")); + lines.push(Line::from(vec![ + Span::from("You can copy the above command with "), + Span::styled("yy", bold), + Span::from(" and then execute it with "), + Span::styled("@\"", bold), + ])); + } + } + + Text::from(lines) + } + + fn get_word(&self) -> Option { + None + } +} + +impl Promptable for VerifyItem { + fn prompt( + &mut self, + act: &PromptAction, + _: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult, IambInfo> { + match act { + PromptAction::Submit => Ok(vec![]), + PromptAction::Abort(_) => { + let msg = "Cannot abort entry inside a list"; + let err = EditError::Failure(msg.into()); + + Err(err) + }, + PromptAction::Recall(..) => { + let msg = "Cannot recall history inside a list"; + let err = EditError::Failure(msg.into()); + + Err(err) + }, + } + } +} diff --git a/src/worker.rs b/src/worker.rs index 06744173..d91517dc 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -13,12 +13,16 @@ use std::time::{Duration, Instant}; use futures::{StreamExt, stream::FuturesUnordered}; use gethostname::gethostname; +use matrix_sdk::ruma::events::key::verification::ready::{ + OriginalSyncKeyVerificationReadyEvent, + ToDeviceKeyVerificationReadyEvent, +}; use matrix_sdk_base::RoomStateFilter; use ratatui_image::picker::Picker; use tokio::sync::Semaphore; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}; use tokio::task::JoinHandle; -use tracing::{error, warn}; +use tracing::{Instrument as _, error, warn}; use url::Url; use matrix_sdk::{ @@ -29,11 +33,7 @@ use matrix_sdk::{ RoomMemberships, authentication::matrix::MatrixSession, config::{RequestConfig, SyncSettings}, - encryption::{ - BackupDownloadStrategy, - EncryptionSettings, - verification::{SasVerification, Verification}, - }, + encryption::{BackupDownloadStrategy, EncryptionSettings}, event_handler::Ctx, reqwest, room::{Messages, MessagesOptions, Room as MatrixRoom, RoomMember}, @@ -63,9 +63,6 @@ use matrix_sdk::{ SyncMessageLikeEvent, SyncStateEvent, key::verification::{ - VerificationMethod, - done::{OriginalSyncKeyVerificationDoneEvent, ToDeviceKeyVerificationDoneEvent}, - key::{OriginalSyncKeyVerificationKeyEvent, ToDeviceKeyVerificationKeyEvent}, request::ToDeviceKeyVerificationRequestEvent, start::{OriginalSyncKeyVerificationStartEvent, ToDeviceKeyVerificationStartEvent}, }, @@ -99,6 +96,7 @@ use crate::config::{ImagePreviewSize, ProxyUrl}; use crate::message::{Message, MessageEvent, MessageId, MessageKey}; use crate::notifications::register_notifications; use crate::preview::PreviewKind; +use crate::verifications; use crate::{ ApplicationSettings, base::{ @@ -111,7 +109,6 @@ use crate::{ ProgramStore, RoomFetchStatus, RoomInfo, - VerifyAction, }, }; @@ -739,8 +736,6 @@ pub enum WorkerTask { Members(OwnedRoomId, ClientReply>>), SpaceMembers(OwnedRoomId, ClientReply>>), TypingNotice(OwnedRoomId), - Verify(VerifyAction, SasVerification, ClientReply>), - VerifyRequest(OwnedUserId, ClientReply>), LoadImage(MediaSource, PreviewKind, ImagePreviewSize, Arc, Arc), } @@ -792,19 +787,6 @@ impl Debug for WorkerTask { WorkerTask::TypingNotice(room_id) => { f.debug_tuple("WorkerTask::TypingNotice").field(room_id).finish() }, - WorkerTask::Verify(act, sasv1, _) => { - f.debug_tuple("WorkerTask::Verify") - .field(act) - .field(sasv1) - .field(&format_args!("_")) - .finish() - }, - WorkerTask::VerifyRequest(user_id, _) => { - f.debug_tuple("WorkerTask::VerifyRequest") - .field(user_id) - .field(&format_args!("_")) - .finish() - }, WorkerTask::LoadImage(source, kind, size, _, _) => { f.debug_tuple("WorkerTask::RenderImage") .field(source) @@ -984,22 +966,6 @@ impl Requester { self.tx.send(WorkerTask::TypingNotice(room_id)).unwrap(); } - pub fn verify(&self, act: VerifyAction, sas: SasVerification) -> IambResult { - let (reply, response) = oneshot(); - - self.tx.send(WorkerTask::Verify(act, sas, reply)).unwrap(); - - return response.recv(); - } - - pub fn verify_request(&self, user_id: OwnedUserId) -> IambResult { - let (reply, response) = oneshot(); - - self.tx.send(WorkerTask::VerifyRequest(user_id, reply)).unwrap(); - - return response.recv(); - } - pub fn load_image( &self, source: MediaSource, @@ -1101,14 +1067,6 @@ impl ClientWorker { assert!(self.initialized); self.typing_notice(room_id).await; }, - WorkerTask::Verify(act, sas, reply) => { - assert!(self.initialized); - reply.send(self.verify(act, sas).await); - }, - WorkerTask::VerifyRequest(user_id, reply) => { - assert!(self.initialized); - reply.send(self.verify_request(user_id).await); - }, WorkerTask::LoadImage(source, kind, size, picker, permits) => { assert!(self.initialized); tokio::spawn(crate::preview::load_image( @@ -1179,13 +1137,16 @@ impl ClientWorker { let room_id = room.room_id(); if let Some(msg) = ev.as_original() && - let MessageType::VerificationRequest(_) = msg.content.msgtype && - let Some(request) = client - .encryption() - .get_verification_request(ev.sender(), ev.event_id()) - .await + let MessageType::VerificationRequest(content) = &msg.content.msgtype { - request.accept().await.expect("Failed to accept request"); + verifications::handle_request( + ev.event_id().into(), + ev.sender().into(), + content.from_device.clone(), + client.clone(), + Arc::clone(&store.0), + ) + .await } let mut locked = store.lock().await; @@ -1326,115 +1287,93 @@ impl ClientWorker { ); let _ = self.client.add_event_handler( - |ev: OriginalSyncKeyVerificationStartEvent, - client: Client, - store: Ctx| { - async move { - let tx_id = ev.content.relates_to.event_id.as_ref(); - - if let Some(Verification::SasV1(sas)) = - client.encryption().get_verification(&ev.sender, tx_id).await - { - sas.accept().await.unwrap(); - - store.lock().await.application.insert_sas(sas) - } - } - }, - ); - - let _ = self.client.add_event_handler( - |ev: OriginalSyncKeyVerificationKeyEvent, + |ev: ToDeviceKeyVerificationRequestEvent, client: Client, store: Ctx| { - async move { - let tx_id = ev.content.relates_to.event_id.as_ref(); - - if let Some(Verification::SasV1(sas)) = - client.encryption().get_verification(&ev.sender, tx_id).await - { - store.lock().await.application.insert_sas(sas); - } - } + let span = tracing::info_span!( + "to_device_verify_request", + other_user_id = ?ev.sender, + other_device_id = ?ev.content.from_device, + flow_id = ?ev.content.transaction_id, + ); + verifications::handle_request( + ev.content.transaction_id.into(), + ev.sender, + ev.content.from_device, + client, + store.0, + ) + .instrument(span) }, ); let _ = self.client.add_event_handler( - |ev: OriginalSyncKeyVerificationDoneEvent, + |ev: ToDeviceKeyVerificationReadyEvent, client: Client, store: Ctx| { - async move { - let tx_id = ev.content.relates_to.event_id.as_ref(); - - if let Some(Verification::SasV1(sas)) = - client.encryption().get_verification(&ev.sender, tx_id).await - { - store.lock().await.application.insert_sas(sas); - } - } - }, - ); - - let _ = self.client.add_event_handler( - |ev: ToDeviceKeyVerificationRequestEvent, client: Client| { - async move { - let request = client - .encryption() - .get_verification_request(&ev.sender, &ev.content.transaction_id) - .await; - - if let Some(request) = request { - request.accept().await.unwrap(); - } - } + let span = tracing::info_span!( + "to_device_verify_ready", + other_user_id = ?ev.sender, + other_device_id = ?ev.content.from_device, + flow_id = ?ev.content.transaction_id, + ); + verifications::handle_ready( + ev.content.transaction_id.into(), + ev.sender, + client, + store.0, + ) + .instrument(span) }, ); let _ = self.client.add_event_handler( - |ev: ToDeviceKeyVerificationStartEvent, + |ev: OriginalSyncKeyVerificationReadyEvent, client: Client, store: Ctx| { - async move { - let tx_id = ev.content.transaction_id; - - if let Some(Verification::SasV1(sas)) = - client.encryption().get_verification(&ev.sender, tx_id.as_ref()).await - { - sas.accept().await.unwrap(); - - store.lock().await.application.insert_sas(sas); - } - } + let span = tracing::info_span!( + "room_verify_ready", + other_user_id = ?ev.sender, + other_device_id = ?ev.content.from_device, + flow_id = ?ev.content.relates_to.event_id, + ); + verifications::handle_ready( + ev.content.relates_to.event_id.into(), + ev.sender, + client, + store.0, + ) + .instrument(span) }, ); let _ = self.client.add_event_handler( - |ev: ToDeviceKeyVerificationKeyEvent, client: Client, store: Ctx| { - async move { - let tx_id = ev.content.transaction_id; - - if let Some(Verification::SasV1(sas)) = - client.encryption().get_verification(&ev.sender, tx_id.as_ref()).await - { - store.lock().await.application.insert_sas(sas); - } - } + |ev: ToDeviceKeyVerificationStartEvent, client: Client| { + let span = tracing::info_span!( + "to_device_verify_start", + other_user_id = ?ev.sender, + other_device_id = ?ev.content.from_device, + flow_id = ?ev.content.transaction_id, + ); + verifications::handle_start(ev.content.transaction_id.into(), ev.sender, client) + .instrument(span) }, ); let _ = self.client.add_event_handler( - |ev: ToDeviceKeyVerificationDoneEvent, - client: Client, - store: Ctx| { - async move { - let tx_id = ev.content.transaction_id; - - if let Some(Verification::SasV1(sas)) = - client.encryption().get_verification(&ev.sender, tx_id.as_ref()).await - { - store.lock().await.application.insert_sas(sas); - } - } + |ev: OriginalSyncKeyVerificationStartEvent, client: Client| { + let span = tracing::info_span!( + "room_verify_start", + other_user_id = ?ev.sender, + other_device_id = ?ev.content.from_device, + flow_id = ?ev.content.relates_to.event_id + ); + verifications::handle_start( + ev.content.relates_to.event_id.into(), + ev.sender, + client, + ) + .instrument(span) }, ); @@ -1628,71 +1567,4 @@ impl ClientWorker { let _ = room.typing_notice(true).await; } } - - async fn verify(&self, action: VerifyAction, sas: SasVerification) -> IambResult { - match action { - VerifyAction::Accept => { - sas.accept().await.map_err(IambError::from)?; - - Ok(Some(InfoMessage::from("Accepted verification request"))) - }, - VerifyAction::Confirm => { - if sas.is_done() || sas.is_cancelled() { - let msg = "Can only confirm in-progress verifications!"; - let err = UIError::Failure(msg.into()); - - return Err(err); - } - - sas.confirm().await.map_err(IambError::from)?; - - Ok(Some(InfoMessage::from("Confirmed verification"))) - }, - VerifyAction::Cancel => { - if sas.is_done() || sas.is_cancelled() { - let msg = "Can only cancel in-progress verifications!"; - let err = UIError::Failure(msg.into()); - - return Err(err); - } - - sas.cancel().await.map_err(IambError::from)?; - - Ok(Some(InfoMessage::from("Cancelled verification"))) - }, - VerifyAction::Mismatch => { - if sas.is_done() || sas.is_cancelled() { - let msg = "Can only cancel in-progress verifications!"; - let err = UIError::Failure(msg.into()); - - return Err(err); - } - - sas.mismatch().await.map_err(IambError::from)?; - - Ok(Some(InfoMessage::from("Cancelled verification"))) - }, - } - } - - async fn verify_request(&self, user_id: OwnedUserId) -> IambResult { - let enc = self.client.encryption(); - - match enc.get_user_identity(user_id.as_ref()).await.map_err(IambError::from)? { - Some(identity) => { - let methods = vec![VerificationMethod::SasV1]; - let request = identity.request_verification_with_methods(methods); - let _req = request.await.map_err(IambError::from)?; - let info = format!("Sent verification request to {user_id}"); - - Ok(Some(InfoMessage::from(info))) - }, - None => { - let msg = format!("Could not find identity information for {user_id}"); - let err = UIError::Failure(msg); - - Err(err) - }, - } - } }