|
| 1 | +use std::{ops::Deref, time::Duration}; |
| 2 | + |
| 3 | +use anyhow::Result; |
| 4 | +use assert_matches2::assert_let; |
| 5 | +use assign::assign; |
| 6 | +use futures::{FutureExt, StreamExt, pin_mut}; |
| 7 | +use matrix_sdk::{ |
| 8 | + assert_let_decrypted_state_event_content, |
| 9 | + encryption::EncryptionSettings, |
| 10 | + ruma::{ |
| 11 | + api::client::room::create_room::v3::{Request as CreateRoomRequest, RoomPreset}, |
| 12 | + events::AnyStateEventContent, |
| 13 | + }, |
| 14 | +}; |
| 15 | +use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent; |
| 16 | +use matrix_sdk_ui::sync_service::SyncService; |
| 17 | +use similar_asserts::assert_eq; |
| 18 | +use tracing::{Instrument, info}; |
| 19 | + |
| 20 | +use crate::helpers::{SyncTokenAwareClient, TestClientBuilder}; |
| 21 | + |
| 22 | +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] |
| 23 | +async fn test_e2ee_state_events() -> Result<()> { |
| 24 | + let alice_span = tracing::info_span!("alice"); |
| 25 | + let bob_span = tracing::info_span!("bob"); |
| 26 | + |
| 27 | + let encryption_settings = |
| 28 | + EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() }; |
| 29 | + |
| 30 | + let alice = TestClientBuilder::new("alice") |
| 31 | + .use_sqlite() |
| 32 | + .encryption_settings(encryption_settings) |
| 33 | + .enable_share_history_on_invite(true) |
| 34 | + .build() |
| 35 | + .await?; |
| 36 | + |
| 37 | + let sync_service_span = tracing::info_span!(parent: &alice_span, "sync_service"); |
| 38 | + let alice_sync_service = SyncService::builder(alice.clone()) |
| 39 | + .with_parent_span(sync_service_span) |
| 40 | + .build() |
| 41 | + .await |
| 42 | + .expect("Could not build alice sync service"); |
| 43 | + |
| 44 | + alice.encryption().wait_for_e2ee_initialization_tasks().await; |
| 45 | + alice_sync_service.start().await; |
| 46 | + |
| 47 | + let bob = SyncTokenAwareClient::new( |
| 48 | + TestClientBuilder::new("bob") |
| 49 | + .http_proxy("http://localhost:8080/".to_owned()) |
| 50 | + .encryption_settings(encryption_settings) |
| 51 | + .enable_share_history_on_invite(true) |
| 52 | + .build() |
| 53 | + .await?, |
| 54 | + ); |
| 55 | + |
| 56 | + // Alice creates an encrypted room ... |
| 57 | + let alice_room = alice |
| 58 | + .create_room(assign!(CreateRoomRequest::new(), { |
| 59 | + name: Some("Cat Photos".to_owned()), |
| 60 | + preset: Some(RoomPreset::PublicChat), |
| 61 | + })) |
| 62 | + .await?; |
| 63 | + alice_room.enable_encryption_with_state().await?; |
| 64 | + |
| 65 | + // HACK: wait for sync |
| 66 | + let _ = tokio::time::sleep(Duration::from_secs(3)).await; |
| 67 | + |
| 68 | + // (sanity checks) |
| 69 | + assert!(alice_room.encryption_state().is_encrypted(), "Encryption was not enabled."); |
| 70 | + assert!( |
| 71 | + alice_room.encryption_state().is_state_encrypted(), |
| 72 | + "State encryption was not enabled." |
| 73 | + ); |
| 74 | + |
| 75 | + info!(room_id = ?alice_room.room_id(), "Alice has created and enabled encryption in the room"); |
| 76 | + |
| 77 | + // ... and changes the room name |
| 78 | + let rename_event_id = alice_room |
| 79 | + .set_name("Dog Photos".to_owned()) |
| 80 | + .await |
| 81 | + .expect("We should be able to rename the room") |
| 82 | + .event_id; |
| 83 | + |
| 84 | + let bundle_stream = bob |
| 85 | + .encryption() |
| 86 | + .historic_room_key_stream() |
| 87 | + .await |
| 88 | + .expect("We should be able to get the bundle stream"); |
| 89 | + |
| 90 | + // Alice invites Bob to the room |
| 91 | + alice_room.invite_user_by_id(bob.user_id().unwrap()).await?; |
| 92 | + |
| 93 | + // Alice is done. Bob has been invited and the room key bundle should have been |
| 94 | + // sent out. Let's stop syncing so the logs contain less noise. |
| 95 | + alice_sync_service.stop().await; |
| 96 | + |
| 97 | + let bob_response = bob.sync_once().instrument(bob_span.clone()).await?; |
| 98 | + |
| 99 | + // Bob should have received a to-device event with the payload |
| 100 | + assert_eq!(bob_response.to_device.len(), 1); |
| 101 | + let to_device_event = &bob_response.to_device[0]; |
| 102 | + assert_let!(ProcessedToDeviceEvent::Decrypted { raw, .. } = to_device_event); |
| 103 | + assert_eq!( |
| 104 | + raw.get_field::<String>("type").unwrap().unwrap(), |
| 105 | + "io.element.msc4268.room_key_bundle" |
| 106 | + ); |
| 107 | + |
| 108 | + bob.get_room(alice_room.room_id()).expect("Bob should have received the invite"); |
| 109 | + |
| 110 | + pin_mut!(bundle_stream); |
| 111 | + |
| 112 | + let info = bundle_stream |
| 113 | + .next() |
| 114 | + .now_or_never() |
| 115 | + .flatten() |
| 116 | + .expect("We should be notified about the received bundle"); |
| 117 | + |
| 118 | + assert_eq!(Some(info.sender.deref()), alice.user_id()); |
| 119 | + assert_eq!(info.room_id, alice_room.room_id()); |
| 120 | + |
| 121 | + let bob_room = bob |
| 122 | + .join_room_by_id(alice_room.room_id()) |
| 123 | + .instrument(bob_span.clone()) |
| 124 | + .await |
| 125 | + .expect("Bob should be able to accept the invitation from Alice"); |
| 126 | + |
| 127 | + // Sync the room, so the rename event arrives. |
| 128 | + let _ = bob.sync_once().instrument(bob_span.clone()).await?; |
| 129 | + |
| 130 | + // Check it has been applied. |
| 131 | + assert_eq!( |
| 132 | + "Dog Photos", |
| 133 | + bob_room.name().unwrap(), |
| 134 | + "Bob's copy of the room name should have updated." |
| 135 | + ); |
| 136 | + |
| 137 | + // Let's also check we can inspect the payload manually. |
| 138 | + let rename_event = bob_room |
| 139 | + .event(&rename_event_id, None) |
| 140 | + .instrument(bob_span.clone()) |
| 141 | + .await |
| 142 | + .expect("Bob should be able to fetch the historic event."); |
| 143 | + |
| 144 | + assert_let_decrypted_state_event_content!( |
| 145 | + AnyStateEventContent::RoomName(content) = rename_event |
| 146 | + ); |
| 147 | + |
| 148 | + assert_eq!("Dog Photos", content.name); |
| 149 | + |
| 150 | + Ok(()) |
| 151 | +} |
0 commit comments