Skip to main content

matrix_sdk/authentication/oauth/qrcode/
mod.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Types for the QR code login support defined in [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108).
16//!
17//! Please note, QR code logins are only supported when using OAuth 2.0 as the
18//! authentication mechanism, native Matrix authentication does not support it.
19//!
20//! This currently only implements the case where the new device is scanning the
21//! QR code. To log in using a QR code, please take a look at the
22//! [`OAuth::login_with_qr_code()`] method.
23
24use std::sync::Arc;
25
26use as_variant::as_variant;
27pub use matrix_sdk_base::crypto::types::qr_login::{
28    LoginQrCodeDecodeError, Msc4108IntentData, QrCodeData, QrCodeIntent, QrCodeIntentData,
29};
30use matrix_sdk_base::crypto::{
31    SecretImportError,
32    store::SecretsBundleExportError,
33    types::qr_login::{InvalidLengthError, QrCodeCreationError},
34};
35pub use oauth2::{
36    ConfigurationError, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, HttpClientError,
37    RequestTokenError, StandardErrorResponse,
38    basic::{BasicErrorResponse, BasicRequestTokenError},
39};
40use ruma::api::error::ErrorKind;
41use thiserror::Error;
42use tokio::sync::Mutex;
43pub use vodozemac::{
44    ecies::{Error as EciesError, MessageDecodeError as EciesMessageDecodeError},
45    hpke::{Error as HpkeError, MessageDecodeError as HpkeMessageDecodeError},
46};
47
48mod grant;
49mod login;
50mod messages;
51mod rendezvous_channel;
52mod secure_channel;
53
54pub use self::{
55    grant::{GrantLoginProgress, GrantLoginWithGeneratedQrCode, GrantLoginWithScannedQrCode},
56    login::{LoginProgress, LoginWithGeneratedQrCode, LoginWithQrCode},
57    messages::{
58        AuthorizationGrant, LoginFailureReason, LoginProtocolData, LoginProtocolType,
59        QrAuthMessage, UnknownLoginProtocol,
60    },
61};
62use super::CrossProcessRefreshLockError;
63#[cfg(doc)]
64use super::OAuth;
65use crate::HttpError;
66
67/// The error type for failures while trying to log in a new device using a QR
68/// code.
69#[derive(Debug, Error)]
70#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
71pub enum QRCodeLoginError {
72    /// An error happened while we were communicating with the OAuth 2.0
73    /// authorization server.
74    #[error(transparent)]
75    OAuth(#[from] DeviceAuthorizationOAuthError),
76
77    /// The other device has signaled to us that the login has failed.
78    #[error("The login failed, reason: {reason}")]
79    LoginFailure {
80        /// The reason, as signaled by the other device, for the login failure.
81        reason: LoginFailureReason,
82        /// The homeserver the other device suggested we use, either as a
83        /// server name or as a homeserver URL.
84        homeserver: Option<String>,
85    },
86
87    /// An unexpected message was received from the other device.
88    #[error("We have received an unexpected message, expected: {expected}, got {received:?}")]
89    UnexpectedMessage {
90        /// The message we expected.
91        expected: &'static str,
92        /// The message we received instead.
93        received: Box<QrAuthMessage>,
94    },
95
96    /// An error happened while exchanging messages with the other device.
97    #[error(transparent)]
98    SecureChannel(SecureChannelError),
99
100    /// The rendezvous session was not found and might have expired.
101    #[error("The rendezvous session was not found and might have expired")]
102    NotFound,
103
104    /// The cross-process refresh lock failed to be initialized.
105    #[error(transparent)]
106    CrossProcessRefreshLock(#[from] CrossProcessRefreshLockError),
107
108    /// An error happened while we were trying to discover our user and device
109    /// ID, after we have acquired an access token from the OAuth 2.0
110    /// authorization server.
111    #[error(transparent)]
112    UserIdDiscovery(HttpError),
113
114    /// We failed to set the session tokens after we figured out our device and
115    /// user IDs.
116    #[error(transparent)]
117    SessionTokens(crate::Error),
118
119    /// The device keys failed to be uploaded after we successfully logged in.
120    #[error(transparent)]
121    DeviceKeyUpload(crate::Error),
122
123    /// The secrets bundle we received from the existing device failed to be
124    /// imported.
125    #[error(transparent)]
126    SecretImport(#[from] SecretImportError),
127
128    /// The other party told us to use a different homeserver but we failed to
129    /// reset the server URL.
130    #[error(transparent)]
131    ServerReset(crate::Error),
132}
133
134impl From<SecureChannelError> for QRCodeLoginError {
135    fn from(e: SecureChannelError) -> Self {
136        match e {
137            SecureChannelError::RendezvousChannel(ref http_error) => {
138                if let Some(ErrorKind::NotFound) = http_error.client_api_error_kind() {
139                    return Self::NotFound;
140                }
141                Self::SecureChannel(e)
142            }
143            e => Self::SecureChannel(e),
144        }
145    }
146}
147
148/// The error type for failures while trying to grant log in to a new device
149/// using a QR code.
150#[derive(Debug, Error)]
151pub enum QRCodeGrantLoginError {
152    /// Secrets backup not set up.
153    #[error("Secrets backup not set up")]
154    MissingSecretsBackup(Option<SecretsBundleExportError>),
155
156    /// The check code was incorrect.
157    #[error("The check code was incorrect")]
158    InvalidCheckCode,
159
160    /// The rendezvous session was not found and might have expired.
161    #[error("The rendezvous session was not found and might have expired")]
162    NotFound,
163
164    /// Auth handshake error.
165    #[error("Auth handshake error: {0}")]
166    Unknown(String),
167
168    /// Unsupported protocol.
169    #[error("Unsupported protocol: {0}")]
170    UnsupportedProtocol(LoginProtocolType),
171
172    /// The homeserver doesn't support any of the login protocols that we can
173    /// offer to the new device.
174    #[error("The homeserver doesn't support any of the login protocols we can offer")]
175    NoSupportedProtocols,
176
177    /// The requested device ID is already in use.
178    #[error("The requested device ID is already in use")]
179    DeviceIDAlreadyInUse,
180
181    /// The requested device was not returned by the homeserver.
182    #[error("The requested device was not returned by the homeserver")]
183    DeviceNotFound,
184
185    /// An error happened while exchanging messages with the other device.
186    #[error(transparent)]
187    SecureChannel(SecureChannelError),
188
189    /// An unexpected message was received from the other device.
190    #[error("We have received an unexpected message, expected: {expected}, got {received:?}")]
191    UnexpectedMessage {
192        /// The message we expected.
193        expected: &'static str,
194        /// The message we received instead.
195        received: Box<QrAuthMessage>,
196    },
197
198    /// The other device has signaled to us that the login has failed.
199    #[error("The login failed, reason: {reason}")]
200    LoginFailure {
201        /// The reason, as signaled by the other device, for the login failure.
202        reason: LoginFailureReason,
203    },
204}
205
206impl From<SecureChannelError> for QRCodeGrantLoginError {
207    fn from(e: SecureChannelError) -> Self {
208        match e {
209            SecureChannelError::RendezvousChannel(ref http_error) => {
210                if let Some(ErrorKind::NotFound) = http_error.client_api_error_kind() {
211                    return Self::NotFound;
212                }
213                Self::SecureChannel(e)
214            }
215            SecureChannelError::InvalidCheckCode => Self::InvalidCheckCode,
216            e => Self::SecureChannel(e),
217        }
218    }
219}
220
221impl From<SecretsBundleExportError> for QRCodeGrantLoginError {
222    fn from(e: SecretsBundleExportError) -> Self {
223        Self::MissingSecretsBackup(Some(e))
224    }
225}
226
227/// Error type describing failures in the interaction between the device
228/// attempting to log in and the OAuth 2.0 authorization server.
229#[derive(Debug, Error)]
230pub enum DeviceAuthorizationOAuthError {
231    /// A generic OAuth 2.0 error happened while we were attempting to register
232    /// the device with the OAuth 2.0 authorization server.
233    #[error(transparent)]
234    OAuth(#[from] crate::authentication::oauth::OAuthError),
235
236    /// The OAuth 2.0 server doesn't support the device authorization grant.
237    #[error("OAuth 2.0 server doesn't support the device authorization grant")]
238    NoDeviceAuthorizationEndpoint,
239
240    /// An error happened while we attempted to request a device authorization
241    /// from the OAuth 2.0 authorization server.
242    #[error(transparent)]
243    DeviceAuthorization(#[from] BasicRequestTokenError<HttpClientError<reqwest::Error>>),
244
245    /// An error happened while waiting for the access token to be issued and
246    /// sent to us by the OAuth 2.0 authorization server.
247    #[error(transparent)]
248    RequestToken(
249        #[from] RequestTokenError<HttpClientError<reqwest::Error>, DeviceCodeErrorResponse>,
250    ),
251}
252
253impl DeviceAuthorizationOAuthError {
254    /// If the [`DeviceAuthorizationOAuthError`] is of the
255    /// [`DeviceCodeErrorResponseType`] error variant, return it.
256    pub fn as_request_token_error(&self) -> Option<&DeviceCodeErrorResponseType> {
257        let error = as_variant!(self, DeviceAuthorizationOAuthError::RequestToken)?;
258        let request_token_error = as_variant!(error, RequestTokenError::ServerResponse)?;
259
260        Some(request_token_error.error())
261    }
262}
263
264/// Error type which describes failures when messages which are received over
265/// the secure channel fail to be decoded.
266#[derive(Debug, Error)]
267pub enum MessageDecodeError {
268    /// A received message has failed to be decoded.
269    #[error(transparent)]
270    Ecies(#[from] EciesMessageDecodeError),
271
272    /// A received message has failed to be decoded.
273    #[error(transparent)]
274    Hpke(#[from] HpkeMessageDecodeError),
275
276    /// A message we received over the secure channel was not a valid UTF-8
277    /// encoded string.
278    #[error(transparent)]
279    Utf8(#[from] std::str::Utf8Error),
280
281    /// A message couldn't be deserialized from JSON.
282    #[error(transparent)]
283    Json(#[from] serde_json::Error),
284
285    /// The sequence token of the rendezvous channel needs to be at most
286    /// [`u8::MAX`] bytes long, otherwise it can't be encoded as additional
287    /// authenticated data.
288    #[error("The sequence token is too long")]
289    TooLongSequenceToken(InvalidLengthError),
290
291    /// The base URL of the homeserver needs to be at most [`u8::MAX`] bytes
292    /// long, otherwise it can't be encoded as additional authenticated data.
293    #[error("The base URL of the homeserver is too long")]
294    TooLongBaseUrl(InvalidLengthError),
295
296    /// The rendezvous ID of the channel needs to be at most [`u8::MAX`] bytes
297    /// long, otherwise it can't be encoded as additional authenticated data.
298    #[error("The rendezvous ID is too long")]
299    TooLongRendezvousId(InvalidLengthError),
300}
301
302/// Error type for decryption failures of the secure channel.
303#[derive(Debug, Error)]
304pub enum DecryptionError {
305    /// A ECIES message failed to be decrypted.
306    #[error(transparent)]
307    Ecies(#[from] EciesError),
308    /// A HPKE message failed to be decrypted.
309    #[error(transparent)]
310    Hpke(#[from] HpkeError),
311}
312
313/// Error type for failures in when receiving or sending messages over the
314/// secure channel.
315#[derive(Debug, Error)]
316pub enum SecureChannelError {
317    /// A message has failed to be decrypted.
318    #[error(transparent)]
319    Decryption(#[from] DecryptionError),
320
321    /// A received message has failed to be decoded.
322    #[error(transparent)]
323    MessageDecode(#[from] MessageDecodeError),
324
325    /// The secure channel failed to be established because it received an
326    /// unexpected message.
327    #[error(
328        "The secure channel setup has received an unexpected message, expected: {expected}, got {received}"
329    )]
330    SecureChannelMessage {
331        /// The secure channel message we expected.
332        expected: &'static str,
333        /// The secure channel message we received instead.
334        received: String,
335    },
336
337    /// The secure channel could not have been established, the check code was
338    /// invalid.
339    #[error("The secure channel could not have been established, the check code was invalid")]
340    InvalidCheckCode,
341
342    /// An error happened in the underlying rendezvous channel.
343    #[error("Error in the rendezvous channel: {0:?}")]
344    RendezvousChannel(#[from] HttpError),
345
346    /// Both devices have advertised the same intent in the login attempt, i.e.
347    /// both sides claim to be a new device.
348    #[error(
349        "The secure channel could not have been established, \
350         the two devices have the same login intent"
351    )]
352    InvalidIntent,
353
354    /// The secure channel could not have been established, the check code
355    /// cannot be received.
356    #[error(
357        "The secure channel could not have been established, \
358         the check code cannot be received"
359    )]
360    CannotReceiveCheckCode,
361
362    /// The QR code specifies an unsupported protocol version.
363    #[error("The QR code specifies an unsupported protocol version")]
364    UnsupportedQrCodeType,
365
366    /// The QR code couldn't have been created.
367    #[error(transparent)]
368    QrCodeCreationError(#[from] QrCodeCreationError),
369}
370
371/// Metadata to be used with [`LoginProgress::EstablishingSecureChannel`] or
372/// [`GrantLoginProgress::EstablishingSecureChannel`] when this device is the
373/// one scanning the QR code.
374///
375/// We have established the secure channel, but we need to let the other side
376/// know about the check code so they can verify that the secure channel is
377/// indeed secure.
378#[derive(Clone, Debug)]
379pub struct QrProgress {
380    /// The check code we need to, out of band, send to the other device.
381    pub check_code: u8,
382}
383
384/// Metadata to be used with [`LoginProgress::EstablishingSecureChannel`] and
385/// [`GrantLoginProgress::EstablishingSecureChannel`] when this device is the
386/// one generating the QR code.
387///
388/// We have established the secure channel, but we need to let the other device
389/// know about the [`QrCodeData`] so they can connect to the channel and let us
390/// know about the check code so we can verify that the channel is indeed
391/// secure.
392#[derive(Clone, Debug)]
393pub enum GeneratedQrProgress {
394    /// The QR code has been created and this device is waiting for the other
395    /// device to scan it.
396    QrReady(QrCodeData),
397    /// The QR code has been scanned by the other device and this device is
398    /// waiting for the user to put in the check code displayed on the other
399    /// device.
400    QrScanned(CheckCodeSender),
401}
402
403/// A oneshot sender used to send the check code back to the device that
404/// generated the QR code.
405pub type CheckCodeSender = CloneableSender<u8>;
406
407impl CheckCodeSender {
408    /// Send the check code.
409    ///
410    /// Calling this method more than once will result in an error.
411    ///
412    /// # Arguments
413    ///
414    /// * `check_code` - The check code in digits representation.
415    pub async fn send(&self, check_code: u8) -> Result<(), SenderError> {
416        self.send_impl(check_code).await
417    }
418}
419
420/// The internal message of the [`ContinuationMessageSender`] to either continue
421/// the login granting process or to cancel it.
422#[derive(Clone, Copy, Debug)]
423pub(crate) enum ContinuationMessage {
424    Confirm,
425    Cancel,
426}
427
428/// Struct used to let the QR code granting logic know that it can continue with
429/// the process since applications might suspend things while the verification
430/// URI is open.
431#[derive(Clone, Debug)]
432pub struct ContinuationMessageSender(CloneableSender<ContinuationMessage>);
433
434impl ContinuationMessageSender {
435    /// Confirm the continuation of the login granting process.
436    pub async fn confirm(&self) -> Result<(), SenderError> {
437        self.0.send_impl(ContinuationMessage::Confirm).await
438    }
439
440    /// Cancel the login granting process.
441    pub async fn cancel(&self) -> Result<(), SenderError> {
442        self.0.send_impl(ContinuationMessage::Cancel).await
443    }
444}
445
446/// A oneshot sender we are able to clone so we can put it into a
447/// [`SharedObservable`](eyeball::SharedObservable).
448#[derive(Clone, Debug)]
449pub struct CloneableSender<T> {
450    inner: Arc<Mutex<Option<tokio::sync::oneshot::Sender<T>>>>,
451}
452
453impl<T> CloneableSender<T> {
454    pub(crate) fn new(tx: tokio::sync::oneshot::Sender<T>) -> Self {
455        Self { inner: Arc::new(Mutex::new(Some(tx))) }
456    }
457
458    async fn send_impl(&self, message: T) -> Result<(), SenderError> {
459        match self.inner.lock().await.take() {
460            Some(tx) => tx.send(message).map_err(|_| SenderError::CannotSend),
461            None => Err(SenderError::AlreadySent),
462        }
463    }
464}
465
466/// Possible errors when calling [`CloneableSender::send`].
467#[derive(Debug, thiserror::Error)]
468pub enum SenderError {
469    /// The message has already been sent.
470    #[error("message already sent.")]
471    AlreadySent,
472    /// The message cannot be sent.
473    #[error("message cannot be sent.")]
474    CannotSend,
475}
476
477#[cfg(all(test, not(target_family = "wasm"), feature = "unstable-msc4388"))]
478mod tests {
479    use matrix_sdk_test::async_test;
480    use serde_json::json;
481    use wiremock::{
482        Mock, ResponseTemplate,
483        matchers::{method, path},
484    };
485
486    use crate::test_utils::mocks::MatrixMockServer;
487
488    #[async_test]
489    async fn test_msc_4388_rendezvous_server_supported() {
490        const URL: &str = "/_matrix/client/unstable/io.element.msc4388/rendezvous";
491
492        let server = MatrixMockServer::new().await;
493        let client = server.client_builder().logged_in_with_oauth().build().await;
494
495        {
496            let _discover_guard = server
497                .server()
498                .register_as_scoped(
499                    Mock::given(method("GET"))
500                        .and(path(URL))
501                        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
502                            "create_available": true,
503                        })))
504                        .expect(1),
505                )
506                .await;
507
508            let supported = client
509                .oauth()
510                .msc_4388_rendezvous_server_supported()
511                .await
512                .expect("We should be able to check if the rendezvous server is supported");
513
514            assert!(supported, "The rendezvous server should be supported");
515        }
516
517        {
518            let _discover_guard = server
519                .server()
520                .register_as_scoped(
521                    Mock::given(method("GET"))
522                        .and(path(URL))
523                        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
524                            "create_available": false,
525                        })))
526                        .expect(1),
527                )
528                .await;
529
530            let supported = client
531                .oauth()
532                .msc_4388_rendezvous_server_supported()
533                .await
534                .expect("We should be able to check if the rendezvous server is supported");
535
536            assert!(
537                !supported,
538                "The rendezvous server should not be supported, because create_available is false"
539            );
540        }
541
542        {
543            let _discover_guard = server
544                .server()
545                .register_as_scoped(
546                    Mock::given(method("GET"))
547                        .and(path(URL))
548                        .respond_with(ResponseTemplate::new(404))
549                        .expect(1),
550                )
551                .await;
552
553            let supported = client
554                .oauth()
555                .msc_4388_rendezvous_server_supported()
556                .await
557                .expect("We should be able to check if the rendezvous server is supported");
558
559            assert!(
560                !supported,
561                "The rendezvous server should not be supported if we receive a 404 response"
562            );
563        }
564
565        {
566            let _discover_guard = server
567                .server()
568                .register_as_scoped(
569                    Mock::given(method("GET"))
570                        .and(path(URL))
571                        .respond_with(ResponseTemplate::new(403))
572                        .expect(1),
573                )
574                .await;
575
576            let supported = client
577                .oauth()
578                .msc_4388_rendezvous_server_supported()
579                .await
580                .expect("We should be able to check if the rendezvous server is supported");
581
582            assert!(
583                !supported,
584                "The rendezvous server should not be supported if we receive a 403 response"
585            );
586        }
587
588        {
589            let _discover_guard = server
590                .server()
591                .register_as_scoped(
592                    Mock::given(method("GET"))
593                        .and(path(URL))
594                        .respond_with(ResponseTemplate::new(500))
595                        .expect(1),
596                )
597                .await;
598
599            client
600                .oauth()
601                .msc_4388_rendezvous_server_supported()
602                .await
603                .expect_err("We should return an error if the homeserver can't tell us if the endpoint is supported or not");
604        }
605    }
606}