Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/settings/account_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions src/sliding_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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 {
Expand Down
99 changes: 98 additions & 1 deletion src/verification.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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),
Expand Down Expand Up @@ -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::<VerificationUserResponse>();
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 {
Expand Down
33 changes: 20 additions & 13 deletions src/verification_modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<VerificationAction>() {
// 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(
Expand Down Expand Up @@ -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);
Expand Down
Loading