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 requested device ID is already in use.
173    #[error("The requested device ID is already in use")]
174    DeviceIDAlreadyInUse,
175
176    /// The requested device was not returned by the homeserver.
177    #[error("The requested device was not returned by the homeserver")]
178    DeviceNotFound,
179
180    /// An error happened while exchanging messages with the other device.
181    #[error(transparent)]
182    SecureChannel(SecureChannelError),
183
184    /// An unexpected message was received from the other device.
185    #[error("We have received an unexpected message, expected: {expected}, got {received:?}")]
186    UnexpectedMessage {
187        /// The message we expected.
188        expected: &'static str,
189        /// The message we received instead.
190        received: Box<QrAuthMessage>,
191    },
192
193    /// The other device has signaled to us that the login has failed.
194    #[error("The login failed, reason: {reason}")]
195    LoginFailure {
196        /// The reason, as signaled by the other device, for the login failure.
197        reason: LoginFailureReason,
198    },
199}
200
201impl From<SecureChannelError> for QRCodeGrantLoginError {
202    fn from(e: SecureChannelError) -> Self {
203        match e {
204            SecureChannelError::RendezvousChannel(ref http_error) => {
205                if let Some(ErrorKind::NotFound) = http_error.client_api_error_kind() {
206                    return Self::NotFound;
207                }
208                Self::SecureChannel(e)
209            }
210            SecureChannelError::InvalidCheckCode => Self::InvalidCheckCode,
211            e => Self::SecureChannel(e),
212        }
213    }
214}
215
216impl From<SecretsBundleExportError> for QRCodeGrantLoginError {
217    fn from(e: SecretsBundleExportError) -> Self {
218        Self::MissingSecretsBackup(Some(e))
219    }
220}
221
222/// Error type describing failures in the interaction between the device
223/// attempting to log in and the OAuth 2.0 authorization server.
224#[derive(Debug, Error)]
225pub enum DeviceAuthorizationOAuthError {
226    /// A generic OAuth 2.0 error happened while we were attempting to register
227    /// the device with the OAuth 2.0 authorization server.
228    #[error(transparent)]
229    OAuth(#[from] crate::authentication::oauth::OAuthError),
230
231    /// The OAuth 2.0 server doesn't support the device authorization grant.
232    #[error("OAuth 2.0 server doesn't support the device authorization grant")]
233    NoDeviceAuthorizationEndpoint,
234
235    /// An error happened while we attempted to request a device authorization
236    /// from the OAuth 2.0 authorization server.
237    #[error(transparent)]
238    DeviceAuthorization(#[from] BasicRequestTokenError<HttpClientError<reqwest::Error>>),
239
240    /// An error happened while waiting for the access token to be issued and
241    /// sent to us by the OAuth 2.0 authorization server.
242    #[error(transparent)]
243    RequestToken(
244        #[from] RequestTokenError<HttpClientError<reqwest::Error>, DeviceCodeErrorResponse>,
245    ),
246}
247
248impl DeviceAuthorizationOAuthError {
249    /// If the [`DeviceAuthorizationOAuthError`] is of the
250    /// [`DeviceCodeErrorResponseType`] error variant, return it.
251    pub fn as_request_token_error(&self) -> Option<&DeviceCodeErrorResponseType> {
252        let error = as_variant!(self, DeviceAuthorizationOAuthError::RequestToken)?;
253        let request_token_error = as_variant!(error, RequestTokenError::ServerResponse)?;
254
255        Some(request_token_error.error())
256    }
257}
258
259/// Error type which describes failures when messages which are received over
260/// the secure channel fail to be decoded.
261#[derive(Debug, Error)]
262pub enum MessageDecodeError {
263    /// A received message has failed to be decoded.
264    #[error(transparent)]
265    Ecies(#[from] EciesMessageDecodeError),
266
267    /// A received message has failed to be decoded.
268    #[error(transparent)]
269    Hpke(#[from] HpkeMessageDecodeError),
270
271    /// A message we received over the secure channel was not a valid UTF-8
272    /// encoded string.
273    #[error(transparent)]
274    Utf8(#[from] std::str::Utf8Error),
275
276    /// A message couldn't be deserialized from JSON.
277    #[error(transparent)]
278    Json(#[from] serde_json::Error),
279
280    /// The sequence token of the rendezvous channel needs to be at most
281    /// [`u8::MAX`] bytes long, otherwise it can't be encoded as additional
282    /// authenticated data.
283    #[error("The sequence token is too long")]
284    TooLongSequenceToken(InvalidLengthError),
285
286    /// The base URL of the homeserver needs to be at most [`u8::MAX`] bytes
287    /// long, otherwise it can't be encoded as additional authenticated data.
288    #[error("The base URL of the homeserver is too long")]
289    TooLongBaseUrl(InvalidLengthError),
290
291    /// The rendezvous ID of the channel needs to be at most [`u8::MAX`] bytes
292    /// long, otherwise it can't be encoded as additional authenticated data.
293    #[error("The rendezvous ID is too long")]
294    TooLongRendezvousId(InvalidLengthError),
295}
296
297/// Error type for decryption failures of the secure channel.
298#[derive(Debug, Error)]
299pub enum DecryptionError {
300    /// A ECIES message failed to be decrypted.
301    #[error(transparent)]
302    Ecies(#[from] EciesError),
303    /// A HPKE message failed to be decrypted.
304    #[error(transparent)]
305    Hpke(#[from] HpkeError),
306}
307
308/// Error type for failures in when receiving or sending messages over the
309/// secure channel.
310#[derive(Debug, Error)]
311pub enum SecureChannelError {
312    /// A message has failed to be decrypted.
313    #[error(transparent)]
314    Decryption(#[from] DecryptionError),
315
316    /// A received message has failed to be decoded.
317    #[error(transparent)]
318    MessageDecode(#[from] MessageDecodeError),
319
320    /// The secure channel failed to be established because it received an
321    /// unexpected message.
322    #[error(
323        "The secure channel setup has received an unexpected message, expected: {expected}, got {received}"
324    )]
325    SecureChannelMessage {
326        /// The secure channel message we expected.
327        expected: &'static str,
328        /// The secure channel message we received instead.
329        received: String,
330    },
331
332    /// The secure channel could not have been established, the check code was
333    /// invalid.
334    #[error("The secure channel could not have been established, the check code was invalid")]
335    InvalidCheckCode,
336
337    /// An error happened in the underlying rendezvous channel.
338    #[error("Error in the rendezvous channel: {0:?}")]
339    RendezvousChannel(#[from] HttpError),
340
341    /// Both devices have advertised the same intent in the login attempt, i.e.
342    /// both sides claim to be a new device.
343    #[error(
344        "The secure channel could not have been established, \
345         the two devices have the same login intent"
346    )]
347    InvalidIntent,
348
349    /// The secure channel could not have been established, the check code
350    /// cannot be received.
351    #[error(
352        "The secure channel could not have been established, \
353         the check code cannot be received"
354    )]
355    CannotReceiveCheckCode,
356
357    /// The QR code specifies an unsupported protocol version.
358    #[error("The QR code specifies an unsupported protocol version")]
359    UnsupportedQrCodeType,
360
361    /// The QR code couldn't have been created.
362    #[error(transparent)]
363    QrCodeCreationError(#[from] QrCodeCreationError),
364}
365
366/// Metadata to be used with [`LoginProgress::EstablishingSecureChannel`] or
367/// [`GrantLoginProgress::EstablishingSecureChannel`] when this device is the
368/// one scanning the QR code.
369///
370/// We have established the secure channel, but we need to let the other side
371/// know about the check code so they can verify that the secure channel is
372/// indeed secure.
373#[derive(Clone, Debug)]
374pub struct QrProgress {
375    /// The check code we need to, out of band, send to the other device.
376    pub check_code: u8,
377}
378
379/// Metadata to be used with [`LoginProgress::EstablishingSecureChannel`] and
380/// [`GrantLoginProgress::EstablishingSecureChannel`] when this device is the
381/// one generating the QR code.
382///
383/// We have established the secure channel, but we need to let the other device
384/// know about the [`QrCodeData`] so they can connect to the channel and let us
385/// know about the check code so we can verify that the channel is indeed
386/// secure.
387#[derive(Clone, Debug)]
388pub enum GeneratedQrProgress {
389    /// The QR code has been created and this device is waiting for the other
390    /// device to scan it.
391    QrReady(QrCodeData),
392    /// The QR code has been scanned by the other device and this device is
393    /// waiting for the user to put in the check code displayed on the other
394    /// device.
395    QrScanned(CheckCodeSender),
396}
397
398/// A oneshot sender used to send the check code back to the device that
399/// generated the QR code.
400pub type CheckCodeSender = CloneableSender<u8>;
401
402impl CheckCodeSender {
403    /// Send the check code.
404    ///
405    /// Calling this method more than once will result in an error.
406    ///
407    /// # Arguments
408    ///
409    /// * `check_code` - The check code in digits representation.
410    pub async fn send(&self, check_code: u8) -> Result<(), SenderError> {
411        self.send_impl(check_code).await
412    }
413}
414
415/// The internal message of the [`ContinuationMessageSender`] to either continue
416/// the login granting process or to cancel it.
417#[derive(Clone, Copy, Debug)]
418pub(crate) enum ContinuationMessage {
419    Confirm,
420    Cancel,
421}
422
423/// Struct used to let the QR code granting logic know that it can continue with
424/// the process since applications might suspend things while the verification
425/// URI is open.
426#[derive(Clone, Debug)]
427pub struct ContinuationMessageSender(CloneableSender<ContinuationMessage>);
428
429impl ContinuationMessageSender {
430    /// Confirm the continuation of the login granting process.
431    pub async fn confirm(&self) -> Result<(), SenderError> {
432        self.0.send_impl(ContinuationMessage::Confirm).await
433    }
434
435    /// Cancel the login granting process.
436    pub async fn cancel(&self) -> Result<(), SenderError> {
437        self.0.send_impl(ContinuationMessage::Cancel).await
438    }
439}
440
441/// A oneshot sender we are able to clone so we can put it into a
442/// [`SharedObservable`](eyeball::SharedObservable).
443#[derive(Clone, Debug)]
444pub struct CloneableSender<T> {
445    inner: Arc<Mutex<Option<tokio::sync::oneshot::Sender<T>>>>,
446}
447
448impl<T> CloneableSender<T> {
449    pub(crate) fn new(tx: tokio::sync::oneshot::Sender<T>) -> Self {
450        Self { inner: Arc::new(Mutex::new(Some(tx))) }
451    }
452
453    async fn send_impl(&self, message: T) -> Result<(), SenderError> {
454        match self.inner.lock().await.take() {
455            Some(tx) => tx.send(message).map_err(|_| SenderError::CannotSend),
456            None => Err(SenderError::AlreadySent),
457        }
458    }
459}
460
461/// Possible errors when calling [`CloneableSender::send`].
462#[derive(Debug, thiserror::Error)]
463pub enum SenderError {
464    /// The message has already been sent.
465    #[error("message already sent.")]
466    AlreadySent,
467    /// The message cannot be sent.
468    #[error("message cannot be sent.")]
469    CannotSend,
470}
471
472#[cfg(all(test, not(target_family = "wasm")))]
473mod tests {
474    use matrix_sdk_test::async_test;
475    use serde_json::json;
476    use wiremock::{
477        Mock, ResponseTemplate,
478        matchers::{method, path},
479    };
480
481    use crate::test_utils::mocks::MatrixMockServer;
482
483    #[async_test]
484    async fn test_msc_4388_rendezvous_server_supported() {
485        const URL: &str = "/_matrix/client/unstable/io.element.msc4388/rendezvous";
486
487        let server = MatrixMockServer::new().await;
488        let client = server.client_builder().logged_in_with_oauth().build().await;
489
490        {
491            let _discover_guard = server
492                .server()
493                .register_as_scoped(
494                    Mock::given(method("GET"))
495                        .and(path(URL))
496                        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
497                            "create_available": true,
498                        })))
499                        .expect(1),
500                )
501                .await;
502
503            let supported = client
504                .oauth()
505                .msc_4388_rendezvous_server_supported()
506                .await
507                .expect("We should be able to check if the rendezvous server is supported");
508
509            assert!(supported, "The rendezvous server should be supported");
510        }
511
512        {
513            let _discover_guard = server
514                .server()
515                .register_as_scoped(
516                    Mock::given(method("GET"))
517                        .and(path(URL))
518                        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
519                            "create_available": false,
520                        })))
521                        .expect(1),
522                )
523                .await;
524
525            let supported = client
526                .oauth()
527                .msc_4388_rendezvous_server_supported()
528                .await
529                .expect("We should be able to check if the rendezvous server is supported");
530
531            assert!(
532                !supported,
533                "The rendezvous server should not be supported, because create_available is false"
534            );
535        }
536
537        {
538            let _discover_guard = server
539                .server()
540                .register_as_scoped(
541                    Mock::given(method("GET"))
542                        .and(path(URL))
543                        .respond_with(ResponseTemplate::new(404))
544                        .expect(1),
545                )
546                .await;
547
548            let supported = client
549                .oauth()
550                .msc_4388_rendezvous_server_supported()
551                .await
552                .expect("We should be able to check if the rendezvous server is supported");
553
554            assert!(
555                !supported,
556                "The rendezvous server should not be supported if we receive a 404 response"
557            );
558        }
559
560        {
561            let _discover_guard = server
562                .server()
563                .register_as_scoped(
564                    Mock::given(method("GET"))
565                        .and(path(URL))
566                        .respond_with(ResponseTemplate::new(403))
567                        .expect(1),
568                )
569                .await;
570
571            let supported = client
572                .oauth()
573                .msc_4388_rendezvous_server_supported()
574                .await
575                .expect("We should be able to check if the rendezvous server is supported");
576
577            assert!(
578                !supported,
579                "The rendezvous server should not be supported if we receive a 403 response"
580            );
581        }
582
583        {
584            let _discover_guard = server
585                .server()
586                .register_as_scoped(
587                    Mock::given(method("GET"))
588                        .and(path(URL))
589                        .respond_with(ResponseTemplate::new(500))
590                        .expect(1),
591                )
592                .await;
593
594            client
595                .oauth()
596                .msc_4388_rendezvous_server_supported()
597                .await
598                .expect_err("We should return an error if the homeserver can't tell us if the endpoint is supported or not");
599        }
600    }
601}