@@ -363,14 +363,18 @@ pub mod vodozemac {
363
363
///
364
364
/// ## Uploading identity and one-time keys.
365
365
///
366
- /// The first step is to announce the support for it to other users in the
367
- /// Matrix network. This involves publishing your long-term device keys and a
368
- /// set of one-time prekeys to the homeserver. This information is used by other
369
- /// devices to encrypt messages specifically for your device.
366
+ /// To enable end-to-end encryption in a Matrix client, the first step is to
367
+ /// announce the support for it to other users in the network. This is done by
368
+ /// publishing the client's long-term device keys and a set of one-time prekeys
369
+ /// to the Matrix homeserver. The homeserver then makes this information
370
+ /// available to other devices in the network.
371
+ ///
372
+ /// The long-term device keys and one-time prekeys allow other devices to
373
+ /// encrypt messages specifically for your device.
370
374
///
371
375
/// To achieve this, you will need to extract any requests that need to be sent
372
376
/// to the homeserver from the [`OlmMachine`] and send them to the homeserver.
373
- /// The following snipped showcases how to achieve this using the
377
+ /// The following snippet showcases how to achieve this using the
374
378
/// [`OlmMachine::outgoing_requests()`] method:
375
379
///
376
380
/// ```no_run
@@ -389,42 +393,186 @@ pub mod vodozemac {
389
393
/// let outgoing_requests = machine.outgoing_requests().await?;
390
394
///
391
395
/// // Send each request to the server and push the response into the state machine.
396
+ /// // You can safely send these requests out in parallel.
392
397
/// for request in outgoing_requests {
393
- /// // You can safely send out these requests out in parallel.
394
398
/// let request_id = request.request_id();
399
+ /// // Send the request to the server and await a response.
395
400
/// let response = send_request(request).await?;
401
+ /// // Push the response into the state machine.
396
402
/// machine.mark_request_as_sent(&request_id, &response).await?;
397
403
/// }
398
404
/// # Ok(())
399
405
/// # }
400
406
/// ```
401
407
///
402
- /// ## Receiving room keys and related changes
408
+ /// It's important to note that the outgoing requests method in the
409
+ /// [`OlmMachine`], while thread-safe, may return the same request multiple
410
+ /// times if it is called multiple times before the request has been marked as
411
+ /// sent. To prevent this issue, it is advisable to encapsulate the outgoing
412
+ /// request handling logic into a separate helper method and protect it from
413
+ /// being called multiple times concurrently using a lock.
414
+ ///
415
+ /// This helps to ensure that the request is only handled once and prevents
416
+ /// multiple identical requests from being sent.
403
417
///
418
+ /// Additionally, if an error occurs while sending a request using the
419
+ /// [`OlmMachine::outgoing_requests()`] method, the request will be
420
+ /// naturally retried the next time the method is called.
421
+ ///
422
+ /// A more complete example, which uses a helper method, might look like this:
404
423
/// ```no_run
405
424
/// # use std::collections::BTreeMap;
425
+ /// # use ruma::api::client::keys::upload_keys::v3::Response;
426
+ /// # use anyhow::Result;
427
+ /// # use matrix_sdk_crypto::{OlmMachine, OutgoingRequest};
428
+ /// # async fn send_request(request: &OutgoingRequest) -> Result<Response> {
429
+ /// # let response = unimplemented!();
430
+ /// # Ok(response)
431
+ /// # }
432
+ /// # #[tokio::main]
433
+ /// # async fn main() -> Result<()> {
434
+ /// struct Client {
435
+ /// outgoing_requests_lock: tokio::sync::Mutex<()>,
436
+ /// olm_machine: OlmMachine,
437
+ /// }
438
+ ///
439
+ /// async fn process_outgoing_requests(client: &Client) -> Result<()> {
440
+ /// // Let's acquire a lock so we know that we don't send out the same request out multiple
441
+ /// // times.
442
+ /// let guard = client.outgoing_requests_lock.lock().await;
443
+ ///
444
+ /// for request in client.olm_machine.outgoing_requests().await? {
445
+ /// let request_id = request.request_id();
446
+ ///
447
+ /// match send_request(&request).await {
448
+ /// Ok(response) => {
449
+ /// client.olm_machine.mark_request_as_sent(&request_id, &response).await?;
450
+ /// }
451
+ /// Err(error) => {
452
+ /// // It's OK to ignore transient HTTP errors since requests will be retried.
453
+ /// eprintln!(
454
+ /// "Error while sending out a end-to-end encryption \
455
+ /// related request: {error:?}"
456
+ /// );
457
+ /// }
458
+ /// }
459
+ /// }
460
+ ///
461
+ /// Ok(())
462
+ /// }
463
+ /// # Ok(())
464
+ /// # }
465
+ /// ```
466
+ ///
467
+ /// Once we have the helper method that processes our outgoing requests we can
468
+ /// structure our sync method as follows:
469
+ ///
470
+ /// ```no_run
406
471
/// # use anyhow::Result;
407
472
/// # use matrix_sdk_crypto::OlmMachine;
408
473
/// # #[tokio::main]
409
474
/// # async fn main() -> Result<()> {
410
- /// # let to_device_events = Vec::new();
411
- /// # let changed_devices = Default::default();
412
- /// # let one_time_key_counts = BTreeMap::default();
413
- /// # let unused_fallback_keys = Some(Vec::new());
414
- /// # let machine: OlmMachine = unimplemented!();
415
- /// // Push changes that the server sent to us in a sync response.
416
- /// let decrypted_to_device = machine
417
- /// .receive_sync_changes(
418
- /// to_device_events,
419
- /// &changed_devices,
420
- /// &one_time_key_counts,
421
- /// unused_fallback_keys.as_deref(),
422
- /// )
423
- /// .await?;
475
+ /// # struct Client {
476
+ /// # outgoing_requests_lock: tokio::sync::Mutex<()>,
477
+ /// # olm_machine: OlmMachine,
478
+ /// # }
479
+ /// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
480
+ /// # unimplemented!();
481
+ /// # }
482
+ /// # async fn send_out_sync_request(client: &Client) -> Result<()> {
483
+ /// # unimplemented!();
484
+ /// # }
485
+ /// async fn sync(client: &Client) -> Result<()> {
486
+ /// // This is happening at the top of the method so we advertise our
487
+ /// // end-to-end encryption capabilities as soon as possible.
488
+ /// process_outgoing_requests(client).await?;
489
+ ///
490
+ /// // We can sync with the homeserver now.
491
+ /// let response = send_out_sync_request(client).await?;
492
+ ///
493
+ /// // Process the sync response here.
494
+ ///
495
+ /// Ok(())
496
+ /// }
424
497
/// # Ok(())
425
498
/// # }
426
499
/// ```
427
500
///
501
+ /// ## Receiving room keys and related changes
502
+ ///
503
+ /// The next step in our implementation is to forward messages that were sent
504
+ /// directly to the client's device, and state updates about the one-time
505
+ /// prekeys, to the [`OlmMachine`]. This is achieved using
506
+ /// the [`OlmMachine::receive_sync_changes()`] method.
507
+ ///
508
+ /// The method performs two tasks:
509
+ ///
510
+ /// 1. It processes and, if necessary, decrypts each [to-device] event that was
511
+ /// pushed into it, and returns the decrypted events. The original events are
512
+ /// replaced with their decrypted versions.
513
+ ///
514
+ /// 2. It produces internal state changes that may trigger the creation of new
515
+ /// outgoing requests. For example, if the server informs the client that its
516
+ /// one-time prekeys have been depleted, the OlmMachine will create an outgoing
517
+ /// request to replenish them.
518
+ ///
519
+ /// Our updated sync method now looks like this:
520
+ ///
521
+ /// ```no_run
522
+ /// # use anyhow::Result;
523
+ /// # use matrix_sdk_crypto::OlmMachine;
524
+ /// # use ruma::api::client::sync::sync_events::v3::Response;
525
+ /// # #[tokio::main]
526
+ /// # async fn main() -> Result<()> {
527
+ /// # struct Client {
528
+ /// # outgoing_requests_lock: tokio::sync::Mutex<()>,
529
+ /// # olm_machine: OlmMachine,
530
+ /// # }
531
+ /// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
532
+ /// # unimplemented!();
533
+ /// # }
534
+ /// # async fn send_out_sync_request(client: &Client) -> Result<Response> {
535
+ /// # unimplemented!();
536
+ /// # }
537
+ /// async fn sync(client: &Client) -> Result<()> {
538
+ /// process_outgoing_requests(client).await?;
539
+ ///
540
+ /// let response = send_out_sync_request(client).await?;
541
+ ///
542
+ /// // Push the sync changes into the OlmMachine, make sure that this is
543
+ /// // happening before the `next_batch` token of the sync is persisted.
544
+ /// let to_device_events = client
545
+ /// .olm_machine
546
+ /// .receive_sync_changes(
547
+ /// response.to_device.events,
548
+ /// &response.device_lists,
549
+ /// &response.device_one_time_keys_count,
550
+ /// response.device_unused_fallback_key_types.as_deref(),
551
+ /// )
552
+ /// .await?;
553
+ ///
554
+ /// // Send out the outgoing requests that receiving sync changes produced.
555
+ /// process_outgoing_requests(client).await?;
556
+ ///
557
+ /// // Process the rest of the sync response here.
558
+ ///
559
+ /// Ok(())
560
+ /// }
561
+ /// # Ok(())
562
+ /// # }
563
+ /// ```
564
+ ///
565
+ /// It is important to note that the names of the fields in the response shown
566
+ /// in the example match the names of the fields specified in the [sync]
567
+ /// response specification.
568
+ ///
569
+ /// It is critical to note that due to the ephemeral nature of to-device
570
+ /// events[[1]](https://spec.matrix.org/unstable/client-server-api/#server-behaviour-4),
571
+ /// it is important to process these events before persisting the `next_batch`
572
+ /// sync token. This is because if the `next_batch` sync token is persisted
573
+ /// before processing the to-device events, some messages might be lost, leading
574
+ /// to decryption failures.
575
+ ///
428
576
/// ## Decrypting room events
429
577
///
430
578
/// The final step in the decryption process is to decrypt the room events that
@@ -433,8 +581,8 @@ pub mod vodozemac {
433
581
/// exchanged between devices to decrypt the events. The decrypted events can
434
582
/// then be processed and displayed to the user in the Matrix client.
435
583
///
436
- /// Room events can be decrypted using the [`OlmMachine::decrypt_room_event()`]
437
- /// method.
584
+ /// Room message [ events] can be decrypted using the
585
+ /// [`OlmMachine::decrypt_room_event()`] method:
438
586
///
439
587
/// ```no_run
440
588
/// # use std::collections::BTreeMap;
@@ -451,6 +599,16 @@ pub mod vodozemac {
451
599
/// # }
452
600
/// ```
453
601
///
602
+ /// It's worth mentioning that the [`OlmMachine::decrypt_room_event()`] method
603
+ /// is designed to be thread-safe and can be safely called concurrently. This
604
+ /// means that room message [events] can be processed in parallel, improving the
605
+ /// overall efficiency of the end-to-end encryption implementation.
606
+ ///
607
+ /// By allowing room message [events] to be processed concurrently, the client's
608
+ /// implementation can take full advantage of the capabilities of modern
609
+ /// hardware and achieve better performance, especially when dealing with a
610
+ /// large number of messages at once.
611
+ ///
454
612
/// # Encryption
455
613
///
456
614
/// TODO
@@ -475,6 +633,20 @@ pub mod vodozemac {
475
633
///
476
634
/// ## Tracking users
477
635
///
636
+ /// [Tracking the device list for a user]
637
+ ///
638
+ /// ```mermaid
639
+ /// sequenceDiagram
640
+ /// actor Alice
641
+ /// participant Homeserver
642
+ ///
643
+ /// Alice->>Homeserver: Request to sync changes
644
+ /// Homeserver->>Alice: Users whose device list has changed
645
+ /// Alice->>Alice: Mark user's devicel list as outdated
646
+ /// Alice->>Homeserver: Ask the server for the new device list of all the outdated users
647
+ /// Alice->>Alice: Update the local device list and mark the users as up-to-date
648
+ /// ```
649
+ ///
478
650
/// ```no_run
479
651
/// # use std::collections::{BTreeMap, HashSet};
480
652
/// # use anyhow::Result;
@@ -490,6 +662,7 @@ pub mod vodozemac {
490
662
/// # }
491
663
/// ```
492
664
///
665
+ ///
493
666
/// TODO
494
667
///
495
668
/// ## Establishing end-to-end encrypted channels
@@ -570,7 +743,7 @@ pub mod vodozemac {
570
743
/// # let event = unimplemented!();
571
744
/// # let machine: OlmMachine = unimplemented!();
572
745
/// // Decrypt each room event you'd like to display to the user using this method.
573
- /// let decrypted = machine.decrypt_room_event(event, room_id)?;
746
+ /// let decrypted = machine.decrypt_room_event(event, room_id).await ?;
574
747
/// # Ok(())
575
748
/// # }
576
749
/// ```
@@ -583,6 +756,10 @@ pub mod vodozemac {
583
756
///
584
757
/// TODO
585
758
///
759
+ /// # Room key backups
760
+ ///
761
+ /// TODO
762
+ ///
586
763
/// [Matrix]: https://matrix.org/
587
764
/// [Olm]: https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md
588
765
/// [Diffie-Hellman]: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
@@ -593,6 +770,9 @@ pub mod vodozemac {
593
770
/// [client-server specification]: https://matrix.org/docs/spec/client_server/
594
771
/// [forward secrecy]: https://en.wikipedia.org/wiki/Forward_secrecy
595
772
/// [replay attacks]: https://en.wikipedia.org/wiki/Replay_attack
596
- ///
773
+ /// [Tracking the device list for a user]: https://spec.matrix.org/unstable/client-server-api/#tracking-the-device-list-for-a-user
597
774
/// [X3DH]: https://signal.org/docs/specifications/x3dh/
775
+ /// [to-device]: https://spec.matrix.org/unstable/client-server-api/#send-to-device-messaging
776
+ /// [sync]: https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv3sync
777
+ /// [events]: https://spec.matrix.org/unstable/client-server-api/#events
598
778
pub mod tutorial { }
0 commit comments