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