Skip to main content

matrix_sdk/authentication/oauth/qrcode/secure_channel/
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
15use crypto_channel::*;
16use matrix_sdk_base::crypto::types::qr_login::{
17    Msc4108IntentData, QrCodeData, QrCodeIntent, QrCodeIntentData,
18};
19use serde::{Serialize, de::DeserializeOwned};
20use tracing::{instrument, trace};
21use url::Url;
22use vodozemac::ecies::{Ecies, EstablishedEcies, InboundCreationResult, OutboundCreationResult};
23
24use super::{
25    SecureChannelError as Error,
26    rendezvous_channel::{InboundChannelCreationResult, RendezvousChannel, RendezvousInfo},
27};
28use crate::{config::RequestConfig, http_client::HttpClient};
29mod crypto_channel;
30
31const LOGIN_INITIATE_MESSAGE: &str = "MATRIX_QR_CODE_LOGIN_INITIATE";
32const LOGIN_OK_MESSAGE: &str = "MATRIX_QR_CODE_LOGIN_OK";
33
34pub(super) struct SecureChannel {
35    channel: RendezvousChannel,
36    qr_code_data: QrCodeData,
37    crypto_channel: CryptoChannel,
38}
39
40impl SecureChannel {
41    /// Create a new secure channel to request a login with.
42    pub(super) async fn login(
43        http_client: HttpClient,
44        homeserver_url: &Url,
45    ) -> Result<Self, Error> {
46        let channel = RendezvousChannel::create_outbound(http_client, homeserver_url).await?;
47
48        let (crypto_channel, qr_code_data) = match channel.rendezvous_info() {
49            RendezvousInfo::Msc4108 { rendezvous_url } => {
50                let intent_data = Msc4108IntentData::Login;
51                let crypto_channel = CryptoChannel::new_ecies();
52
53                let qr_code_data = QrCodeData::new_msc4108(
54                    crypto_channel.public_key(),
55                    rendezvous_url.clone(),
56                    intent_data,
57                );
58
59                (crypto_channel, qr_code_data)
60            }
61        };
62
63        Ok(Self { channel, qr_code_data, crypto_channel })
64    }
65
66    /// Create a new secure channel to reciprocate an existing login with.
67    pub(super) async fn reciprocate(
68        http_client: HttpClient,
69        homeserver_url: &Url,
70    ) -> Result<Self, Error> {
71        let mut channel = SecureChannel::login(http_client, homeserver_url).await?;
72
73        match channel.channel.rendezvous_info() {
74            RendezvousInfo::Msc4108 { rendezvous_url } => {
75                let mode_data =
76                    Msc4108IntentData::Reciprocate { server_name: homeserver_url.to_string() };
77
78                channel.qr_code_data = QrCodeData::new_msc4108(
79                    channel.crypto_channel.public_key(),
80                    rendezvous_url.clone(),
81                    mode_data,
82                );
83            }
84        }
85
86        Ok(channel)
87    }
88
89    pub(super) fn qr_code_data(&self) -> &QrCodeData {
90        &self.qr_code_data
91    }
92
93    #[instrument(skip(self))]
94    pub(super) async fn connect(mut self) -> Result<AlmostEstablishedSecureChannel, Error> {
95        trace!("Trying to connect the secure channel.");
96
97        let message = self.channel.receive().await?;
98        let result = self.crypto_channel.establish_inbound_channel(&message)?;
99
100        let message = std::str::from_utf8(result.plaintext())?;
101
102        trace!("Received the initial secure channel message");
103
104        if message == LOGIN_INITIATE_MESSAGE {
105            let secure_channel = match result {
106                CryptoChannelCreationResult::Ecies(InboundCreationResult { ecies, .. }) => {
107                    let crypto_channel = EstablishedCryptoChannel::Ecies(ecies);
108
109                    let mut secure_channel =
110                        EstablishedSecureChannel { channel: self.channel, crypto_channel };
111
112                    trace!("Sending the LOGIN OK message");
113
114                    secure_channel.send(LOGIN_OK_MESSAGE).await?;
115                    secure_channel
116                }
117            };
118
119            Ok(AlmostEstablishedSecureChannel { secure_channel })
120        } else {
121            Err(Error::SecureChannelMessage {
122                expected: LOGIN_INITIATE_MESSAGE,
123                received: message.to_owned(),
124            })
125        }
126    }
127}
128
129/// An SecureChannel that is yet to be confirmed as with the [`CheckCode`].
130/// Same deal as for the [`SecureChannel`], not used for now.
131pub(super) struct AlmostEstablishedSecureChannel {
132    secure_channel: EstablishedSecureChannel,
133}
134
135impl AlmostEstablishedSecureChannel {
136    /// Confirm that the secure channel is indeed secure.
137    ///
138    /// The check code needs to be received out of band from the other side of
139    /// the secure channel.
140    pub(super) fn confirm(self, check_code: u8) -> Result<EstablishedSecureChannel, Error> {
141        if check_code == self.secure_channel.check_code() {
142            Ok(self.secure_channel)
143        } else {
144            Err(Error::InvalidCheckCode)
145        }
146    }
147}
148
149pub(super) struct EstablishedSecureChannel {
150    channel: RendezvousChannel,
151    crypto_channel: EstablishedCryptoChannel,
152}
153
154impl EstablishedSecureChannel {
155    /// Establish a secure channel from a scanned QR code.
156    #[instrument(skip(client))]
157    pub(super) async fn from_qr_code(
158        client: reqwest::Client,
159        qr_code_data: &QrCodeData,
160        expected_mode: QrCodeIntent,
161    ) -> Result<Self, Error> {
162        enum ChannelType {
163            Ecies(EstablishedEcies),
164        }
165
166        if qr_code_data.intent() == expected_mode {
167            Err(Error::InvalidIntent)
168        } else {
169            trace!("Attempting to create a new inbound secure channel from a QR code.");
170
171            let client = HttpClient::new(client, RequestConfig::short_retry());
172
173            // Let's establish an outbound ECIES channel, the other side won't know that
174            // it's talking to us, the device that scanned the QR code, until it
175            // receives and successfully decrypts the initial message. We're here encrypting
176            // the `LOGIN_INITIATE_MESSAGE`.
177            let (crypto_channel, encoded_message) = {
178                let ecies = Ecies::new();
179
180                let OutboundCreationResult { ecies, message } = ecies.establish_outbound_channel(
181                    qr_code_data.public_key(),
182                    LOGIN_INITIATE_MESSAGE.as_bytes(),
183                )?;
184                (ChannelType::Ecies(ecies), message.encode())
185            };
186
187            // The other side has crated a rendezvous channel, we're going to connect to it
188            // and send this initial encrypted message through it. The initial message on
189            // the rendezvous channel will have an empty body, so we can just
190            // drop it.
191            let mut channel = match qr_code_data.intent_data() {
192                QrCodeIntentData::Msc4108 { rendezvous_url, .. } => {
193                    let InboundChannelCreationResult { channel, .. } =
194                        RendezvousChannel::create_inbound(client, rendezvous_url).await?;
195                    channel
196                }
197                // TODO: We need to support the new rendezvous channel type and HPKE for the crypto
198                // channel when we encounter this QR code variant.
199                QrCodeIntentData::Msc4388 { .. } => return Err(Error::UnsupportedQrCodeType),
200            };
201
202            trace!(
203                "Received the initial message from the rendezvous channel, sending the LOGIN \
204                     INITIATE message"
205            );
206
207            // Now we're sending the encrypted message through the rendezvous channel to the
208            // other side.
209            channel.send(encoded_message).await?;
210
211            trace!("Waiting for the LOGIN OK message");
212
213            let (response, channel) = match crypto_channel {
214                ChannelType::Ecies(ecies) => {
215                    // We can create our EstablishedSecureChannel struct now and use the
216                    // convenient helpers which transparently decrypt on receival.
217                    let crypto_channel = EstablishedCryptoChannel::Ecies(ecies);
218                    let mut channel = Self { channel, crypto_channel };
219
220                    let response = channel.receive().await?;
221                    (response, channel)
222                }
223            };
224
225            trace!("Received the LOGIN OK message, maybe.");
226
227            if response == LOGIN_OK_MESSAGE {
228                Ok(channel)
229            } else {
230                Err(Error::SecureChannelMessage { expected: LOGIN_OK_MESSAGE, received: response })
231            }
232        }
233    }
234
235    /// Get the [`CheckCode`] which can be used to, out of band, verify that
236    /// both sides of the channel are indeed communicating with each other and
237    /// not with a 3rd party.
238    pub(super) fn check_code(&self) -> u8 {
239        self.crypto_channel.check_code()
240    }
241
242    /// Send the given message over to the other side.
243    ///
244    /// The message will be encrypted before it is sent over the rendezvous
245    /// channel.
246    pub(super) async fn send_json(&mut self, message: impl Serialize) -> Result<(), Error> {
247        let message = serde_json::to_string(&message)?;
248        self.send(&message).await
249    }
250
251    /// Attempt to receive a message from the channel.
252    ///
253    /// The message will be decrypted after it has been received over the
254    /// rendezvous channel.
255    pub(super) async fn receive_json<D: DeserializeOwned>(&mut self) -> Result<D, Error> {
256        let message = self.receive().await?;
257        Ok(serde_json::from_str(&message)?)
258    }
259
260    async fn send(&mut self, message: &str) -> Result<(), Error> {
261        let message = self.crypto_channel.seal(message);
262
263        Ok(self.channel.send(message).await?)
264    }
265
266    async fn receive(&mut self) -> Result<String, Error> {
267        let message = self.channel.receive().await?;
268        self.crypto_channel.open(&message)
269    }
270}
271
272#[cfg(all(test, not(target_family = "wasm")))]
273pub(super) mod test {
274    use std::{
275        sync::{
276            Arc, Mutex,
277            atomic::{AtomicU8, Ordering},
278        },
279        time::Duration,
280    };
281
282    use matrix_sdk_base::crypto::types::qr_login::QrCodeIntent;
283    use matrix_sdk_common::executor::spawn;
284    use matrix_sdk_test::async_test;
285    use ruma::time::Instant;
286    use serde_json::json;
287    use similar_asserts::assert_eq;
288    use url::Url;
289    use wiremock::{
290        Mock, MockGuard, MockServer, ResponseTemplate,
291        matchers::{method, path},
292    };
293
294    use super::{EstablishedSecureChannel, SecureChannel};
295    use crate::http_client::HttpClient;
296
297    #[allow(dead_code)]
298    pub struct MockedRendezvousServer {
299        pub homeserver_url: Url,
300        pub rendezvous_url: Url,
301        expiration: Duration,
302        content: Arc<Mutex<Option<String>>>,
303        created: Arc<Mutex<Option<Instant>>>,
304        etag: Arc<AtomicU8>,
305        post_guard: MockGuard,
306        put_guard: MockGuard,
307        get_guard: MockGuard,
308    }
309
310    impl MockedRendezvousServer {
311        pub async fn new(server: &MockServer, location: &str, expiration: Duration) -> Self {
312            let content: Arc<Mutex<Option<String>>> = Mutex::default().into();
313            let created: Arc<Mutex<Option<Instant>>> = Mutex::default().into();
314            let etag = Arc::new(AtomicU8::new(0));
315
316            let homeserver_url = Url::parse(&server.uri())
317                .expect("We should be able to parse the example homeserver");
318
319            let rendezvous_url = homeserver_url
320                .join(location)
321                .expect("We should be able to create a rendezvous URL");
322
323            let post_guard = server
324                .register_as_scoped(
325                    Mock::given(method("POST"))
326                        .and(path("/_matrix/client/unstable/org.matrix.msc4108/rendezvous"))
327                        .respond_with({
328                            *created.lock().unwrap() = Some(Instant::now());
329
330                            ResponseTemplate::new(200)
331                                .append_header("X-Max-Bytes", "10240")
332                                .append_header("ETag", "1")
333                                .append_header("Expires", "Wed, 07 Sep 2022 14:28:51 GMT")
334                                .append_header("Last-Modified", "Wed, 07 Sep 2022 14:27:51 GMT")
335                                .set_body_json(json!({
336                                    "url": rendezvous_url,
337                                }))
338                        }),
339                )
340                .await;
341
342            let put_guard = server
343                .register_as_scoped(
344                    Mock::given(method("PUT")).and(path("/abcdEFG12345")).respond_with({
345                        let content = content.clone();
346                        let created = created.clone();
347                        let etag = etag.clone();
348
349                        move |request: &wiremock::Request| {
350                            // Fail the request if the session has expired.
351                            if created.lock().unwrap().unwrap().elapsed() > expiration {
352                                return ResponseTemplate::new(404).set_body_json(json!({
353                                    "errcode": "M_NOT_FOUND",
354                                    "error": "This rendezvous session does not exist.",
355                                }));
356                            }
357
358                            *content.lock().unwrap() =
359                                Some(String::from_utf8(request.body.clone()).unwrap());
360                            let current_etag = etag.fetch_add(1, Ordering::SeqCst);
361
362                            ResponseTemplate::new(200)
363                                .append_header("ETag", (current_etag + 2).to_string())
364                                .append_header("Expires", "Wed, 07 Sep 2022 14:28:51 GMT")
365                                .append_header("Last-Modified", "Wed, 07 Sep 2022 14:27:51 GMT")
366                        }
367                    }),
368                )
369                .await;
370
371            let get_guard = server
372                .register_as_scoped(
373                    Mock::given(method("GET")).and(path("/abcdEFG12345")).respond_with({
374                        let content = content.clone();
375                        let created = created.clone();
376                        let etag = etag.clone();
377
378                        move |request: &wiremock::Request| {
379                            // Fail the request if the session has expired.
380                            if created.lock().unwrap().unwrap().elapsed() > expiration {
381                                return ResponseTemplate::new(404).set_body_json(json!({
382                                    "errcode": "M_NOT_FOUND",
383                                    "error": "This rendezvous session does not exist.",
384                                }));
385                            }
386
387                            let requested_etag = request.headers.get("if-none-match").map(|etag| {
388                                str::parse::<u8>(std::str::from_utf8(etag.as_bytes()).unwrap())
389                                    .unwrap()
390                            });
391
392                            let mut content = content.lock().unwrap();
393                            let current_etag = etag.load(Ordering::SeqCst);
394
395                            if requested_etag == Some(current_etag) || requested_etag.is_none() {
396                                let content = content.take();
397
398                                ResponseTemplate::new(200)
399                                    .append_header("ETag", (current_etag).to_string())
400                                    .append_header("Expires", "Wed, 07 Sep 2022 14:28:51 GMT")
401                                    .append_header("Last-Modified", "Wed, 07 Sep 2022 14:27:51 GMT")
402                                    .set_body_string(content.unwrap_or_default())
403                            } else {
404                                let etag = requested_etag.unwrap_or_default();
405
406                                ResponseTemplate::new(304)
407                                    .append_header("ETag", etag.to_string())
408                                    .append_header("Expires", "Wed, 07 Sep 2022 14:28:51 GMT")
409                                    .append_header("Last-Modified", "Wed, 07 Sep 2022 14:27:51 GMT")
410                            }
411                        }
412                    }),
413                )
414                .await;
415
416            Self {
417                expiration,
418                content,
419                created,
420                etag,
421                post_guard,
422                put_guard,
423                get_guard,
424                homeserver_url,
425                rendezvous_url,
426            }
427        }
428    }
429
430    #[async_test]
431    async fn test_creation() {
432        let server = MockServer::start().await;
433        let rendezvous_server =
434            MockedRendezvousServer::new(&server, "abcdEFG12345", Duration::MAX).await;
435
436        let client = HttpClient::new(reqwest::Client::new(), Default::default());
437        let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url)
438            .await
439            .expect("Alice should be able to create a secure channel.");
440
441        let qr_code_data = alice.qr_code_data().clone();
442
443        let bob_task = spawn(async move {
444            EstablishedSecureChannel::from_qr_code(
445                reqwest::Client::new(),
446                &qr_code_data,
447                QrCodeIntent::Login,
448            )
449            .await
450            .expect("Bob should be able to fully establish the secure channel.")
451        });
452
453        let alice_task = spawn(async move {
454            alice
455                .connect()
456                .await
457                .expect("Alice should be able to connect the established secure channel")
458        });
459
460        let bob = bob_task.await.unwrap();
461        let alice = alice_task.await.unwrap();
462
463        assert_eq!(alice.secure_channel.check_code(), bob.check_code());
464
465        let alice = alice
466            .confirm(bob.check_code())
467            .expect("Alice should be able to confirm the established secure channel.");
468
469        assert_eq!(bob.channel.rendezvous_info(), alice.channel.rendezvous_info());
470    }
471}