Skip to content

feat(ffi): add support for starting and responding to user verification requests #4618

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 6, 2025
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
41 changes: 32 additions & 9 deletions bindings/matrix-sdk-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ use matrix_sdk::{
user_directory::search_users,
},
events::{
room::{avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent},
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
room::{
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent,
message::MessageType,
},
AnyInitialStateEvent, InitialStateEvent,
},
serde::Raw,
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
Expand All @@ -53,10 +56,12 @@ use ruma::{
},
events::{
ignored_user_list::IgnoredUserListEventContent,
key::verification::request::ToDeviceKeyVerificationRequestEvent,
room::{
join_rules::{
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
},
message::OriginalSyncRoomMessageEvent,
power_levels::RoomPowerLevelsEventContent,
},
GlobalAccountDataEventType,
Expand Down Expand Up @@ -204,12 +209,27 @@ impl Client {
tokio::sync::RwLock<Option<SessionVerificationController>>,
> = Default::default();
let controller = session_verification_controller.clone();
sdk_client.add_event_handler(
move |event: ToDeviceKeyVerificationRequestEvent| async move {
if let Some(session_verification_controller) = &*controller.clone().read().await {
session_verification_controller
.process_incoming_verification_request(
&event.sender,
event.content.transaction_id,
)
.await;
}
},
);

sdk_client.add_event_handler(move |ev: AnyToDeviceEvent| async move {
if let Some(session_verification_controller) = &*controller.clone().read().await {
session_verification_controller.process_to_device_message(ev).await;
} else {
debug!("received to-device message, but verification controller isn't ready");
let controller = session_verification_controller.clone();
sdk_client.add_event_handler(move |event: OriginalSyncRoomMessageEvent| async move {
if let MessageType::VerificationRequest(_) = &event.content.msgtype {
if let Some(session_verification_controller) = &*controller.clone().read().await {
session_verification_controller
.process_incoming_verification_request(&event.sender, event.event_id)
.await;
}
}
});

Expand Down Expand Up @@ -777,8 +797,11 @@ impl Client {
.await?
.context("Failed retrieving user identity")?;

let session_verification_controller =
SessionVerificationController::new(self.inner.encryption(), user_identity);
let session_verification_controller = SessionVerificationController::new(
self.inner.encryption(),
user_identity,
self.inner.account(),
);

*self.session_verification_controller.write().await =
Some(session_verification_controller.clone());
Expand Down
6 changes: 3 additions & 3 deletions bindings/matrix-sdk-ffi/src/room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,13 +252,13 @@ impl Room {
}

pub async fn member(&self, user_id: String) -> Result<RoomMember, ClientError> {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let user_id = UserId::parse(&*user_id)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a point in removing these? Perhaps it would make sense to have a helper that always applies this context to the UserId parsing step instead of just removing this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just looked at where UserId::parse was used and noticed there's significantly more without a context than with. Figured I might as well make it consistent.

I'm not sure it's worth wrapping it, I don't think I've ever seen one of these in practice and even if it were to happen the underlying ruma error is clear enough.

let member = self.inner.get_member(&user_id).await?.context("User not found")?;
Ok(member.try_into().context("Unknown state membership")?)
}

pub async fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let user_id = UserId::parse(&*user_id)?;
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
let avatar_url_string = member.avatar_url().map(|m| m.to_string());
Ok(avatar_url_string)
Expand All @@ -268,7 +268,7 @@ impl Room {
&self,
user_id: String,
) -> Result<Option<String>, ClientError> {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let user_id = UserId::parse(&*user_id)?;
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
let avatar_url_string = member.display_name().map(|m| m.to_owned());
Ok(avatar_url_string)
Expand Down
144 changes: 92 additions & 52 deletions bindings/matrix-sdk-ffi/src/session_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ use matrix_sdk::{
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
Encryption,
},
ruma::events::{key::verification::VerificationMethod, AnyToDeviceEvent},
ruma::events::key::verification::VerificationMethod,
Account,
};
use ruma::UserId;
use tracing::{error, info};
use tracing::error;

use super::RUNTIME;
use crate::{error::ClientError, utils::Timestamp};
Expand Down Expand Up @@ -42,9 +43,10 @@ pub enum SessionVerificationData {
#[derive(Debug, uniffi::Record)]
pub struct SessionVerificationRequestDetails {
sender_id: String,
sender_display_name: Option<String>,
flow_id: String,
device_id: String,
display_name: Option<String>,
device_display_name: Option<String>,
/// First time this device was seen in milliseconds since epoch.
first_seen_timestamp: Timestamp,
}
Expand All @@ -66,6 +68,7 @@ pub type Delegate = Arc<RwLock<Option<Box<dyn SessionVerificationControllerDeleg
pub struct SessionVerificationController {
encryption: Encryption,
user_identity: UserIdentity,
account: Account,
delegate: Delegate,
verification_request: Arc<RwLock<Option<VerificationRequest>>>,
sas_verification: Arc<RwLock<Option<SasVerification>>>,
Expand Down Expand Up @@ -94,15 +97,7 @@ impl SessionVerificationController {
.await
.ok_or(ClientError::new("Unknown session verification request"))?;

*self.verification_request.write().unwrap() = Some(verification_request.clone());

RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));

Ok(())
self.set_ongoing_verification_request(verification_request)
}

/// Accept the previously acknowledged verification request
Expand All @@ -118,23 +113,39 @@ impl SessionVerificationController {
}

/// Request verification for the current device
pub async fn request_verification(&self) -> Result<(), ClientError> {
pub async fn request_device_verification(&self) -> Result<(), ClientError> {
let methods = vec![VerificationMethod::SasV1];
let verification_request = self
.user_identity
.request_verification_with_methods(methods)
.await
.map_err(anyhow::Error::from)?;

*self.verification_request.write().unwrap() = Some(verification_request.clone());
self.set_ongoing_verification_request(verification_request)
}

RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
/// Request verification for the given user
pub async fn request_user_verification(&self, user_id: String) -> Result<(), ClientError> {
let user_id = UserId::parse(user_id)?;

Ok(())
let user_identity = self
.encryption
.get_user_identity(&user_id)
.await?
.ok_or(ClientError::new("Unknown user identity"))?;

if user_identity.is_verified() {
return Err(ClientError::new("User is already verified"));
}

let methods = vec![VerificationMethod::SasV1];

let verification_request = user_identity
.request_verification_with_methods(methods)
.await
.map_err(anyhow::Error::from)?;

self.set_ongoing_verification_request(verification_request)
}

/// Transition the current verification request into a SAS verification
Expand Down Expand Up @@ -202,50 +213,79 @@ impl SessionVerificationController {
}

impl SessionVerificationController {
pub(crate) fn new(encryption: Encryption, user_identity: UserIdentity) -> Self {
pub(crate) fn new(
encryption: Encryption,
user_identity: UserIdentity,
account: Account,
) -> Self {
SessionVerificationController {
encryption,
user_identity,
account,
delegate: Arc::new(RwLock::new(None)),
verification_request: Arc::new(RwLock::new(None)),
sas_verification: Arc::new(RwLock::new(None)),
}
}

pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) {
if let AnyToDeviceEvent::KeyVerificationRequest(event) = event {
info!("Received verification request: {:}", event.sender);

let Some(request) = self
.encryption
.get_verification_request(&event.sender, &event.content.transaction_id)
.await
else {
error!("Failed retrieving verification request");
return;
};

if !request.is_self_verification() {
info!("Received non-self verification request. Ignoring.");
return;
}
/// Ask the controller to process an incoming request based on the sender
/// and flow identifier. It will fetch the request, verify that it's in the
/// correct state and then and notify the delegate.
pub(crate) async fn process_incoming_verification_request(
&self,
sender: &UserId,
flow_id: impl AsRef<str>,
) {
let Some(request) = self.encryption.get_verification_request(sender, flow_id).await else {
error!("Failed retrieving verification request");
return;
};

let VerificationRequestState::Requested { other_device_data, .. } = request.state() else {
error!("Received verification request event but the request is in the wrong state.");
return;
};

let VerificationRequestState::Requested { other_device_data, .. } = request.state()
else {
error!("Received key verification event but the request is in the wrong state.");
return;
};

if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
sender_id: request.other_user_id().into(),
flow_id: request.flow_id().into(),
device_id: other_device_data.device_id().into(),
display_name: other_device_data.display_name().map(str::to_string),
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
});
let Ok(user_profile) = self.account.fetch_user_profile_of(sender).await else {
error!("Failed fetching user profile for verification request");
return;
};

if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
sender_id: request.other_user_id().into(),
sender_display_name: user_profile.displayname,
flow_id: request.flow_id().into(),
device_id: other_device_data.device_id().into(),
device_display_name: other_device_data.display_name().map(str::to_string),
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
});
}
}

fn set_ongoing_verification_request(
&self,
verification_request: VerificationRequest,
) -> Result<(), ClientError> {
if let Some(ongoing_verification_request) =
self.verification_request.read().unwrap().clone()
{
if !ongoing_verification_request.is_done()
&& !ongoing_verification_request.is_cancelled()
{
return Err(ClientError::new("There is another verification flow ongoing."));
}
}

*self.verification_request.write().unwrap() = Some(verification_request.clone());

RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));

Ok(())
}

async fn listen_to_verification_request_changes(
Expand Down
37 changes: 35 additions & 2 deletions crates/matrix-sdk-base/src/latest_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use ruma::{
relation::RelationType,
room::{
member::{MembershipState, SyncRoomMemberEvent},
message::SyncRoomMessageEvent,
message::{MessageType, SyncRoomMessageEvent},
power_levels::RoomPowerLevels,
},
sticker::SyncStickerEvent,
Expand Down Expand Up @@ -67,8 +67,13 @@ pub fn is_suitable_for_latest_event<'a>(
match event {
// Suitable - we have an m.room.message that was not redacted or edited
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
// Check if this is a replacement for another message. If it is, ignore it
if let Some(original_message) = message.as_original() {
// Don't show incoming verification requests
if let MessageType::VerificationRequest(_) = original_message.content.msgtype {
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
}

// Check if this is a replacement for another message. If it is, ignore it
let is_replacement =
original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
if let Some(relation_type) = relates_to.rel_type() {
Expand Down Expand Up @@ -589,6 +594,34 @@ mod tests {
);
}

#[cfg(feature = "e2e-encryption")]
#[test]
fn test_verification_requests_are_unsuitable() {
use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id};

let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
content: RoomMessageEventContent::new(MessageType::VerificationRequest(
KeyVerificationRequestEventContent::new(
"body".to_owned(),
vec![],
device_id!("device_id").to_owned(),
user_id!("@user_id:example.com").to_owned(),
),
)),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));

assert_let!(
PossibleLatestEvent::NoUnsupportedMessageLikeType =
is_suitable_for_latest_event(&event, None)
);
}

#[test]
fn test_deserialize_latest_event() {
#[derive(Debug, serde::Serialize, serde::Deserialize)]
Expand Down
Loading