You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1083 lines
40 KiB
1083 lines
40 KiB
use futures_util::StreamExt; |
|
use gtk::{ |
|
glib, |
|
glib::{clone, closure_local}, |
|
prelude::*, |
|
subclass::prelude::*, |
|
}; |
|
use matrix_sdk::encryption::verification::{ |
|
CancelInfo, Emoji, QrVerification, QrVerificationData, QrVerificationState, SasState, |
|
SasVerification, Verification, VerificationRequest, VerificationRequestState, |
|
}; |
|
use qrcode::QrCode; |
|
use ruma::{ |
|
events::key::verification::{cancel::CancelCode, VerificationMethod, REQUEST_RECEIVED_TIMEOUT}, |
|
OwnedDeviceId, |
|
}; |
|
use tracing::{debug, error}; |
|
|
|
use super::{load_supported_verification_methods, VerificationKey}; |
|
use crate::{ |
|
components::QrCodeScanner, |
|
prelude::*, |
|
session::model::{Member, Membership, Room, User}, |
|
spawn, spawn_tokio, |
|
utils::BoundConstructOnlyObject, |
|
}; |
|
|
|
#[glib::flags(name = "VerificationSupportedMethods")] |
|
pub enum VerificationSupportedMethods { |
|
SAS = 0b0000_0001, |
|
QR_SHOW = 0b0000_0010, |
|
QR_SCAN = 0b0000_0100, |
|
} |
|
|
|
impl<'a> From<&'a [VerificationMethod]> for VerificationSupportedMethods { |
|
fn from(methods: &'a [VerificationMethod]) -> Self { |
|
let mut result = Self::empty(); |
|
|
|
for method in methods { |
|
match method { |
|
VerificationMethod::SasV1 => result.insert(Self::SAS), |
|
VerificationMethod::QrCodeScanV1 => result.insert(Self::QR_SCAN), |
|
VerificationMethod::QrCodeShowV1 => result.insert(Self::QR_SHOW), |
|
_ => {} |
|
} |
|
} |
|
|
|
result |
|
} |
|
} |
|
|
|
impl Default for VerificationSupportedMethods { |
|
fn default() -> Self { |
|
Self::empty() |
|
} |
|
} |
|
|
|
#[derive(Debug, Default, Eq, PartialEq, Clone, Copy, glib::Enum)] |
|
#[enum_type(name = "VerificationState")] |
|
pub enum VerificationState { |
|
/// We created and sent the request. |
|
/// |
|
/// We must wait for the other user/device to accept it. |
|
#[default] |
|
Created, |
|
/// The other user/device sent us a request. |
|
/// |
|
/// We should ask the user if they want to accept it. |
|
Requested, |
|
/// We support none of the other user's verification methods. |
|
NoSupportedMethods, |
|
/// The request was accepted. |
|
/// |
|
/// We should ask the user to choose a method. |
|
Ready, |
|
/// An SAS verification was started. |
|
/// |
|
/// We should show the emojis and ask the user to confirm that they match. |
|
SasConfirm, |
|
/// The user wants to scan a QR Code. |
|
QrScan, |
|
/// The user scanned a QR Code. |
|
QrScanned, |
|
/// Our QR Code was scanned. |
|
/// |
|
/// We should ask the user to confirm that the QR Code was scanned |
|
/// successfully. |
|
QrConfirm, |
|
/// The verification was successful. |
|
Done, |
|
/// The verification was cancelled. |
|
Cancelled, |
|
/// The verification was automatically dismissed. |
|
/// |
|
/// Happens when a received request is not accepted by us after 2 minutes. |
|
Dismissed, |
|
/// The verification was happening in-room but the room was left. |
|
RoomLeft, |
|
/// An unexpected error happened. |
|
Error, |
|
} |
|
|
|
mod imp { |
|
use std::{ |
|
cell::{Cell, OnceCell, RefCell}, |
|
marker::PhantomData, |
|
sync::LazyLock, |
|
}; |
|
|
|
use glib::subclass::Signal; |
|
|
|
use super::*; |
|
|
|
#[derive(Default, glib::Properties)] |
|
#[properties(wrapper_type = super::IdentityVerification)] |
|
pub struct IdentityVerification { |
|
/// The SDK's verification request. |
|
request: OnceCell<VerificationRequest>, |
|
request_changes_abort_handle: RefCell<Option<tokio::task::AbortHandle>>, |
|
/// The SDK's verification, if one was started. |
|
verification: RefCell<Option<Verification>>, |
|
verification_changes_abort_handle: RefCell<Option<tokio::task::AbortHandle>>, |
|
/// The user to verify. |
|
#[property(get, set = Self::set_user, construct_only)] |
|
user: BoundConstructOnlyObject<User>, |
|
/// The room of this verification, if any. |
|
#[property(get, set = Self::set_room, construct_only)] |
|
room: glib::WeakRef<Room>, |
|
membership_handler: RefCell<Option<glib::SignalHandlerId>>, |
|
/// The state of this verification |
|
#[property(get, set = Self::set_state, construct_only, builder(VerificationState::default()))] |
|
state: Cell<VerificationState>, |
|
/// Whether the verification request was accepted. |
|
/// |
|
/// It means that the verification reached at least the `Ready` state. |
|
#[property(get)] |
|
was_accepted: Cell<bool>, |
|
/// Whether this verification is finished. |
|
#[property(get = Self::is_finished)] |
|
is_finished: PhantomData<bool>, |
|
/// The supported methods of the verification request. |
|
#[property(get = Self::supported_methods, type = VerificationSupportedMethods)] |
|
supported_methods: RefCell<Vec<VerificationMethod>>, |
|
/// The flow ID of this verification. |
|
#[property(get = Self::flow_id)] |
|
flow_id: PhantomData<String>, |
|
/// The time and date when the verification request was received. |
|
#[property(get)] |
|
received_time: OnceCell<glib::DateTime>, |
|
received_timeout_source: RefCell<Option<glib::SourceId>>, |
|
/// The display name of this verification. |
|
#[property(get = Self::display_name)] |
|
display_name: PhantomData<String>, |
|
/// The QR Code, if the `QrCodeShowV1` method is supported. |
|
pub(super) qr_code: RefCell<Option<QrCode>>, |
|
/// The QR code scanner, if the user wants to scan a QR Code and we |
|
/// have access to the camera. |
|
#[property(get)] |
|
pub(super) qrcode_scanner: RefCell<Option<QrCodeScanner>>, |
|
/// Whether this verification was viewed by the user. |
|
#[property(get, set = Self::set_was_viewed, explicit_notify)] |
|
was_viewed: Cell<bool>, |
|
} |
|
|
|
#[glib::object_subclass] |
|
impl ObjectSubclass for IdentityVerification { |
|
const NAME: &'static str = "IdentityVerification"; |
|
type Type = super::IdentityVerification; |
|
} |
|
|
|
#[glib::derived_properties] |
|
impl ObjectImpl for IdentityVerification { |
|
fn signals() -> &'static [Signal] { |
|
static SIGNALS: LazyLock<Vec<Signal>> = LazyLock::new(|| { |
|
vec![ |
|
// The SAS data changed. |
|
Signal::builder("sas-data-changed").build(), |
|
// The cancel info changed. |
|
Signal::builder("cancel-info-changed").build(), |
|
// The verification has been replaced by a new one. |
|
Signal::builder("replaced") |
|
.param_types([super::IdentityVerification::static_type()]) |
|
.build(), |
|
// The verification is done, but has not changed its state yes. |
|
// |
|
// Return `glib::Propagation::Stop` in a signal handler to prevent the state |
|
// from changing to `VerificationState::Done`. Can be used to replace the last |
|
// screen of `IdentityVerificationView`. |
|
Signal::builder("done").return_type::<bool>().build(), |
|
// The verification can be dismissed. |
|
Signal::builder("dismiss").build(), |
|
// The verification should be removed from the verification list. |
|
Signal::builder("remove-from-list").build(), |
|
] |
|
}); |
|
SIGNALS.as_ref() |
|
} |
|
|
|
fn dispose(&self) { |
|
if let Some(handler) = self.membership_handler.take() { |
|
if let Some(room) = self.room.upgrade() { |
|
room.own_member().disconnect(handler); |
|
} |
|
} |
|
if let Some(handle) = self.request_changes_abort_handle.take() { |
|
handle.abort(); |
|
} |
|
if let Some(handle) = self.verification_changes_abort_handle.take() { |
|
handle.abort(); |
|
} |
|
if let Some(source) = self.received_timeout_source.take() { |
|
source.remove(); |
|
} |
|
|
|
let request = self.request().clone(); |
|
if !request.is_done() && !request.is_passive() && !request.is_cancelled() { |
|
spawn_tokio!(async move { |
|
if let Err(error) = request.cancel().await { |
|
error!("Could not cancel verification request on dispose: {error}"); |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
|
|
impl IdentityVerification { |
|
/// Set the SDK's verification request. |
|
pub(super) async fn set_request(&self, request: VerificationRequest) { |
|
let request = self.request.get_or_init(|| request); |
|
|
|
let Ok(datetime) = glib::DateTime::now_local() else { |
|
error!("Could not get current GDateTime"); |
|
return; |
|
}; |
|
|
|
// Set up the timeout if we received the request and it is not accepted yet. |
|
if matches!(request.state(), VerificationRequestState::Requested { .. }) { |
|
let source_id = glib::timeout_add_local_once( |
|
REQUEST_RECEIVED_TIMEOUT, |
|
clone!( |
|
#[weak(rename_to = imp)] |
|
self, |
|
move || { |
|
imp.received_timeout_source.take(); |
|
|
|
imp.set_state(VerificationState::Dismissed); |
|
imp.obj().dismiss(); |
|
} |
|
), |
|
); |
|
self.received_timeout_source.replace(Some(source_id)); |
|
} |
|
|
|
self.received_time |
|
.set(datetime) |
|
.expect("received time should be uninitialized"); |
|
|
|
let obj_weak = glib::SendWeakRef::from(self.obj().downgrade()); |
|
let fut = request.changes().for_each(move |state| { |
|
let obj_weak = obj_weak.clone(); |
|
async move { |
|
let ctx = glib::MainContext::default(); |
|
ctx.spawn(async move { |
|
spawn!(async move { |
|
if let Some(obj) = obj_weak.upgrade() { |
|
obj.imp().handle_request_state(state).await; |
|
} |
|
}); |
|
}); |
|
} |
|
}); |
|
|
|
self.request_changes_abort_handle |
|
.replace(Some(spawn_tokio!(fut).abort_handle())); |
|
|
|
let state = request.state(); |
|
self.handle_request_state(state).await; |
|
} |
|
|
|
/// The SDK's verification request. |
|
pub(super) fn request(&self) -> &VerificationRequest { |
|
self.request.get().expect("request should be initialized") |
|
} |
|
|
|
/// Set the user to verify. |
|
fn set_user(&self, user: User) { |
|
let mut handlers = Vec::new(); |
|
|
|
// If the user is a room member, it means it's an in-room verification, we need |
|
// to keep track of their name since it's used as the display name. |
|
if user.is::<Member>() { |
|
let obj = self.obj(); |
|
let display_name_handler = user.connect_display_name_notify(clone!( |
|
#[weak] |
|
obj, |
|
move |_| { |
|
obj.notify_display_name(); |
|
} |
|
)); |
|
handlers.push(display_name_handler); |
|
} |
|
|
|
self.user.set(user, handlers); |
|
} |
|
|
|
/// Set the room of the verification, if any. |
|
fn set_room(&self, room: Option<&Room>) { |
|
let Some(room) = room else { |
|
// Nothing to do if there is no room. |
|
return; |
|
}; |
|
|
|
let handler = room.own_member().connect_membership_notify(clone!( |
|
#[weak(rename_to = imp)] |
|
self, |
|
move |own_member| { |
|
if matches!(own_member.membership(), Membership::Leave | Membership::Ban) { |
|
// If the user is not in the room anymore, nothing can be done with this |
|
// verification. |
|
imp.set_state(VerificationState::RoomLeft); |
|
|
|
if let Some(handler) = imp.membership_handler.take() { |
|
own_member.disconnect(handler); |
|
} |
|
} |
|
} |
|
)); |
|
self.membership_handler.replace(Some(handler)); |
|
|
|
self.room.set(Some(room)); |
|
} |
|
|
|
/// Set the state of this verification. |
|
pub(super) fn set_state(&self, state: VerificationState) { |
|
if self.state.get() == state { |
|
return; |
|
} |
|
let obj = self.obj(); |
|
|
|
if state == VerificationState::Done { |
|
let ret = obj.emit_by_name::<bool>("done", &[]); |
|
if glib::Propagation::from(ret).is_stop() { |
|
return; |
|
} |
|
} else if state != VerificationState::QrScan && self.qrcode_scanner.take().is_some() { |
|
obj.notify_qrcode_scanner(); |
|
} |
|
|
|
self.state.set(state); |
|
|
|
obj.notify_state(); |
|
|
|
if self.is_finished() { |
|
obj.notify_is_finished(); |
|
} |
|
} |
|
|
|
/// Whether this verification is finished. |
|
fn is_finished(&self) -> bool { |
|
matches!( |
|
self.state.get(), |
|
VerificationState::Cancelled |
|
| VerificationState::Dismissed |
|
| VerificationState::Done |
|
| VerificationState::Error |
|
| VerificationState::RoomLeft |
|
) |
|
} |
|
|
|
/// Set the supported methods of this verification. |
|
fn set_supported_methods(&self, supported_methods: Vec<VerificationMethod>) { |
|
if *self.supported_methods.borrow() == supported_methods { |
|
return; |
|
} |
|
|
|
self.supported_methods.replace(supported_methods); |
|
self.obj().notify_supported_methods(); |
|
} |
|
|
|
/// The supported methods of this verifications. |
|
fn supported_methods(&self) -> VerificationSupportedMethods { |
|
self.supported_methods.borrow().as_slice().into() |
|
} |
|
|
|
/// The display name of this verification request. |
|
fn display_name(&self) -> String { |
|
let user = self.user.obj(); |
|
|
|
if user.is_own_user() { |
|
// TODO: give this request a name based on the device |
|
"Login Request".to_string() |
|
} else { |
|
user.display_name() |
|
} |
|
} |
|
|
|
/// The flow ID of this verification request. |
|
fn flow_id(&self) -> String { |
|
self.request().flow_id().to_owned() |
|
} |
|
|
|
/// Set whether this verification was viewed by the user. |
|
fn set_was_viewed(&self, was_viewed: bool) { |
|
if !was_viewed { |
|
// The user cannot unview the verification. |
|
return; |
|
} |
|
|
|
self.was_viewed.set(was_viewed); |
|
self.obj().notify_was_viewed(); |
|
} |
|
|
|
/// Set whether this request was accepted. |
|
fn set_was_accepted(&self, was_accepted: bool) { |
|
if !was_accepted || self.was_accepted.get() { |
|
// The state cannot go backwards. |
|
return; |
|
} |
|
|
|
self.was_accepted.set(true); |
|
self.obj().notify_was_accepted(); |
|
} |
|
|
|
/// Handle a change in the request's state. |
|
async fn handle_request_state(&self, state: VerificationRequestState) { |
|
let request = self.request(); |
|
|
|
if !matches!(state, VerificationRequestState::Requested { .. }) { |
|
if let Some(source) = self.received_timeout_source.take() { |
|
source.remove(); |
|
} |
|
} |
|
if !matches!( |
|
state, |
|
VerificationRequestState::Created { .. } |
|
| VerificationRequestState::Requested { .. } |
|
) { |
|
self.set_was_accepted(true); |
|
} |
|
|
|
match state { |
|
VerificationRequestState::Created { .. } => {} |
|
VerificationRequestState::Requested { their_methods, .. } => { |
|
let our_methods = load_supported_verification_methods().await; |
|
let supported_methods = intersect_methods(our_methods, &their_methods); |
|
|
|
if supported_methods.is_empty() { |
|
self.set_state(VerificationState::NoSupportedMethods); |
|
} else { |
|
self.set_state(VerificationState::Requested); |
|
} |
|
} |
|
VerificationRequestState::Ready { |
|
their_methods, |
|
our_methods, |
|
.. |
|
} => { |
|
let mut supported_methods = intersect_methods(our_methods, &their_methods); |
|
|
|
// Remove the reciprocate method, it's not a flow in itself. |
|
let reciprocate_idx = |
|
supported_methods.iter().enumerate().find_map(|(idx, m)| { |
|
(*m == VerificationMethod::ReciprocateV1).then_some(idx) |
|
}); |
|
if let Some(idx) = reciprocate_idx { |
|
supported_methods.remove(idx); |
|
} |
|
|
|
// Check that we can get the QR Code, to avoid exposing the method if it doesn't |
|
// work. |
|
let show_qr_idx = supported_methods.iter().enumerate().find_map(|(idx, m)| { |
|
(*m == VerificationMethod::QrCodeShowV1).then_some(idx) |
|
}); |
|
if let Some(idx) = show_qr_idx { |
|
if !self.load_qr_code().await { |
|
supported_methods.remove(idx); |
|
} |
|
} |
|
|
|
if supported_methods.is_empty() { |
|
// This should not happen. |
|
error!("Invalid verification: no methods are supported by both sessions, cancelling…"); |
|
if self.obj().cancel().await.is_err() { |
|
self.set_state(VerificationState::NoSupportedMethods); |
|
} |
|
} else { |
|
self.set_supported_methods(supported_methods.clone()); |
|
|
|
if supported_methods.len() == 1 |
|
&& !request.we_started() |
|
&& supported_methods[0] == VerificationMethod::SasV1 |
|
{ |
|
// We only go forward for SAS, because QrCodeShow is the |
|
// same screen as the one to choose a method and we need |
|
// to tell the user we are going to need to access the |
|
// camera for QrCodeScan. |
|
if self.obj().start_sas().await.is_ok() { |
|
return; |
|
} |
|
} |
|
|
|
self.set_state(VerificationState::Ready); |
|
} |
|
} |
|
VerificationRequestState::Transitioned { verification } => { |
|
self.set_verification(verification).await; |
|
} |
|
VerificationRequestState::Done => { |
|
self.set_state(VerificationState::Done); |
|
} |
|
VerificationRequestState::Cancelled(info) => self.handle_cancelled_state(&info), |
|
} |
|
} |
|
|
|
/// Handle when the request was cancelled. |
|
fn handle_cancelled_state(&self, cancel_info: &CancelInfo) { |
|
debug!("Verification was cancelled: {cancel_info:?}"); |
|
let cancel_code = cancel_info.cancel_code(); |
|
|
|
if cancel_info.cancelled_by_us() && *cancel_code == CancelCode::User { |
|
// We should handle this already. |
|
return; |
|
} |
|
|
|
if *cancel_code == CancelCode::Accepted && !self.was_viewed.get() { |
|
// We can safely remove it. |
|
self.obj().dismiss(); |
|
return; |
|
} |
|
|
|
self.obj().emit_by_name::<()>("cancel-info-changed", &[]); |
|
self.set_state(VerificationState::Cancelled); |
|
} |
|
|
|
/// Set the SDK's Verification. |
|
async fn set_verification(&self, verification: Verification) { |
|
if let Some(handle) = self.verification_changes_abort_handle.take() { |
|
handle.abort(); |
|
} |
|
|
|
let obj_weak = glib::SendWeakRef::from(self.obj().downgrade()); |
|
let handle = match &verification { |
|
Verification::SasV1(sas_verification) => { |
|
let fut = sas_verification.changes().for_each(move |state| { |
|
let obj_weak = obj_weak.clone(); |
|
async move { |
|
let ctx = glib::MainContext::default(); |
|
ctx.spawn(async move { |
|
spawn!(async move { |
|
if let Some(obj) = obj_weak.upgrade() { |
|
obj.imp().handle_sas_verification_state(state).await; |
|
} |
|
}); |
|
}); |
|
} |
|
}); |
|
spawn_tokio!(fut).abort_handle() |
|
} |
|
Verification::QrV1(qr_verification) => { |
|
let fut = qr_verification.changes().for_each(move |state| { |
|
let obj_weak = obj_weak.clone(); |
|
async move { |
|
let ctx = glib::MainContext::default(); |
|
ctx.spawn(async move { |
|
spawn!(async move { |
|
if let Some(obj) = obj_weak.upgrade() { |
|
obj.imp().handle_qr_verification_state(state); |
|
} |
|
}); |
|
}); |
|
} |
|
}); |
|
spawn_tokio!(fut).abort_handle() |
|
} |
|
_ => { |
|
error!("We only support SAS and QR verification"); |
|
self.set_state(VerificationState::Error); |
|
return; |
|
} |
|
}; |
|
|
|
self.verification.replace(Some(verification.clone())); |
|
self.verification_changes_abort_handle.replace(Some(handle)); |
|
|
|
match verification { |
|
Verification::SasV1(sas_verification) => { |
|
self.handle_sas_verification_state(sas_verification.state()) |
|
.await; |
|
} |
|
Verification::QrV1(qr_verification) => { |
|
self.handle_qr_verification_state(qr_verification.state()); |
|
} |
|
_ => unreachable!(), |
|
} |
|
} |
|
|
|
/// Handle a change in the QR verification's state. |
|
fn handle_qr_verification_state(&self, state: QrVerificationState) { |
|
match state { |
|
QrVerificationState::Started |
|
| QrVerificationState::Confirmed |
|
| QrVerificationState::Reciprocated => {} |
|
QrVerificationState::Scanned => self.set_state(VerificationState::QrConfirm), |
|
QrVerificationState::Done { .. } => self.set_state(VerificationState::Done), |
|
QrVerificationState::Cancelled(info) => self.handle_cancelled_state(&info), |
|
} |
|
} |
|
|
|
/// The SDK's QR verification, if one was started. |
|
pub(super) fn qr_verification(&self) -> Option<QrVerification> { |
|
match self.verification.borrow().as_ref()? { |
|
Verification::QrV1(v) => Some(v.clone()), |
|
_ => None, |
|
} |
|
} |
|
|
|
/// Handle a change in the SAS verification's state. |
|
async fn handle_sas_verification_state(&self, state: SasState) { |
|
let Some(sas_verification) = self.sas_verification() else { |
|
return; |
|
}; |
|
|
|
match state { |
|
SasState::Created { .. } | SasState::Accepted { .. } | SasState::Confirmed => {} |
|
SasState::Started { .. } => { |
|
let handle = spawn_tokio!(async move { sas_verification.accept().await }); |
|
if let Err(error) = handle.await.expect("task was not aborted") { |
|
error!("Could not accept SAS verification: {error}"); |
|
self.set_state(VerificationState::Error); |
|
} |
|
} |
|
SasState::KeysExchanged { .. } => { |
|
self.obj().emit_by_name::<()>("sas-data-changed", &[]); |
|
self.set_state(VerificationState::SasConfirm); |
|
} |
|
SasState::Done { .. } => self.set_state(VerificationState::Done), |
|
SasState::Cancelled(info) => self.handle_cancelled_state(&info), |
|
} |
|
} |
|
|
|
/// The SDK's SAS verification, if one was started. |
|
pub(super) fn sas_verification(&self) -> Option<SasVerification> { |
|
match self.verification.borrow().as_ref()? { |
|
Verification::SasV1(v) => Some(v.clone()), |
|
_ => None, |
|
} |
|
} |
|
|
|
/// Try to load the QR Code. |
|
/// |
|
/// Return `true` if it was successfully loaded, `false` otherwise. |
|
async fn load_qr_code(&self) -> bool { |
|
let request = self.request().clone(); |
|
|
|
let handle = spawn_tokio!(async move { request.generate_qr_code().await }); |
|
|
|
let qr_verification = match handle.await.expect("task was not aborted") { |
|
Ok(Some(qr_verification)) => qr_verification, |
|
Ok(None) => { |
|
error!("Could not start QR verification generation: unknown reason"); |
|
return false; |
|
} |
|
Err(error) => { |
|
error!("Could not start QR verification generation: {error}"); |
|
return false; |
|
} |
|
}; |
|
|
|
match qr_verification.to_qr_code() { |
|
Ok(qr_code) => { |
|
self.qr_code.replace(Some(qr_code)); |
|
true |
|
} |
|
Err(error) => { |
|
error!("Could not generate verification QR code: {error}"); |
|
false |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
glib::wrapper! { |
|
/// An identity verification request. |
|
pub struct IdentityVerification(ObjectSubclass<imp::IdentityVerification>); |
|
} |
|
|
|
impl IdentityVerification { |
|
/// Construct a verification for the given request. |
|
pub async fn new(request: VerificationRequest, user: &User, room: Option<&Room>) -> Self { |
|
let obj = glib::Object::builder::<Self>() |
|
.property("user", user) |
|
.property("room", room) |
|
.build(); |
|
obj.imp().set_request(request).await; |
|
obj |
|
} |
|
|
|
/// The unique identifying key of this verification. |
|
pub(crate) fn key(&self) -> VerificationKey { |
|
VerificationKey::from_request(self.imp().request()) |
|
} |
|
|
|
/// Whether this is a self-verification. |
|
pub(crate) fn is_self_verification(&self) -> bool { |
|
self.imp().request().is_self_verification() |
|
} |
|
|
|
/// Whether we started this verification. |
|
pub(crate) fn started_by_us(&self) -> bool { |
|
self.imp().request().we_started() |
|
} |
|
|
|
/// The ID of the other device that is being verified. |
|
pub(crate) fn other_device_id(&self) -> Option<OwnedDeviceId> { |
|
let request_state = self.imp().request().state(); |
|
let other_device_data = match &request_state { |
|
VerificationRequestState::Requested { |
|
other_device_data, .. |
|
} |
|
| VerificationRequestState::Ready { |
|
other_device_data, .. |
|
} => other_device_data, |
|
VerificationRequestState::Transitioned { verification } => match verification { |
|
Verification::SasV1(sas) => sas.other_device(), |
|
Verification::QrV1(qr) => qr.other_device(), |
|
_ => None?, |
|
}, |
|
VerificationRequestState::Created { .. } |
|
| VerificationRequestState::Done |
|
| VerificationRequestState::Cancelled(_) => None?, |
|
}; |
|
|
|
Some(other_device_data.device_id().to_owned()) |
|
} |
|
|
|
/// Information about the verification cancellation, if any. |
|
pub(crate) fn cancel_info(&self) -> Option<CancelInfo> { |
|
self.imp().request().cancel_info() |
|
} |
|
|
|
/// Cancel the verification request. |
|
/// |
|
/// This can be used to decline the request or cancel it at any time. |
|
pub(crate) async fn cancel(&self) -> Result<(), matrix_sdk::Error> { |
|
let request = self.imp().request().clone(); |
|
|
|
if request.is_done() || request.is_passive() || request.is_cancelled() { |
|
return Err(matrix_sdk::Error::UnknownError( |
|
"Cannot cancel request that is already finished".into(), |
|
)); |
|
} |
|
|
|
let handle = spawn_tokio!(async move { request.cancel().await }); |
|
|
|
match handle.await.expect("task was not aborted") { |
|
Ok(()) => { |
|
self.dismiss(); |
|
Ok(()) |
|
} |
|
Err(error) => { |
|
error!("Could not cancel verification request: {error}"); |
|
Err(error) |
|
} |
|
} |
|
} |
|
|
|
/// Accept the verification request. |
|
pub(crate) async fn accept(&self) -> Result<(), ()> { |
|
let request = self.imp().request().clone(); |
|
|
|
let VerificationRequestState::Requested { their_methods, .. } = request.state() else { |
|
error!("Cannot accept verification that is not in the requested state"); |
|
return Err(()); |
|
}; |
|
let our_methods = load_supported_verification_methods().await; |
|
let methods = intersect_methods(our_methods, &their_methods); |
|
|
|
let handle = spawn_tokio!(async move { request.accept_with_methods(methods).await }); |
|
|
|
match handle.await.expect("task was not aborted") { |
|
Ok(()) => Ok(()), |
|
Err(error) => { |
|
error!("Could not accept verification request: {error}"); |
|
Err(()) |
|
} |
|
} |
|
} |
|
|
|
/// Go back to the state to choose a verification method. |
|
pub(crate) fn choose_method(&self) { |
|
self.imp().set_state(VerificationState::Ready); |
|
} |
|
|
|
/// Whether the current SAS verification supports emoji. |
|
pub(crate) fn sas_supports_emoji(&self) -> bool { |
|
self.imp() |
|
.sas_verification() |
|
.is_some_and(|v| v.supports_emoji()) |
|
} |
|
|
|
/// The list of emojis for the current SAS verification, if any. |
|
pub(crate) fn sas_emoji(&self) -> Option<[Emoji; 7]> { |
|
self.imp().sas_verification()?.emoji() |
|
} |
|
|
|
/// The list of decimals for the current SAS verification, if any. |
|
pub(crate) fn sas_decimals(&self) -> Option<(u16, u16, u16)> { |
|
self.imp().sas_verification()?.decimals() |
|
} |
|
|
|
/// The QR Code, if the `QrCodeShowV1` method is supported. |
|
pub(crate) fn qr_code(&self) -> Option<QrCode> { |
|
self.imp().qr_code.borrow().clone() |
|
} |
|
|
|
/// Whether we have the QR code. |
|
pub(crate) fn has_qr_code(&self) -> bool { |
|
self.imp().qr_code.borrow().is_some() |
|
} |
|
|
|
/// Start a QR Code scan. |
|
pub(crate) async fn start_qr_code_scan(&self) -> Result<(), ()> { |
|
let imp = self.imp(); |
|
|
|
match QrCodeScanner::new().await { |
|
Some(qrcode_scanner) => { |
|
imp.qrcode_scanner.replace(Some(qrcode_scanner)); |
|
self.notify_qrcode_scanner(); |
|
|
|
imp.set_state(VerificationState::QrScan); |
|
|
|
Ok(()) |
|
} |
|
None => Err(()), |
|
} |
|
} |
|
|
|
/// The QR Code was scanned. |
|
pub(crate) async fn qr_code_scanned(&self, data: QrVerificationData) -> Result<(), ()> { |
|
let imp = self.imp(); |
|
imp.set_state(VerificationState::QrScanned); |
|
let request = imp.request().clone(); |
|
|
|
let handle = spawn_tokio!(async move { request.scan_qr_code(data).await }); |
|
|
|
match handle.await.expect("task was not aborted") { |
|
Ok(Some(_)) => Ok(()), |
|
Ok(None) => { |
|
error!("Could not validate scanned verification QR code: unknown reason"); |
|
Err(()) |
|
} |
|
Err(error) => { |
|
error!("Could not validate scanned verification QR code: {error}"); |
|
Err(()) |
|
} |
|
} |
|
} |
|
|
|
/// Confirm that the QR Code was scanned by the other party. |
|
pub(crate) async fn confirm_qr_code_scanned(&self) -> Result<(), ()> { |
|
let Some(qr_verification) = self.imp().qr_verification() else { |
|
error!("Cannot confirm QR Code scan without an ongoing QR verification"); |
|
return Err(()); |
|
}; |
|
|
|
let handle = spawn_tokio!(async move { qr_verification.confirm().await }); |
|
|
|
match handle.await.expect("task was not aborted") { |
|
Ok(()) => Ok(()), |
|
Err(error) => { |
|
error!("Could not confirm scanned verification QR code: {error}"); |
|
Err(()) |
|
} |
|
} |
|
} |
|
|
|
/// Start a SAS verification. |
|
pub(crate) async fn start_sas(&self) -> Result<(), ()> { |
|
let request = self.imp().request().clone(); |
|
let handle = spawn_tokio!(async move { request.start_sas().await }); |
|
|
|
match handle.await.expect("task was not aborted") { |
|
Ok(Some(_)) => Ok(()), |
|
Ok(None) => { |
|
error!("Could not start SAS verification: unknown reason"); |
|
Err(()) |
|
} |
|
Err(error) => { |
|
error!("Could not start SAS verification: {error}"); |
|
Err(()) |
|
} |
|
} |
|
} |
|
|
|
/// The SAS data does not match. |
|
pub(crate) async fn sas_mismatch(&self) -> Result<(), ()> { |
|
let Some(sas_verification) = self.imp().sas_verification() else { |
|
error!("Cannot send SAS mismatch without an ongoing SAS verification"); |
|
return Err(()); |
|
}; |
|
|
|
let handle = spawn_tokio!(async move { sas_verification.mismatch().await }); |
|
|
|
match handle.await.expect("task was not aborted") { |
|
Ok(()) => Ok(()), |
|
Err(error) => { |
|
error!("Could not send SAS verification mismatch: {error}"); |
|
Err(()) |
|
} |
|
} |
|
} |
|
|
|
/// The SAS data matches. |
|
pub(crate) async fn sas_match(&self) -> Result<(), ()> { |
|
let Some(sas_verification) = self.imp().sas_verification() else { |
|
error!("Cannot send SAS match without an ongoing SAS verification"); |
|
return Err(()); |
|
}; |
|
|
|
let handle = spawn_tokio!(async move { sas_verification.confirm().await }); |
|
|
|
match handle.await.expect("task was not aborted") { |
|
Ok(()) => Ok(()), |
|
Err(error) => { |
|
error!("Could not send SAS verification match: {error}"); |
|
Err(()) |
|
} |
|
} |
|
} |
|
|
|
/// Restart this verification with a new one to the same user. |
|
pub(crate) async fn restart(&self) -> Result<Self, ()> { |
|
let user = self.user(); |
|
let verification_list = user.session().verification_list(); |
|
|
|
let new_user = (!self.is_self_verification()).then_some(user); |
|
let new_verification = verification_list.create(new_user).await?; |
|
|
|
self.emit_by_name::<()>("replaced", &[&new_verification]); |
|
|
|
// If we restart because an unexpected error happened, try to cancel it. |
|
if self.cancel().await.is_err() { |
|
self.dismiss(); |
|
} |
|
|
|
Ok(new_verification) |
|
} |
|
|
|
/// The verification can be dismissed. |
|
/// |
|
/// Also removes it from the verification list. |
|
pub(crate) fn dismiss(&self) { |
|
self.remove_from_list(); |
|
self.emit_by_name::<()>("dismiss", &[]); |
|
} |
|
|
|
/// The verification can be removed from the verification list. |
|
/// |
|
/// You will usually want to use [`IdentityVerification::dismiss()`] because |
|
/// the interface listens for the signal it emits, and it calls this method |
|
/// internally. |
|
pub(crate) fn remove_from_list(&self) { |
|
self.emit_by_name::<()>("remove-from-list", &[]); |
|
} |
|
|
|
/// Connect to the signal emitted when the SAS data changed. |
|
pub fn connect_sas_data_changed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId { |
|
self.connect_closure( |
|
"sas-data-changed", |
|
true, |
|
closure_local!(move |obj: Self| { |
|
f(&obj); |
|
}), |
|
) |
|
} |
|
|
|
/// Connect to the signal emitted when the cancel info changed. |
|
pub fn connect_cancel_info_changed<F: Fn(&Self) + 'static>( |
|
&self, |
|
f: F, |
|
) -> glib::SignalHandlerId { |
|
self.connect_closure( |
|
"cancel-info-changed", |
|
true, |
|
closure_local!(move |obj: Self| { |
|
f(&obj); |
|
}), |
|
) |
|
} |
|
|
|
/// Connect to the signal emitted when the verification has been replaced. |
|
/// |
|
/// The second parameter to the function is the new verification. |
|
pub fn connect_replaced<F: Fn(&Self, &Self) + 'static>(&self, f: F) -> glib::SignalHandlerId { |
|
self.connect_closure( |
|
"replaced", |
|
true, |
|
closure_local!(move |obj: Self, new_verification: Self| { |
|
f(&obj, &new_verification); |
|
}), |
|
) |
|
} |
|
|
|
/// Connect to the signal emitted when the verification is done, but its |
|
/// state does not reflect that yet. |
|
/// |
|
/// Return `glib::Propagation::Stop` in the signal handler to prevent the |
|
/// state from changing to `VerificationState::Done`. Can be used to replace |
|
/// the last screen of `IdentityVerificationView`. |
|
pub fn connect_done<F: Fn(&Self) -> glib::Propagation + 'static>( |
|
&self, |
|
f: F, |
|
) -> glib::SignalHandlerId { |
|
self.connect_closure( |
|
"done", |
|
true, |
|
closure_local!(move |obj: Self| { |
|
let ret = f(&obj); |
|
|
|
if ret.is_stop() { |
|
obj.stop_signal_emission_by_name("done"); |
|
} |
|
|
|
bool::from(ret) |
|
}), |
|
) |
|
} |
|
|
|
/// Connect to the signal emitted when the verification can be dismissed. |
|
pub fn connect_dismiss<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId { |
|
self.connect_closure( |
|
"dismiss", |
|
true, |
|
closure_local!(move |obj: Self| { |
|
f(&obj); |
|
}), |
|
) |
|
} |
|
|
|
/// Connect to the signal emitted when the verification can be removed from |
|
/// the verification list. |
|
pub(super) fn connect_remove_from_list<F: Fn(&Self) + 'static>( |
|
&self, |
|
f: F, |
|
) -> glib::SignalHandlerId { |
|
self.connect_closure( |
|
"remove-from-list", |
|
true, |
|
closure_local!(move |obj: Self| { |
|
f(&obj); |
|
}), |
|
) |
|
} |
|
} |
|
|
|
/// Get the intersection or our methods and their methods. |
|
fn intersect_methods( |
|
our_methods: Vec<VerificationMethod>, |
|
their_methods: &[VerificationMethod], |
|
) -> Vec<VerificationMethod> { |
|
let mut supported_methods = our_methods; |
|
|
|
supported_methods.retain(|m| match m { |
|
VerificationMethod::SasV1 => their_methods.contains(&VerificationMethod::SasV1), |
|
VerificationMethod::QrCodeScanV1 => { |
|
their_methods.contains(&VerificationMethod::QrCodeShowV1) |
|
&& their_methods.contains(&VerificationMethod::ReciprocateV1) |
|
} |
|
VerificationMethod::QrCodeShowV1 => { |
|
their_methods.contains(&VerificationMethod::QrCodeScanV1) |
|
&& their_methods.contains(&VerificationMethod::ReciprocateV1) |
|
} |
|
VerificationMethod::ReciprocateV1 => { |
|
(their_methods.contains(&VerificationMethod::QrCodeShowV1) |
|
|| their_methods.contains(&VerificationMethod::QrCodeScanV1)) |
|
&& their_methods.contains(&VerificationMethod::ReciprocateV1) |
|
} |
|
_ => false, |
|
}); |
|
|
|
supported_methods |
|
}
|
|
|