From 5f54237f4f8a3349824fe04debe6622ff242c2bf Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 16 Oct 2025 14:50:34 +0200 Subject: [PATCH] feat(ffi): add bindings for logging in by generating a QR code on the new device Signed-off-by: Johannes Marbach --- bindings/matrix-sdk-ffi/CHANGELOG.md | 4 + bindings/matrix-sdk-ffi/src/client.rs | 44 +--- bindings/matrix-sdk-ffi/src/qr_code.rs | 208 +++++++++++++++++- .../src/authentication/oauth/qrcode/login.rs | 4 +- .../src/authentication/oauth/qrcode/mod.rs | 3 +- 5 files changed, 221 insertions(+), 42 deletions(-) diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 1933952e3..471e9872c 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -23,6 +23,10 @@ All notable changes to this project will be documented in this file. ([#5760](https://github.com/matrix-org/matrix-rust-sdk/pull/5760)) - Add `Room::subscribe_to_send_queue_updates` to observe room send queue updates. ([#5761](https://github.com/matrix-org/matrix-rust-sdk/pull/5761)) +- `Client::login_with_qr_code` now returns a handler that allows performing the flow with either the + current device scanning or generating the QR code. Additionally, new errors `HumanQrLoginError::CheckCodeAlreadySent` + and `HumanQrLoginError::CheckCodeCannotBeSent` were added. + ([#5786](https://github.com/matrix-org/matrix-rust-sdk/pull/5786)) ### Features: diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 171ecaf36..0713134b9 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -102,7 +102,7 @@ use crate::{ encryption::Encryption, notification::NotificationClient, notification_settings::NotificationSettings, - qr_code::{HumanQrLoginError, QrCodeData, QrLoginProgressListener}, + qr_code::LoginWithQrCodeHandler, room::{RoomHistoryVisibility, RoomInfoListener}, room_directory_search::RoomDirectorySearch, room_preview::RoomPreview, @@ -543,43 +543,17 @@ impl Client { Ok(()) } - /// Log in using the provided [`QrCodeData`]. The `Client` must be built - /// by providing [`QrCodeData::server_name`] as the server name for this - /// login to succeed. + /// Log in using a QR code. /// - /// This method uses the login mechanism described in [MSC4108]. As such - /// this method requires OAuth 2.0 support as well as sliding sync support. + /// # Arguments /// - /// The usage of the progress_listener is required to transfer the - /// [`CheckCode`] to the existing client. - /// - /// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108 - pub async fn login_with_qr_code( + /// * `oidc_configuration` - The data to restore or register the client with + /// the server. + pub fn login_with_qr_code( self: Arc, - qr_code_data: &QrCodeData, - oidc_configuration: &OidcConfiguration, - progress_listener: Box, - ) -> Result<(), HumanQrLoginError> { - let registration_data = oidc_configuration - .registration_data() - .map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?; - - let oauth = self.inner.oauth(); - let login = oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code_data.inner); - - let mut progress = login.subscribe_to_progress(); - - // We create this task, which will get cancelled once it's dropped, just in case - // the progress stream doesn't end. - let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { - while let Some(state) = progress.next().await { - progress_listener.on_update(state.into()); - } - })); - - login.await?; - - Ok(()) + oidc_configuration: OidcConfiguration, + ) -> LoginWithQrCodeHandler { + LoginWithQrCodeHandler::new(self.inner.oauth(), oidc_configuration) } /// Restores the client from a `Session`. diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index 3d9dbe6cf..c375111c0 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -1,12 +1,130 @@ use std::sync::Arc; use matrix_sdk::{ - authentication::oauth::qrcode::{ - self, DeviceCodeErrorResponseType, LoginFailureReason, QrProgress, + authentication::oauth::{ + qrcode::{ + self, CheckCodeSender as SdkCheckCodeSender, CheckCodeSenderError, + DeviceCodeErrorResponseType, GeneratedQrProgress, LoginFailureReason, QrProgress, + }, + OAuth, }, crypto::types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData}, }; -use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm}; +use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm}; + +use crate::{ + authentication::OidcConfiguration, runtime::get_runtime_handle, task_handle::TaskHandle, +}; + +/// Handler for logging in with a QR code. +#[derive(uniffi::Object)] +pub struct LoginWithQrCodeHandler { + oauth: OAuth, + oidc_configuration: OidcConfiguration, +} + +impl LoginWithQrCodeHandler { + pub(crate) fn new(oauth: OAuth, oidc_configuration: OidcConfiguration) -> Self { + Self { oauth, oidc_configuration } + } +} + +#[matrix_sdk_ffi_macros::export] +impl LoginWithQrCodeHandler { + /// This method allows you to log in with a scanned QR code. + /// + /// The existing device needs to display the QR code which this device can + /// scan, call this method and handle its progress updates to log in. + /// + /// For the login to succeed, the [`Client`] associated with the + /// [`LoginWithQrCodeHandler`] must have been built with + /// [`QrCodeData::server_name`] as the server name. + /// + /// This method uses the login mechanism described in [MSC4108]. As such, + /// it requires OAuth 2.0 support as well as Sliding Sync support. + /// + /// For the reverse flow where this device generates the QR code for the + /// existing device to scan, use [`LoginWithQrCodeHandler::generate`]. + /// + /// # Arguments + /// + /// * `qr_code_data` - The [`QrCodeData`] scanned from the QR code. + /// * `progress_listener` - A progress listener that must also be used to + /// transfer the [`CheckCode`] to the existing device. + /// + /// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108 + pub async fn scan( + self: Arc, + qr_code_data: &QrCodeData, + progress_listener: Box, + ) -> Result<(), HumanQrLoginError> { + let registration_data = self + .oidc_configuration + .registration_data() + .map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?; + + let login = + self.oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code_data.inner); + + let mut progress = login.subscribe_to_progress(); + + // We create this task, which will get cancelled once it's dropped, just in case + // the progress stream doesn't end. + let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { + while let Some(state) = progress.next().await { + progress_listener.on_update(state.into()); + } + })); + + login.await?; + + Ok(()) + } + + /// This method allows you to log in by generating a QR code. + /// + /// This device needs to call this method and handle its progress updates to + /// generate a QR code which the existing device can scan and grant the + /// log in. + /// + /// This method uses the login mechanism described in [MSC4108]. As such, + /// it requires OAuth 2.0 support as well as Sliding Sync support. + /// + /// For the reverse flow where the existing device generates the QR code + /// for this device to scan, use [`LoginWithQrCodeHandler::scan`]. + /// + /// # Arguments + /// + /// * `progress_listener` - A progress listener that must also be used to + /// obtain the [`QrCodeData`] and collect the [`CheckCode`] from the user. + /// + /// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108 + pub async fn generate( + self: Arc, + progress_listener: Box, + ) -> Result<(), HumanQrLoginError> { + let registration_data = self + .oidc_configuration + .registration_data() + .map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?; + + let login = self.oauth.login_with_qr_code(Some(®istration_data)).generate(); + + let mut progress = login.subscribe_to_progress(); + + // We create this task, which will get cancelled once it's dropped, just in case + // the progress stream doesn't end. + let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move { + while let Some(state) = progress.next().await { + progress_listener.on_update(state.into()); + } + })); + + login.await?; + + Ok(()) + } +} /// Data for the QR code login mechanism. /// @@ -71,6 +189,10 @@ pub enum HumanQrLoginError { OidcMetadataInvalid, #[error("The other device is not signed in and as such can't sign in other devices.")] OtherDeviceNotSignedIn, + #[error("The check code was already sent.")] + CheckCodeAlreadySent, + #[error("The check code could not be sent.")] + CheckCodeCannotBeSent, } impl From for HumanQrLoginError { @@ -122,7 +244,17 @@ impl From for HumanQrLoginError { } } -/// Enum describing the progress of the QR-code login. +impl From for HumanQrLoginError { + fn from(value: CheckCodeSenderError) -> Self { + match value { + CheckCodeSenderError::AlreadySent => HumanQrLoginError::CheckCodeAlreadySent, + CheckCodeSenderError::CannotSend => HumanQrLoginError::CheckCodeCannotBeSent, + } + } +} + +/// Enum describing the progress of logging in by scanning a QR code that was +/// generated on an existing device. #[derive(Debug, Default, Clone, uniffi::Enum)] pub enum QrLoginProgress { /// The login process is starting. @@ -172,3 +304,71 @@ impl From> for QrLoginProgress { } } } + +/// Enum describing the progress of logging in by generating a QR code and +/// having an existing device scan it. +#[derive(Debug, Default, Clone, uniffi::Enum)] +pub enum GeneratedQrLoginProgress { + /// The login process is starting. + #[default] + Starting, + /// We have established the secure channel and now need to display the + /// QR code so that the existing device can scan it. + QrReady { qr_code: Arc }, + /// The existing device has scanned the QR code and is displaying the + /// checkcode. We now need to ask the user to enter the checkcode so that + /// we can verify that the channel is indeed secure. + QrScanned { check_code_sender: Arc }, + /// We are waiting for the login and for the OAuth 2.0 authorization server + /// to give us an access token. + WaitingForToken { user_code: String }, + /// We are syncing secrets. + SyncingSecrets, + /// The login has successfully finished. + Done, +} + +#[matrix_sdk_ffi_macros::export(callback_interface)] +pub trait GeneratedQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm { + fn on_update(&self, state: GeneratedQrLoginProgress); +} + +impl From> for GeneratedQrLoginProgress { + fn from(value: qrcode::LoginProgress) -> Self { + use qrcode::LoginProgress; + + match value { + LoginProgress::Starting => Self::Starting, + LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(inner)) => { + Self::QrReady { qr_code: Arc::new(QrCodeData { inner }) } + } + LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(inner)) => { + Self::QrScanned { check_code_sender: Arc::new(CheckCodeSender { inner }) } + } + LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code }, + LoginProgress::SyncingSecrets => Self::SyncingSecrets, + LoginProgress::Done => Self::Done, + } + } +} + +#[derive(Debug, uniffi::Object)] +/// Used to pass back the [`CheckCode`] entered by the user to verify that the +/// secure channel is indeed secure. +pub struct CheckCodeSender { + inner: SdkCheckCodeSender, +} + +#[matrix_sdk_ffi_macros::export] +impl CheckCodeSender { + /// Send the [`CheckCode`]. + /// + /// Calling this method more than once will result in an error. + /// + /// # Arguments + /// + /// * `check_code` - The check code in digits representation. + pub async fn send(&self, code: u8) -> Result<(), HumanQrLoginError> { + self.inner.send(code).await.map_err(HumanQrLoginError::from) + } +} diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index 05eaf9173..64bee7dec 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -289,8 +289,8 @@ pub enum GeneratedQrProgress { QrScanned(CheckCodeSender), } -/// Used to send the [`CheckCode`] to the new device that generated the -/// QR code. +/// Used to pass back the [`CheckCode`] entered by the user to verify that the +/// secure channel is indeed secure. #[derive(Clone, Debug)] pub struct CheckCodeSender { inner: Arc>>>, diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index 543e94e49..9f7a9f66b 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -42,7 +42,8 @@ mod secure_channel; pub use self::{ login::{ - GeneratedQrProgress, LoginProgress, LoginWithGeneratedQrCode, LoginWithQrCode, QrProgress, + CheckCodeSender, CheckCodeSenderError, GeneratedQrProgress, LoginProgress, + LoginWithGeneratedQrCode, LoginWithQrCode, QrProgress, }, messages::{LoginFailureReason, LoginProtocolType, QrAuthMessage}, };