diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index 7eab91baf..ff2c699f1 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -71,6 +71,17 @@ script_mod! { } text: "This device is not verified and can't view encrypted messages." } + + verify_device_button := RobrixIconButton { + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, + padding: Inset{top: 10, bottom: 10, left: 12, right: 15} + margin: Inset{top: 10, left: 5, bottom: 4} + draw_icon.svg: (VERIFICATION_YES) + icon_walk: Walk{width: 16, height: 16} + text: "Verify this Device" + } + Label { width: Fill, height: Fit flow: Flow.Right{wrap: true} @@ -79,8 +90,9 @@ script_mod! { color: (MESSAGE_TEXT_COLOR), text_style: theme.font_regular { font_size: 11.5 }, } - text: "Verify it from another client using this info:" + text: "Or verify it from another client using this info:" } + // Filled in from Rust with the session name + device ID. unverified_device_info_label := Label { width: Fill, height: Fit @@ -577,6 +589,10 @@ impl MatchEvent for AccountSettings { ); } + if self.view.button(cx, ids!(verify_device_button)).clicked(actions) { + submit_async_request(MatrixRequest::RequestSelfVerification); + } + if self.view.button(cx, ids!(manage_account_button)).clicked(actions) { // TODO: support opening the user's account management page in a browser, // or perhaps in an in-app pane if that's what is needed for regular UN+PW login. @@ -702,6 +718,7 @@ impl AccountSettings { self.view.button(cx, ids!(accept_display_name_button)).reset_hover(cx); self.view.button(cx, ids!(cancel_display_name_button)).reset_hover(cx); self.view.button(cx, ids!(copy_user_id_button)).reset_hover(cx); + self.view.button(cx, ids!(verify_device_button)).reset_hover(cx); self.view.button(cx, ids!(manage_account_button)).reset_hover(cx); self.view.button(cx, ids!(logout_button)).reset_hover(cx); self.view.redraw(cx); diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 016b743ba..f068e11eb 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -598,6 +598,9 @@ pub enum MatrixRequest { /// Request to fetch our own [`Device`]. /// The response is delivered via [`AccountDataAction::OwnDeviceFetched`]. GetOwnDevice, + /// Request to verify this device by sending an outgoing verification request + /// to the user's other logged-in devices, which'll open the verification modal. + RequestSelfVerification, /// Request to fetch an Avatar image from the server. /// Upon completion of the async media request, the `on_fetched` function /// will be invoked with the content of an `AvatarUpdate`. @@ -1466,6 +1469,13 @@ async fn matrix_worker_task( }); } + MatrixRequest::RequestSelfVerification => { + let Some(client) = get_client() else { continue }; + let _verify_task = Handle::current().spawn( + crate::verification::request_self_verification_handler(client) + ); + } + MatrixRequest::GenerateMatrixLink { room_id, event_id, use_matrix_scheme, join_on_click } => { let Some(client) = get_client() else { continue }; let _gen_link_task = Handle::current().spawn(async move { diff --git a/src/verification.rs b/src/verification.rs index e3848429a..a47740413 100644 --- a/src/verification.rs +++ b/src/verification.rs @@ -1,7 +1,7 @@ use std::fmt::Write; use std::sync::Arc; use futures_util::StreamExt; -use makepad_widgets::{log, Cx}; +use makepad_widgets::{error, log, Cx}; use matrix_sdk_base::crypto::{AcceptedProtocols, CancelInfo, EmojiShortAuthString}; use matrix_sdk::{ encryption::{ @@ -15,6 +15,8 @@ use matrix_sdk::{ }; use tokio::{runtime::Handle, sync::mpsc::{UnboundedReceiver, UnboundedSender}}; +use crate::shared::popup_list::{enqueue_popup_notification, PopupKind}; + #[derive(Clone, Debug, Default)] pub enum VerificationStateAction { Update(VerificationState), @@ -256,6 +258,101 @@ async fn request_verification_handler(client: Client, request: VerificationReque } +/// Sends a self-verification request to the user's other logged-in sessions, +pub async fn request_self_verification_handler(client: Client) { + let Some(user_id) = client.user_id() else { + enqueue_popup_notification("Can't verify this device: you are not logged in.", PopupKind::Error, Some(5.0)); + return; + }; + + let identity = match client.encryption().get_user_identity(user_id).await { + Ok(Some(identity)) => identity, + Ok(None) => { + enqueue_popup_notification( + "Can't verify this device yet: no cross-signing identity exists. \ + Verify from another client first, or reset your identity.", + PopupKind::Error, + Some(8.0), + ); + return; + } + Err(e) => { + error!("Failed to get own user identity for self-verification: {e:?}"); + enqueue_popup_notification(format!("Couldn't start verification: {e}"), PopupKind::Error, Some(6.0)); + return; + } + }; + + let request = match identity.request_verification_with_methods(vec![VerificationMethod::SasV1]).await { + Ok(request) => request, + Err(e) => { + error!("Failed to send self-verification request: {e:?}"); + enqueue_popup_notification(format!("Couldn't send verification request: {e}"), PopupKind::Error, Some(6.0)); + return; + } + }; + log!("Sent self-verification request, flow ID: {}", request.flow_id()); + + // we use the same verification modal as we do for incoming requests. + let (sender, mut response_receiver) = tokio::sync::mpsc::unbounded_channel::(); + Cx::post_action(VerificationAction::RequestReceived( + VerificationRequestActionState { + request: request.clone(), + response_sender: sender, + } + )); + + // Wait for another session to respond, then start SAS verification. + let mut stream = request.changes(); + let sas = loop { + // Use `select` so we can receive a cancel request from the modal + tokio::select! { + // The user canceled from the modal while we were waiting on another session. + response = response_receiver.recv() => { + if !matches!(response, Some(VerificationUserResponse::Accept)) { + let _ = request.cancel().await; + return; + } + } + state = stream.next() => { + let Some(state) = state else { return }; + match state { + VerificationRequestState::Created { .. } + | VerificationRequestState::Requested { .. } => { } + // Another session accepted, so we can now start SAS + VerificationRequestState::Ready { .. } => match request.start_sas().await { + Ok(Some(sas)) => break sas, + // If the other side already started SAS, handle it in the `Transitioned` state below. + Ok(None) => { } + Err(e) => { + Cx::post_action(VerificationAction::RequestAcceptError(Arc::new(e))); + return; + } + } + VerificationRequestState::Transitioned { verification } => match verification { + Verification::SasV1(sas) => break sas, + unsupported => { + Cx::post_action(VerificationAction::RequestTransitionedToUnsupportedMethod(unsupported)); + return; + } + } + VerificationRequestState::Cancelled(info) => { + Cx::post_action(VerificationAction::RequestCancelled(info)); + return; + } + VerificationRequestState::Done => { + Cx::post_action(VerificationAction::RequestCompleted); + return; + } + } + } + } + }; + + sas_verification_handler(client, sas, response_receiver).await; +} + + /// Actions related to verification that should be handled by the top-level app context. #[derive(Clone, Debug, Default)] pub enum VerificationAction { diff --git a/src/verification_modal.rs b/src/verification_modal.rs index b87912589..70f010d91 100644 --- a/src/verification_modal.rs +++ b/src/verification_modal.rs @@ -108,6 +108,9 @@ impl WidgetMatchEvent for VerificationModal { // `VerificationAction`s come from a background thread, so they are NOT widget actions. // Therefore, we cannot use `as_widget_action().cast()` to match them. if let Some(verification_action) = action.downcast_ref::() { + // Outgoing verification requests start with the accept button hidden + // since we're still in the waiting state then, so show it now + accept_button.set_visible(cx, true); match verification_action { VerificationAction::RequestCancelled(cancel_info) => { self.label(cx, ids!(body)).set_text( @@ -274,27 +277,31 @@ impl VerificationModal { ) { log!("Initializing verification modal with state: {:?}", state); let request = &state.request; - let prompt_text = if request.is_self_verification() { + // `we_started` means this is an outgoing request we just sent, so we don't need + // to accept it, but rather just wait for another device to accept & respond. + let we_started = request.we_started(); + let prompt_text = if we_started { + Cow::from("Send a verification request to your other logged-in devices.\n\n\ + Accept it on one of those devices to continue verifying this device.") + } else if request.is_self_verification() { Cow::from("Do you wish to verify your own device?") + } else if let Some(room_id) = request.room_id() { + format!("Do you wish to verify user {} in room {}?", + request.other_user_id(), + room_id, + ).into() } else { - if let Some(room_id) = request.room_id() { - format!("Do you wish to verify user {} in room {}?", - request.other_user_id(), - room_id, - ).into() - } else { - format!("Do you wish to verify user {}?", - request.other_user_id() - ).into() - } + format!("Do you wish to verify user {}?", + request.other_user_id() + ).into() }; self.label(cx, ids!(body)).set_text(cx, &prompt_text); let accept_button = self.button(cx, ids!(accept_button)); let cancel_button = self.button(cx, ids!(cancel_button)); accept_button.set_text(cx, "Yes"); - accept_button.set_enabled(cx, true); - accept_button.set_visible(cx, true); + accept_button.set_enabled(cx, !we_started); + accept_button.set_visible(cx, !we_started); cancel_button.set_text(cx, "Cancel"); cancel_button.set_enabled(cx, true); cancel_button.set_visible(cx, true);