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