matrix_sdk/authentication/oauth/qrcode/secure_channel/
mod.rs1use 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 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 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
129pub(super) struct AlmostEstablishedSecureChannel {
132 secure_channel: EstablishedSecureChannel,
133}
134
135impl AlmostEstablishedSecureChannel {
136 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 #[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 (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 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 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 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 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 pub(super) fn check_code(&self) -> u8 {
239 self.crypto_channel.check_code()
240 }
241
242 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 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 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 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}