From b73bd05068404eda4298e02e5190c50f188c6bd9 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Fri, 24 Oct 2025 10:07:32 +0200 Subject: [PATCH 1/3] feat(testing): add mock for get device endpoint Signed-off-by: Johannes Marbach --- crates/matrix-sdk/src/test_utils/mocks/mod.rs | 16 ++++++++++++++++ .../src/test_json/api_responses.rs | 10 ++++++++++ testing/matrix-sdk-test/src/test_json/mod.rs | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index c71aa2ca05c..afcc79ad6f9 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -1427,6 +1427,12 @@ impl MatrixMockServer { self.mock_endpoint(mock, DevicesEndpoint).expect_default_access_token() } + /// Create a prebuilt mock for the endpoint used to query a single device. + pub fn mock_get_device(&self) -> MockEndpoint<'_, GetDeviceEndpoint> { + let mock = Mock::given(method("GET")).and(path_regex("/_matrix/client/v3/devices/.*")); + self.mock_endpoint(mock, GetDeviceEndpoint).expect_default_access_token() + } + /// Create a prebuilt mock for the endpoint used to search in the user /// directory. pub fn mock_user_directory(&self) -> MockEndpoint<'_, UserDirectoryEndpoint> { @@ -4139,6 +4145,16 @@ impl<'a> MockEndpoint<'a, DevicesEndpoint> { } } +/// A prebuilt mock for `GET /devices/{deviceId}` requests. +pub struct GetDeviceEndpoint; + +impl<'a> MockEndpoint<'a, GetDeviceEndpoint> { + /// Returns a successful response. + pub fn ok(self) -> MatrixMock<'a> { + self.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::DEVICE)) + } +} + /// A prebuilt mock for `POST /user_directory/search` requests. pub struct UserDirectoryEndpoint; diff --git a/testing/matrix-sdk-test/src/test_json/api_responses.rs b/testing/matrix-sdk-test/src/test_json/api_responses.rs index 6e726aa602d..0dc2429d270 100644 --- a/testing/matrix-sdk-test/src/test_json/api_responses.rs +++ b/testing/matrix-sdk-test/src/test_json/api_responses.rs @@ -25,6 +25,16 @@ pub static DEVICES: Lazy = Lazy::new(|| { }) }); +/// `GET /_matrix/client/v3/device/{deviceId}` +pub static DEVICE: Lazy = Lazy::new(|| { + json!({ + "device_id": "QBUAZIFURK", + "display_name": "android", + "last_seen_ip": "1.2.3.4", + "last_seen_ts": 1474491775024u64 + }) +}); + /// `GET /_matrix/client/v3/directory/room/{roomAlias}` pub static GET_ALIAS: Lazy = Lazy::new(|| { json!({ diff --git a/testing/matrix-sdk-test/src/test_json/mod.rs b/testing/matrix-sdk-test/src/test_json/mod.rs index dfe30ad7a28..285955ae1fa 100644 --- a/testing/matrix-sdk-test/src/test_json/mod.rs +++ b/testing/matrix-sdk-test/src/test_json/mod.rs @@ -18,7 +18,7 @@ pub mod sync; pub mod sync_events; pub use api_responses::{ - DEVICES, GET_ALIAS, KEYS_QUERY, KEYS_QUERY_TWO_DEVICES_ONE_SIGNED, KEYS_UPLOAD, LOGIN, + DEVICE, DEVICES, GET_ALIAS, KEYS_QUERY, KEYS_QUERY_TWO_DEVICES_ONE_SIGNED, KEYS_UPLOAD, LOGIN, LOGIN_RESPONSE_ERR, LOGIN_TYPES, LOGIN_WITH_DISCOVERY, LOGIN_WITH_REFRESH_TOKEN, NOT_FOUND, PUBLIC_ROOMS, PUBLIC_ROOMS_FINAL_PAGE, REFRESH_TOKEN, REFRESH_TOKEN_WITH_REFRESH_TOKEN, REGISTRATION_RESPONSE_ERR, UNKNOWN_TOKEN_SOFT_LOGOUT, VERSIONS, WELL_KNOWN, WHOAMI, From e08b8511fd112fc3cd31bae2664ec02cc758e8cf Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Fri, 24 Oct 2025 10:09:40 +0200 Subject: [PATCH 2/3] refactor(secure_channel): rename SecureChannel::new to SecureChannel::reciprocate and make it available outside of tests Signed-off-by: Johannes Marbach --- .../src/authentication/oauth/qrcode/login.rs | 6 +++--- .../authentication/oauth/qrcode/secure_channel.rs | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index 64bee7dec86..70b4af3cb06 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -666,7 +666,7 @@ mod test { server.mock_query_keys().ok().expect(1).named("query_keys").mount().await; let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let alice = SecureChannel::new(client, &rendezvous_server.homeserver_url) + let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url) .await .expect("Alice should be able to create a secure channel."); @@ -1043,7 +1043,7 @@ mod test { server.mock_who_am_i().ok().named("whoami").mount().await; let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let alice = SecureChannel::new(client, &rendezvous_server.homeserver_url) + let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url) .await .expect("Alice should be able to create a secure channel."); @@ -1325,7 +1325,7 @@ mod test { server.mock_who_am_i().ok().named("whoami").mount().await; let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let alice = SecureChannel::new(client, &rendezvous_server.homeserver_url) + let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url) .await .expect("Alice should be able to create a secure channel."); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel.rs index d351dee0bc0..184442b5f41 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel.rs @@ -54,12 +54,12 @@ impl SecureChannel { Ok(Self { channel, qr_code_data, ecies }) } - /// Create a new login to reciprocate an existing login with. - #[cfg(test)] - pub(super) async fn new(http_client: HttpClient, homeserver_url: &Url) -> Result { + /// Create a new secure channel to reciprocate an existing login with. + pub(super) async fn reciprocate( + http_client: HttpClient, + homeserver_url: &Url, + ) -> Result { let mut channel = SecureChannel::login(http_client, homeserver_url).await?; - // We're a bit abusing the QR code data here, since we're passing the homeserver - // URL, but for our tests this is fine. channel.qr_code_data.mode_data = QrCodeModeData::Reciprocate { server_name: homeserver_url.to_string() }; Ok(channel) @@ -354,7 +354,7 @@ pub(super) mod test { let rendezvous_server = MockedRendezvousServer::new(&server, "abcdEFG12345").await; let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let alice = SecureChannel::new(client, &rendezvous_server.homeserver_url) + let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url) .await .expect("Alice should be able to create a secure channel."); From 61a698772f979b34f4f1575009ebdfb2c05f553d Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Fri, 24 Oct 2025 10:12:58 +0200 Subject: [PATCH 3/3] feat(oauth): add flow for reciprocating a login using a QR code generated on the existing device Signed-off-by: Johannes Marbach --- crates/matrix-sdk/CHANGELOG.md | 3 + .../src/authentication/oauth/mod.rs | 95 +- .../src/authentication/oauth/qrcode/grant.rs | 938 ++++++++++++++++++ .../src/authentication/oauth/qrcode/login.rs | 61 +- .../src/authentication/oauth/qrcode/mod.rs | 113 ++- crates/matrix-sdk/src/client/mod.rs | 56 +- 6 files changed, 1197 insertions(+), 69 deletions(-) create mode 100644 crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 313a6c79075..57a0833a6cd 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -16,6 +16,9 @@ All notable changes to this project will be documented in this file. - [**breaking**] Add `authentication::oauth::qrcode::login::LoginProgress::SyncingSecrets` to indicate that secrets are being synced between the two devices. ([#5760](https://github.com/matrix-org/matrix-rust-sdk/pull/5760)) +- Add `authentication::oauth::OAuth::grant_login_with_qr_code` to reciprocate a login by + generating a QR code on the existing device. + ([#5801](https://github.com/matrix-org/matrix-rust-sdk/pull/5801)) ### Refactor diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index 98c1c1f232d..d8bc2c4ce25 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -214,7 +214,7 @@ mod tests; #[cfg(feature = "e2e-encryption")] use self::cross_process::{CrossProcessRefreshLockGuard, CrossProcessRefreshManager}; #[cfg(feature = "e2e-encryption")] -use self::qrcode::{LoginWithGeneratedQrCode, LoginWithQrCode}; +use self::qrcode::{GrantLoginWithGeneratedQrCode, LoginWithGeneratedQrCode, LoginWithQrCode}; pub use self::{ account_management_url::{AccountManagementActionFull, AccountManagementUrlBuilder}, auth_code_builder::{OAuthAuthCodeUrlBuilder, OAuthAuthorizationData}, @@ -364,7 +364,7 @@ impl OAuth { as_variant!(data, AuthData::OAuth) } - /// Log in using a QR code. + /// Log in this device using a QR code. /// /// # Arguments /// @@ -380,6 +380,12 @@ impl OAuth { LoginWithQrCodeBuilder { client: &self.client, registration_data } } + /// Grant login to a new device using a QR code. + #[cfg(feature = "e2e-encryption")] + pub fn grant_login_with_qr_code<'a>(&'a self) -> GrantLoginWithQrCodeBuilder<'a> { + GrantLoginWithQrCodeBuilder { client: &self.client } + } + /// Restore or register the OAuth 2.0 client for the server with the given /// metadata, with the given optional [`ClientRegistrationData`]. /// @@ -1450,7 +1456,7 @@ impl<'a> LoginWithQrCodeBuilder<'a> { /// ruma::serde::Raw, /// Client, /// }; - /// use std::io::stdin; + /// use std::{error::Error, io::stdin}; /// # fn client_metadata() -> Raw { unimplemented!() } /// # _ = async { /// // Build the client as usual. @@ -1481,9 +1487,9 @@ impl<'a> LoginWithQrCodeBuilder<'a> { /// LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(cctx)) => { /// println!("Please enter the code displayed on your other device"); /// let mut s = String::new(); - /// stdin().read_line(&mut s).unwrap(); - /// let check_code = s.trim().parse::().unwrap(); - /// cctx.send(check_code).await.unwrap() + /// stdin().read_line(&mut s)?; + /// let check_code = s.trim().parse::()?; + /// cctx.send(check_code).await? /// } /// LoginProgress::WaitingForToken { user_code } => { /// println!("Please use your other device to confirm the log in {user_code}") @@ -1491,6 +1497,7 @@ impl<'a> LoginWithQrCodeBuilder<'a> { /// LoginProgress::Done => break, /// } /// } + /// Ok::<(), Box>(()) /// }); /// /// // Now run the future to complete the login. @@ -1505,6 +1512,82 @@ impl<'a> LoginWithQrCodeBuilder<'a> { } } +/// Builder for QR login grant handlers. +#[cfg(feature = "e2e-encryption")] +#[derive(Debug)] +pub struct GrantLoginWithQrCodeBuilder<'a> { + /// The underlying Matrix API client. + client: &'a Client, +} + +#[cfg(feature = "e2e-encryption")] +impl<'a> GrantLoginWithQrCodeBuilder<'a> { + /// Grant login by generating a QR code on this device to be scanned by the + /// new device. + /// + /// # Example + /// + /// ```no_run + /// use anyhow::bail; + /// use futures_util::StreamExt; + /// use matrix_sdk::{ + /// Client, authentication::oauth::{ + /// qrcode::{GeneratedQrProgress, GrantLoginProgress} + /// } + /// }; + /// use std::{error::Error, io::stdin}; + /// # _ = async { + /// // Build the client as usual. + /// let client = Client::builder() + /// .server_name_or_homeserver_url("matrix.org") + /// .handle_refresh_tokens() + /// .build() + /// .await?; + /// + /// let oauth = client.oauth(); + /// + /// // Subscribing to the progress is necessary since we need to capture the + /// // QR code, feed the checkcode back in and obtain the verification URL to + /// // open it in a browser so the user can consent to the new login. + /// let mut grant = oauth.grant_login_with_qr_code().generate(); + /// let mut progress = grant.subscribe_to_progress(); + /// + /// // Create a task which will show us the progress and allows us to receive + /// // and feed back data. + /// let task = tokio::spawn(async move { + /// while let Some(state) = progress.next().await { + /// match state { + /// GrantLoginProgress::Starting | GrantLoginProgress::SyncingSecrets => (), + /// GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(qr_code_data)) => { + /// println!("Please scan the QR code on your other device: {:?}", qr_code_data); + /// } + /// GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(checkcode_sender)) => { + /// println!("Please enter the code displayed on your other device"); + /// let mut s = String::new(); + /// stdin().read_line(&mut s)?; + /// let check_code = s.trim().parse::()?; + /// checkcode_sender.send(check_code).await?; + /// } + /// GrantLoginProgress::WaitingForAuth { verification_uri } => { + /// println!("Please open {verification_uri} to confirm the new login") + /// }, + /// GrantLoginProgress::Done => break, + /// } + /// } + /// Ok::<(), Box>(()) + /// }); + /// + /// // Now run the future to grant the login. + /// grant.await?; + /// task.abort(); + /// + /// println!("Successfully granted login"); + /// # anyhow::Ok(()) }; + /// ``` + pub fn generate(self) -> GrantLoginWithGeneratedQrCode<'a> { + GrantLoginWithGeneratedQrCode::new(self.client) + } +} /// A full session for the OAuth 2.0 API. #[derive(Debug, Clone)] pub struct OAuthSession { diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs new file mode 100644 index 00000000000..c45bbc59b96 --- /dev/null +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs @@ -0,0 +1,938 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::time::{Duration, Instant}; + +use eyeball::SharedObservable; +use futures_core::Stream; +use matrix_sdk_base::{boxed_into_future, crypto::types::SecretsBundle}; +use oauth2::VerificationUriComplete; +use url::Url; + +use super::{ + LoginProtocolType, QrAuthMessage, + secure_channel::{EstablishedSecureChannel, SecureChannel}, +}; +use crate::{ + Client, + authentication::oauth::qrcode::{ + CheckCodeSender, GeneratedQrProgress, LoginFailureReason, QRCodeGrantLoginError, + SecureChannelError, + }, +}; + +async fn export_secrets_bundle(client: &Client) -> Result { + let secrets_bundle = client + .olm_machine() + .await + .as_ref() + .ok_or_else(|| QRCodeGrantLoginError::MissingSecretsBackup(None))? + .store() + .export_secrets_bundle() + .await?; + Ok(secrets_bundle) +} + +async fn finish_login_grant( + client: &Client, + channel: &mut EstablishedSecureChannel, + secrets_bundle: &SecretsBundle, + state: &SharedObservable>, +) -> Result<(), QRCodeGrantLoginError> { + // The new device registers with the authorization server and sends it a device + // authorization authorization request. + // -- MSC4108 OAuth 2.0 login step 2 + + // We wait for the new device to send us the m.login.protocol message with the + // device authorization grant information. -- MSC4108 OAuth 2.0 login step 3 + let message = channel.receive_json().await?; + let QrAuthMessage::LoginProtocol { device_authorization_grant, protocol, device_id } = message + else { + return Err(QRCodeGrantLoginError::Unknown( + "Receiving unexpected message when expecting LoginProtocol".to_owned(), + )); + }; + + // We verify the selected protocol. + // -- MSC4108 OAuth 2.0 login step 4 + if protocol != LoginProtocolType::DeviceAuthorizationGrant { + channel + .send_json(QrAuthMessage::LoginFailure { + reason: LoginFailureReason::UnsupportedProtocol, + homeserver: None, + }) + .await?; + return Err(QRCodeGrantLoginError::UnsupportedProtocol(protocol)); + } + + // We check that the device ID is still available. + // -- MSC4108 OAuth 2.0 login step 4 continued + if !matches!(client.device_exists(device_id.to_base64().into()).await, Ok(false)) { + channel + .send_json(QrAuthMessage::LoginFailure { + reason: LoginFailureReason::DeviceAlreadyExists, + homeserver: None, + }) + .await?; + return Err(QRCodeGrantLoginError::DeviceIDAlreadyInUse); + } + + // We emit an update so that the caller can open the verification URI in a + // system browser to consent to the login. + // -- MSC4108 OAuth 2.0 login step 4 continued + let verification_uri = Url::parse( + device_authorization_grant + .verification_uri_complete + .map(VerificationUriComplete::into_secret) + .unwrap_or(device_authorization_grant.verification_uri.to_string()) + .as_str(), + ) + .map_err(|_| QRCodeGrantLoginError::UnableToCreateDevice)?; + state.set(GrantLoginProgress::WaitingForAuth { verification_uri }); + + // We send the new device the m.login.protocol_accepted message to let it know + // that the consent process is in progress. + // -- MSC4108 OAuth 2.0 login step 4 continued + let message = QrAuthMessage::LoginProtocolAccepted; + channel.send_json(&message).await?; + + // The new device displays the user code it received from the authorization + // server and starts polling for an access token. In parallel, the user + // consents to the new login in the browser on this device, while verifying + // the user code displayed on the other device. -- MSC4108 OAuth 2.0 login + // steps 5 & 6 + + // We wait for the new device to send us the m.login.success message + let message: QrAuthMessage = channel.receive_json().await?; + let QrAuthMessage::LoginSuccess = message else { + return Err(QRCodeGrantLoginError::Unknown( + "Receiving unexpected message when expecting LoginSuccess".to_owned(), + )); + }; + + // We check that the new device was created successfully allowing for up to 10 + // seconds of delay. -- MSC4108 Secret sharing and device verification step + // 1 + let deadline = Instant::now() + Duration::from_secs(10); + + loop { + if matches!(client.device_exists(device_id.to_base64().into()).await, Ok(true)) { + break; + } else { + // If the deadline hasn't yet passed, give it some time and retry the request. + if Instant::now() < deadline { + tokio::time::sleep(Duration::from_millis(500)).await; + continue; + } else { + // The deadline has passed. Let's fail the login process. + channel + .send_json(QrAuthMessage::LoginFailure { + reason: LoginFailureReason::DeviceNotFound, + homeserver: None, + }) + .await?; + return Err(QRCodeGrantLoginError::DeviceIDAlreadyInUse); + } + } + } + + // We send the new device the secrets bundle. + // -- MSC4108 Secret sharing and device verification step 2 + state.set(GrantLoginProgress::SyncingSecrets); + let message = QrAuthMessage::LoginSecrets(secrets_bundle.clone()); + channel.send_json(&message).await?; + + // And we're done. + state.set(GrantLoginProgress::Done); + + Ok(()) +} + +/// The progress of granting the login. +#[derive(Clone, Debug, Default)] +pub enum GrantLoginProgress { + /// We're just starting up, this is the default and initial state. + #[default] + Starting, + /// The secure channel is being established by exchanging the QR code + /// and/or checkcode. + EstablishingSecureChannel(Q), + /// The secure channel has been confirmed using the checkcode and this + /// device is waiting for the authorization to complete. + WaitingForAuth { + /// A URI to open in a (secure) system browser to verify the new login. + verification_uri: Url, + }, + /// The new device has been granted access and this device is sending the + /// secrets to it. + SyncingSecrets, + /// The process is complete. + Done, +} + +/// Named future for granting login by generating a QR code on this, existing, +/// device to be scanned by the other, new, device. +#[derive(Debug)] +pub struct GrantLoginWithGeneratedQrCode<'a> { + client: &'a Client, + state: SharedObservable>, +} + +impl<'a> GrantLoginWithGeneratedQrCode<'a> { + pub(crate) fn new(client: &'a Client) -> GrantLoginWithGeneratedQrCode<'a> { + GrantLoginWithGeneratedQrCode { client, state: Default::default() } + } +} + +impl GrantLoginWithGeneratedQrCode<'_> { + /// Subscribe to the progress of QR code login. + /// + /// It's necessary to subscribe to this to show the QR code to the existing + /// device so it can send the check code back to this device. + pub fn subscribe_to_progress( + &self, + ) -> impl Stream> + use<> { + self.state.subscribe() + } +} + +impl<'a> IntoFuture for GrantLoginWithGeneratedQrCode<'a> { + type Output = Result<(), QRCodeGrantLoginError>; + boxed_into_future!(extra_bounds: 'a); + + fn into_future(self) -> Self::IntoFuture { + Box::pin(async move { + // Create a new ephemeral key pair and a rendezvous session to grant a + // login with. + // -- MSC4108 Secure channel setup steps 1 & 2 + let homeserver_url = self.client.homeserver(); + let http_client = self.client.inner.http_client.clone(); + let channel = SecureChannel::reciprocate(http_client, &homeserver_url).await?; + + // Extract the QR code data and emit an update so that the caller can + // present the QR code for scanning by the new device. + // -- MSC4108 Secure channel setup step 3 + self.state.set(GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrReady(channel.qr_code_data().clone()), + )); + + // Wait for the secure channel to connect. The other device now needs to scan + // the QR code and send us the LoginInitiateMessage which we respond to + // with the LoginOkMessage. -- MSC4108 step 4 & 5 + let channel = channel.connect().await?; + + // The other device now needs to verify our message, compute the checkcode and + // display it. We emit a progress update to let the caller prompt the + // user to enter the checkcode and feed it back to us. + // -- MSC4108 Secure channel setup step 6 + let (tx, rx) = tokio::sync::oneshot::channel(); + self.state.set(GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrScanned(CheckCodeSender::new(tx)), + )); + let check_code = rx.await.map_err(|_| SecureChannelError::CannotReceiveCheckCode)?; + + // Use the checkcode to verify that the channel is actually secure. + // -- MSC4108 Secure channel setup step 7 + let mut channel = channel.confirm(check_code)?; + + // Since the QR code was generated on this existing device, the new device can + // derive the homeserver to use for logging in from the QR code and we + // don't need to send the m.login.protocols message. + // -- MSC4108 OAuth 2.0 login step 1 + + // Proceed with granting the login. + // -- MSC4108 OAuth 2.0 login remaining steps + finish_login_grant( + self.client, + &mut channel, + &export_secrets_bundle(self.client).await?, + &self.state, + ) + .await + }) + } +} + +#[cfg(all(test, not(target_family = "wasm")))] +mod test { + use assert_matches2::{assert_let, assert_matches}; + use futures_util::{StreamExt, join}; + use matrix_sdk_base::crypto::types::{ + SecretsBundle, + qr_login::{QrCodeData, QrCodeMode}, + }; + use matrix_sdk_common::executor::spawn; + use matrix_sdk_test::async_test; + use oauth2::{EndUserVerificationUrl, VerificationUriComplete}; + use ruma::{owned_device_id, owned_user_id}; + use tokio::sync::oneshot; + use tracing::debug; + use vodozemac::Curve25519PublicKey; + + use super::*; + use crate::{ + authentication::oauth::qrcode::{ + LoginFailureReason, QrAuthMessage, + messages::{AuthorizationGrant, LoginProtocolType}, + secure_channel::{EstablishedSecureChannel, test::MockedRendezvousServer}, + }, + test_utils::mocks::MatrixMockServer, + }; + + enum BobBehaviour { + HappyPath, + UnexpectedMessageInsteadOfLoginProtocol, + DeviceAlreadyExists, + DeviceNotCreated, + } + + async fn request_login( + behaviour: BobBehaviour, + qr_code_rx: oneshot::Receiver, + check_code_tx: oneshot::Sender, + server: MatrixMockServer, + device_authorization_grant: Option, + secrets_bundle: Option, + ) { + // Wait for Alice to produce the qr code. + let qr_code_data = qr_code_rx.await.expect("Bob should receive the QR code"); + + // Use the QR code to establish the secure channel from the new client (Bob). + let mut bob = EstablishedSecureChannel::from_qr_code( + reqwest::Client::new(), + &qr_code_data, + QrCodeMode::Login, + ) + .await + .expect("Bob should be able to connect the secure channel"); + + // Let Alice know about the checkcode so she can verify the channel. + check_code_tx + .send(bob.check_code().to_digit()) + .expect("Bob should be able to send the checkcode"); + + match behaviour { + BobBehaviour::UnexpectedMessageInsteadOfLoginProtocol => { + // Send an unexpected message and exit. + let message = QrAuthMessage::LoginSuccess; + bob.send_json(message).await.unwrap(); + return; + } + BobBehaviour::DeviceAlreadyExists => { + // Mock the endpoint for querying devices so that Alice thinks the device + // already exists. + server.mock_get_device().ok().expect(1..).named("get_device").mount().await; + + // Now send the LoginProtocol message. + let message = QrAuthMessage::LoginProtocol { + protocol: LoginProtocolType::DeviceAuthorizationGrant, + device_authorization_grant: device_authorization_grant + .expect("Bob needs the device authorization grant"), + device_id: Curve25519PublicKey::from_base64( + "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4", + ) + .unwrap(), + }; + bob.send_json(message).await.unwrap(); + + // Alice should fail the login with the appropriate reason. + let message = bob + .receive_json() + .await + .expect("Bob should receive the LoginFailure message from Alice"); + assert_let!(QrAuthMessage::LoginFailure { reason, .. } = message); + assert_matches!(reason, LoginFailureReason::DeviceAlreadyExists); + + return; // Exit. + } + _ => { + // Send the LoginProtocol message. + let message = QrAuthMessage::LoginProtocol { + protocol: LoginProtocolType::DeviceAuthorizationGrant, + device_authorization_grant: device_authorization_grant + .expect("Bob needs the device authorization grant"), + device_id: Curve25519PublicKey::from_base64( + "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4", + ) + .unwrap(), + }; + bob.send_json(message).await.unwrap(); + } + } + + // Receive the LoginProtocolAccepted message. + let message = bob + .receive_json() + .await + .expect("Bob should receive the LoginProtocolAccepted message from Alice"); + assert_let!(QrAuthMessage::LoginProtocolAccepted = message); + + match behaviour { + BobBehaviour::DeviceNotCreated => { + // Don't mock the endpoint for querying devices so that Alice cannot verify that + // we have logged in. + + // Send the LoginSuccess message to claim that we have logged in. + let message = QrAuthMessage::LoginSuccess; + bob.send_json(message).await.unwrap(); + + // Alice should eventually give up querying our device and fail the login with + // the appropriate reason. + let message = bob + .receive_json() + .await + .expect("Bob should receive the LoginFailure message from Alice"); + assert_let!(QrAuthMessage::LoginFailure { reason, .. } = message); + assert_matches!(reason, LoginFailureReason::DeviceNotFound); + + return; // Exit. + } + _ => { + // Mock the endpoint for querying devices so that Alice thinks we have logged + // in. + server.mock_get_device().ok().expect(1..).named("get_device").mount().await; + + // Send the LoginSuccess message. + let message = QrAuthMessage::LoginSuccess; + bob.send_json(message).await.unwrap(); + } + } + + // Receive the LoginSecrets message. + let message = bob + .receive_json() + .await + .expect("Bob should receive the LoginSecrets message from Alice"); + assert_let!(QrAuthMessage::LoginSecrets(bundle) = message); + + // Verify that we received the correct secrets. + assert_eq!( + serde_json::to_value(&secrets_bundle).unwrap(), + serde_json::to_value(&bundle).unwrap() + ); + } + + #[async_test] + async fn test_grant_login_with_generated_qr_code() { + let server = MatrixMockServer::new().await; + let rendezvous_server = MockedRendezvousServer::new(server.server(), "abcdEFG12345").await; + debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); + + let device_authorization_grant = AuthorizationGrant { + verification_uri_complete: Some(VerificationUriComplete::new( + "https://id.matrix.org/device/abcde".to_owned(), + )), + verification_uri: EndUserVerificationUrl::new( + "https://id.matrix.org/device/abcde?code=ABCDE".to_owned(), + ) + .unwrap(), + }; + + server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await; + server + .mock_upload_cross_signing_keys() + .ok() + .expect(1) + .named("upload_xsigning_keys") + .mount() + .await; + server + .mock_upload_cross_signing_signatures() + .ok() + .expect(1) + .named("upload_xsigning_signatures") + .mount() + .await; + + // Create the existing client (Alice). + let user_id = owned_user_id!("@alice:example.org"); + let device_id = owned_device_id!("ALICE_DEVICE"); + let alice = server + .client_builder_for_crypto_end_to_end(&user_id, &device_id) + .logged_in_with_oauth() + .build() + .await; + alice + .encryption() + .bootstrap_cross_signing(None) + .await + .expect("Alice should be able to set up cross signing"); + + // Prepare the login granting future. + let oauth = alice.oauth(); + let grant = oauth.grant_login_with_qr_code().generate(); + let secrets_bundle = export_secrets_bundle(&alice) + .await + .expect("Alice should be able to export the secrets bundle"); + let (qr_code_tx, qr_code_rx) = oneshot::channel(); + let (checkcode_tx, checkcode_rx) = oneshot::channel(); + + // Spawn the updates task. + let mut updates = grant.subscribe_to_progress(); + let mut state = grant.state.get(); + let verification_uri_complete = + device_authorization_grant.clone().verification_uri_complete.unwrap().into_secret(); + assert_matches!(state.clone(), GrantLoginProgress::Starting); + let updates_task = spawn(async move { + let mut qr_code_tx = Some(qr_code_tx); + let mut checkcode_rx = Some(checkcode_rx); + + while let Some(update) = updates.next().await { + match &update { + GrantLoginProgress::Starting => { + assert_matches!(state, GrantLoginProgress::Starting); + } + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrReady(qr_code_data), + ) => { + assert_matches!(state, GrantLoginProgress::Starting); + qr_code_tx + .take() + .expect("The QR code should only be forwarded once") + .send(qr_code_data.clone()) + .expect("Alice should be able to forward the QR code"); + } + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrScanned(checkcode_sender), + ) => { + assert_matches!( + state, + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrReady(_) + ) + ); + let checkcode = checkcode_rx + .take() + .expect("The checkcode should only be forwarded once") + .await + .expect("Alice should receive the checkcode"); + checkcode_sender + .send(checkcode) + .await + .expect("Alice should be able to forward the checkcode"); + } + GrantLoginProgress::WaitingForAuth { verification_uri } => { + assert_matches!( + state, + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrScanned(_) + ) + ); + assert_eq!(verification_uri.as_str(), verification_uri_complete); + } + GrantLoginProgress::SyncingSecrets => { + assert_matches!(state, GrantLoginProgress::WaitingForAuth { .. }); + } + GrantLoginProgress::Done => { + assert_matches!(state, GrantLoginProgress::SyncingSecrets); + break; + } + } + state = update; + } + }); + + // Let Bob request the login and run through the process. + let bob_task = spawn(async move { + request_login( + BobBehaviour::HappyPath, + qr_code_rx, + checkcode_tx, + server, + Some(device_authorization_grant), + Some(secrets_bundle), + ) + .await; + }); + + // Wait for all tasks to finish. + join!( + async { updates_task.await.expect("Alice should run through all progress states") }, + async { grant.await.expect("Alice should be able to grant the login") }, + async { bob_task.await.expect("Bob's task should finish") } + ); + } + + #[async_test] + async fn test_grant_login_with_generated_qr_code_unexpected_message_instead_of_login_protocol() + { + let server = MatrixMockServer::new().await; + let rendezvous_server = MockedRendezvousServer::new(server.server(), "abcdEFG12345").await; + debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); + + server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await; + server + .mock_upload_cross_signing_keys() + .ok() + .expect(1) + .named("upload_xsigning_keys") + .mount() + .await; + server + .mock_upload_cross_signing_signatures() + .ok() + .expect(1) + .named("upload_xsigning_signatures") + .mount() + .await; + + // Create the existing client (Alice). + let user_id = owned_user_id!("@alice:example.org"); + let device_id = owned_device_id!("ALICE_DEVICE"); + let alice = server + .client_builder_for_crypto_end_to_end(&user_id, &device_id) + .logged_in_with_oauth() + .build() + .await; + alice + .encryption() + .bootstrap_cross_signing(None) + .await + .expect("Alice should be able to set up cross signing"); + + // Prepare the login granting future. + let oauth = alice.oauth(); + let grant = oauth.grant_login_with_qr_code().generate(); + let (qr_code_tx, qr_code_rx) = oneshot::channel(); + let (checkcode_tx, checkcode_rx) = oneshot::channel(); + + // Spawn the updates task. + let mut updates = grant.subscribe_to_progress(); + let mut state = grant.state.get(); + assert_matches!(state.clone(), GrantLoginProgress::Starting); + let updates_task = spawn(async move { + let mut qr_code_tx = Some(qr_code_tx); + let mut checkcode_rx = Some(checkcode_rx); + + while let Some(update) = updates.next().await { + match &update { + GrantLoginProgress::Starting => { + assert_matches!(state, GrantLoginProgress::Starting); + } + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrReady(qr_code_data), + ) => { + assert_matches!(state, GrantLoginProgress::Starting); + qr_code_tx + .take() + .expect("The QR code should only be forwarded once") + .send(qr_code_data.clone()) + .expect("Alice should be able to forward the QR code"); + } + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrScanned(checkcode_sender), + ) => { + assert_matches!( + state, + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrReady(_) + ) + ); + let checkcode = checkcode_rx + .take() + .expect("The checkcode should only be forwarded once") + .await + .expect("Alice should receive the checkcode"); + checkcode_sender + .send(checkcode) + .await + .expect("Alice should be able to forward the checkcode"); + break; + } + _ => { + panic!("Alice should abort the process"); + } + } + state = update; + } + }); + + // Let Bob request the login and run through the process. + let bob_task = spawn(async move { + request_login( + BobBehaviour::UnexpectedMessageInsteadOfLoginProtocol, + qr_code_rx, + checkcode_tx, + server, + None, + None, + ) + .await; + }); + + // Wait for all tasks to finish / fail. + join!( + async { updates_task.await.expect("Alice should run through all progress states") }, + async { grant.await.expect_err("Alice should abort the login") }, + async { bob_task.await.expect("Bob's task should finish") } + ); + } + + #[async_test] + async fn test_grant_login_with_generated_qr_code_device_already_exists() { + let server = MatrixMockServer::new().await; + let rendezvous_server = MockedRendezvousServer::new(server.server(), "abcdEFG12345").await; + debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); + + let device_authorization_grant = AuthorizationGrant { + verification_uri_complete: Some(VerificationUriComplete::new( + "https://id.matrix.org/device/abcde".to_owned(), + )), + verification_uri: EndUserVerificationUrl::new( + "https://id.matrix.org/device/abcde?code=ABCDE".to_owned(), + ) + .unwrap(), + }; + + server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await; + server + .mock_upload_cross_signing_keys() + .ok() + .expect(1) + .named("upload_xsigning_keys") + .mount() + .await; + server + .mock_upload_cross_signing_signatures() + .ok() + .expect(1) + .named("upload_xsigning_signatures") + .mount() + .await; + + // Create the existing client (Alice). + let user_id = owned_user_id!("@alice:example.org"); + let device_id = owned_device_id!("ALICE_DEVICE"); + let alice = server + .client_builder_for_crypto_end_to_end(&user_id, &device_id) + .logged_in_with_oauth() + .build() + .await; + alice + .encryption() + .bootstrap_cross_signing(None) + .await + .expect("Alice should be able to set up cross signing"); + + // Prepare the login granting future. + let oauth = alice.oauth(); + let grant = oauth.grant_login_with_qr_code().generate(); + let (qr_code_tx, qr_code_rx) = oneshot::channel(); + let (checkcode_tx, checkcode_rx) = oneshot::channel(); + + // Spawn the updates task. + let mut updates = grant.subscribe_to_progress(); + let mut state = grant.state.get(); + assert_matches!(state.clone(), GrantLoginProgress::Starting); + let updates_task = spawn(async move { + let mut qr_code_tx = Some(qr_code_tx); + let mut checkcode_rx = Some(checkcode_rx); + + while let Some(update) = updates.next().await { + match &update { + GrantLoginProgress::Starting => { + assert_matches!(state, GrantLoginProgress::Starting); + } + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrReady(qr_code_data), + ) => { + assert_matches!(state, GrantLoginProgress::Starting); + qr_code_tx + .take() + .expect("The QR code should only be forwarded once") + .send(qr_code_data.clone()) + .expect("Alice should be able to forward the QR code"); + } + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrScanned(checkcode_sender), + ) => { + assert_matches!( + state, + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrReady(_) + ) + ); + let checkcode = checkcode_rx + .take() + .expect("The checkcode should only be forwarded once") + .await + .expect("Alice should receive the checkcode"); + checkcode_sender + .send(checkcode) + .await + .expect("Alice should be able to forward the checkcode"); + } + _ => { + panic!("Alice should abort the process"); + } + } + state = update; + } + }); + + // Let Bob request the login and run through the process. + let bob_task = spawn(async move { + request_login( + BobBehaviour::DeviceAlreadyExists, + qr_code_rx, + checkcode_tx, + server, + Some(device_authorization_grant), + None, + ) + .await; + }); + + // Wait for all tasks to finish. + join!( + async { updates_task.await.expect("Alice should run through all progress states") }, + async { grant.await.expect_err("Alice should abort the login") }, + async { bob_task.await.expect("Bob's task should finish") } + ); + } + + #[async_test] + async fn test_grant_login_with_generated_qr_code_device_not_created() { + let server = MatrixMockServer::new().await; + let rendezvous_server = MockedRendezvousServer::new(server.server(), "abcdEFG12345").await; + debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); + + let device_authorization_grant = AuthorizationGrant { + verification_uri_complete: Some(VerificationUriComplete::new( + "https://id.matrix.org/device/abcde".to_owned(), + )), + verification_uri: EndUserVerificationUrl::new( + "https://id.matrix.org/device/abcde?code=ABCDE".to_owned(), + ) + .unwrap(), + }; + + server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await; + server + .mock_upload_cross_signing_keys() + .ok() + .expect(1) + .named("upload_xsigning_keys") + .mount() + .await; + server + .mock_upload_cross_signing_signatures() + .ok() + .expect(1) + .named("upload_xsigning_signatures") + .mount() + .await; + + // Create the existing client (Alice). + let user_id = owned_user_id!("@alice:example.org"); + let device_id = owned_device_id!("ALICE_DEVICE"); + let alice = server + .client_builder_for_crypto_end_to_end(&user_id, &device_id) + .logged_in_with_oauth() + .build() + .await; + alice + .encryption() + .bootstrap_cross_signing(None) + .await + .expect("Alice should be able to set up cross signing"); + + // Prepare the login granting future. + let oauth = alice.oauth(); + let grant = oauth.grant_login_with_qr_code().generate(); + let (qr_code_tx, qr_code_rx) = oneshot::channel(); + let (checkcode_tx, checkcode_rx) = oneshot::channel(); + + // Spawn the updates task. + let mut updates = grant.subscribe_to_progress(); + let mut state = grant.state.get(); + let verification_uri_complete = + device_authorization_grant.clone().verification_uri_complete.unwrap().into_secret(); + assert_matches!(state.clone(), GrantLoginProgress::Starting); + let updates_task = spawn(async move { + let mut qr_code_tx = Some(qr_code_tx); + let mut checkcode_rx = Some(checkcode_rx); + + while let Some(update) = updates.next().await { + match &update { + GrantLoginProgress::Starting => { + assert_matches!(state, GrantLoginProgress::Starting); + } + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrReady(qr_code_data), + ) => { + assert_matches!(state, GrantLoginProgress::Starting); + qr_code_tx + .take() + .expect("The QR code should only be forwarded once") + .send(qr_code_data.clone()) + .expect("Alice should be able to forward the QR code"); + } + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrScanned(checkcode_sender), + ) => { + assert_matches!( + state, + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrReady(_) + ) + ); + let checkcode = checkcode_rx + .take() + .expect("The checkcode should only be forwarded once") + .await + .expect("Alice should receive the checkcode"); + checkcode_sender + .send(checkcode) + .await + .expect("Alice should be able to forward the checkcode"); + } + GrantLoginProgress::WaitingForAuth { verification_uri } => { + assert_matches!( + state, + GrantLoginProgress::EstablishingSecureChannel( + GeneratedQrProgress::QrScanned(_) + ) + ); + assert_eq!(verification_uri.as_str(), verification_uri_complete); + } + _ => { + panic!("Alice should abort the process"); + } + } + state = update; + } + }); + + // Let Bob request the login and run through the process. + let bob_task = spawn(async move { + request_login( + BobBehaviour::DeviceNotCreated, + qr_code_rx, + checkcode_tx, + server, + Some(device_authorization_grant), + None, + ) + .await; + }); + + // Wait for all tasks to finish. + join!( + async { updates_task.await.expect("Alice should run through all progress states") }, + async { grant.await.expect_err("Alice should abort the login") }, + async { bob_task.await.expect("Bob's task should finish") } + ); + } +} diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index 70b4af3cb06..db5bdeec167 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{future::IntoFuture, sync::Arc}; +use std::future::IntoFuture; use eyeball::SharedObservable; use futures_core::Stream; @@ -26,7 +26,6 @@ use ruma::{ OwnedDeviceId, api::client::discovery::get_authorization_server_metadata::v1::AuthorizationServerMetadata, }; -use tokio::sync::Mutex; use tracing::trace; use vodozemac::{Curve25519PublicKey, ecies::CheckCode}; @@ -37,7 +36,10 @@ use super::{ }; use crate::{ Client, - authentication::oauth::{ClientRegistrationData, OAuth, OAuthError, qrcode::LoginProtocolType}, + authentication::oauth::{ + ClientRegistrationData, OAuth, OAuthError, + qrcode::{CheckCodeSender, GeneratedQrProgress, LoginProtocolType}, + }, }; async fn send_unexpected_message_error( @@ -274,59 +276,6 @@ pub struct QrProgress { pub check_code: CheckCode, } -/// Metadata to be used with [`LoginProgress::EstablishingSecureChannel`] when -/// this device is the one generating the QR code. -/// -/// We have established the secure channel, but we need to let the -/// other side know about the [`QrCodeData`] so they can send us the -/// [`CheckCode`] and verify that the secure channel is indeed secure. -#[derive(Clone, Debug)] -pub enum GeneratedQrProgress { - /// The QR code data that must be sent to the existing device. - QrReady(QrCodeData), - /// Used to send the [`CheckCode`] to the login task once we receive - /// it from the existing device. - QrScanned(CheckCodeSender), -} - -/// 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>>>, -} - -impl CheckCodeSender { - fn new(tx: tokio::sync::oneshot::Sender) -> Self { - Self { inner: Arc::new(Mutex::new(Some(tx))) } - } - - /// 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, check_code: u8) -> Result<(), CheckCodeSenderError> { - match self.inner.lock().await.take() { - Some(tx) => tx.send(check_code).map_err(|_| CheckCodeSenderError::CannotSend), - None => Err(CheckCodeSenderError::AlreadySent), - } - } -} - -/// Possible errors when calling [`CheckCodeSender::send`]. -#[derive(Debug, thiserror::Error)] -pub enum CheckCodeSenderError { - /// The check code has already been sent. - #[error("check code already sent.")] - AlreadySent, - /// The check code cannot be sent. - #[error("check code cannot be sent.")] - CannotSend, -} - /// Named future for logging in by scanning a QR code with the /// [`OAuth::login_with_qr_code()`] method. #[derive(Debug)] diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index 9f7a9f66bda..939f1cb3cd1 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -21,30 +21,32 @@ //! QR code. To log in using a QR code, please take a look at the //! [`OAuth::login_with_qr_code()`] method. +use std::sync::Arc; + use as_variant::as_variant; -use matrix_sdk_base::crypto::SecretImportError; pub use matrix_sdk_base::crypto::types::qr_login::{ LoginQrCodeDecodeError, QrCodeData, QrCodeMode, QrCodeModeData, }; +use matrix_sdk_base::crypto::{SecretImportError, store::SecretsBundleExportError}; pub use oauth2::{ ConfigurationError, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, HttpClientError, RequestTokenError, StandardErrorResponse, basic::{BasicErrorResponse, BasicRequestTokenError}, }; use thiserror::Error; +use tokio::sync::Mutex; use url::Url; pub use vodozemac::ecies::{Error as EciesError, MessageDecodeError}; +mod grant; mod login; mod messages; mod rendezvous_channel; mod secure_channel; pub use self::{ - login::{ - CheckCodeSender, CheckCodeSenderError, GeneratedQrProgress, LoginProgress, - LoginWithGeneratedQrCode, LoginWithQrCode, QrProgress, - }, + grant::{GrantLoginProgress, GrantLoginWithGeneratedQrCode}, + login::{LoginProgress, LoginWithGeneratedQrCode, LoginWithQrCode, QrProgress}, messages::{LoginFailureReason, LoginProtocolType, QrAuthMessage}, }; use super::CrossProcessRefreshLockError; @@ -114,6 +116,50 @@ pub enum QRCodeLoginError { ServerReset(crate::Error), } +/// The error type for failures while trying to grant log in to a new device +/// using a QR code. +#[derive(Debug, Error)] +pub enum QRCodeGrantLoginError { + /// Secrets backup not set up. + #[error("Secrets backup not set up")] + MissingSecretsBackup(Option), + + /// The check code was incorrect. + #[error("The check code was incorrect")] + InvalidCheckCode, + + /// The device could not be created. + #[error("The device could not be created")] + UnableToCreateDevice, + + /// Auth handshake error. + #[error("Auth handshake error: {0}")] + Unknown(String), + + /// Unsupported protocol. + #[error("Unsupported protocol: {0}")] + UnsupportedProtocol(LoginProtocolType), + + /// The requested device ID is already in use. + #[error("The requested device ID is already in use")] + DeviceIDAlreadyInUse, +} + +impl From for QRCodeGrantLoginError { + fn from(e: SecureChannelError) -> Self { + match e { + SecureChannelError::InvalidCheckCode => Self::InvalidCheckCode, + e => Self::Unknown(e.to_string()), + } + } +} + +impl From for QRCodeGrantLoginError { + fn from(e: SecretsBundleExportError) -> Self { + Self::MissingSecretsBackup(Some(e)) + } +} + /// Error type describing failures in the interaction between the device /// attempting to log in and the OAuth 2.0 authorization server. #[derive(Debug, Error)] @@ -209,3 +255,60 @@ pub enum SecureChannelError { )] CannotReceiveCheckCode, } + +/// Metadata to be used with [`LoginProgress::EstablishingSecureChannel`] and +/// [`GrantLoginProgress::EstablishingSecureChannel`] when this device is the +/// one generating the QR code. +/// +/// We have established the secure channel, but we need to let the +/// other device know about the [`QrCodeData`] so they can connect to the +/// channel and let us know about the checkcode so we can verify that the +/// channel is indeed secure. +#[derive(Clone, Debug)] +pub enum GeneratedQrProgress { + /// The QR code has been created and this device is waiting for the other + /// device to scan it. + QrReady(QrCodeData), + /// The QR code has been scanned by the other device and this device is + /// waiting for the user to put in the checkcode displayed on the + /// other device. + QrScanned(CheckCodeSender), +} + +/// 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>>>, +} + +impl CheckCodeSender { + pub(crate) fn new(tx: tokio::sync::oneshot::Sender) -> Self { + Self { inner: Arc::new(Mutex::new(Some(tx))) } + } + + /// 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, check_code: u8) -> Result<(), CheckCodeSenderError> { + match self.inner.lock().await.take() { + Some(tx) => tx.send(check_code).map_err(|_| CheckCodeSenderError::CannotSend), + None => Err(CheckCodeSenderError::AlreadySent), + } + } +} + +/// Possible errors when calling [`CheckCodeSender::send`]. +#[derive(Debug, thiserror::Error)] +pub enum CheckCodeSenderError { + /// The check code has already been sent. + #[error("check code already sent.")] + AlreadySent, + /// The check code cannot be sent. + #[error("check code cannot be sent.")] + CannotSend, +} diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 505911ee819..f7f115e6314 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -50,7 +50,7 @@ use ruma::{ account::whoami, alias::{create_alias, delete_alias, get_alias}, authenticated_media, - device::{delete_devices, get_devices, update_device}, + device::{self, delete_devices, get_devices, update_device}, directory::{get_public_rooms, get_public_rooms_filtered}, discovery::{ discover_homeserver::{self, RtcFocusInfo}, @@ -2312,6 +2312,30 @@ impl Client { self.send(request).await } + /// Check whether a device with a specific ID exists on the server. + /// + /// Returns Ok(true) if the device exists, Ok(false) if the server responded + /// with 404 and the underlying error otherwise. + /// + /// # Arguments + /// + /// * `device_id` - The ID of the device to query. + pub async fn device_exists(&self, device_id: OwnedDeviceId) -> Result { + let request = device::get_device::v3::Request::new(device_id); + match self.send(request).await { + Ok(_) => Ok(true), + Err(err) => { + if let Some(error) = err.as_client_api_error() + && error.status_code == 404 + { + Ok(false) + } else { + Err(err.into()) + } + } + } + } + /// Synchronize the client's state with the latest state on the server. /// /// ## Syncing Events @@ -3128,7 +3152,7 @@ pub(crate) mod tests { ignored_user_list::IgnoredUserListEventContent, media_preview_config::{InviteAvatars, MediaPreviewConfigEventContent, MediaPreviews}, }, - owned_room_id, owned_user_id, room_alias_id, room_id, user_id, + owned_device_id, owned_room_id, owned_user_id, room_alias_id, room_id, user_id, }; use serde_json::json; use stream_assert::{assert_next_matches, assert_pending}; @@ -4197,4 +4221,32 @@ pub(crate) mod tests { let found_room = client.get_dm_room(user_id).expect("DM not found!"); assert!(found_room.get_member_no_sync(user_id).await.unwrap().is_some()); } + + #[async_test] + async fn test_device_exists() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + server.mock_get_device().ok().expect(1).mount().await; + + assert_matches!(client.device_exists(owned_device_id!("ABCDEF")).await, Ok(true)); + } + + #[async_test] + async fn test_device_exists_404() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + assert_matches!(client.device_exists(owned_device_id!("ABCDEF")).await, Ok(false)); + } + + #[async_test] + async fn test_device_exists_500() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + server.mock_get_device().error500().expect(1).mount().await; + + assert_matches!(client.device_exists(owned_device_id!("ABCDEF")).await, Err(_)); + } }