From 7a26db66b51ec0e6bf696279898cd5dd454e199e Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Mon, 16 Mar 2026 11:02:28 +0000 Subject: [PATCH] tests: Assert room key is rotated when a member leaves the room Signed-off-by: Skye Elliot --- .../src/tests/e2ee/shared_history.rs | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs b/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs index b37cfeb71..2b82668b4 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs @@ -1127,6 +1127,115 @@ async fn test_history_share_on_invite_respects_history_visibility() -> Result<() Ok(()) } +/// Test that when a user leaves a room that uses history sharing, the room key +/// is rotated so they cannot decrypt future messages. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_history_share_on_invite_room_key_rotation() -> Result<()> { + let alice_span = tracing::info_span!("alice"); + let bob_span = tracing::info_span!("bob"); + let charlie_span = tracing::info_span!("charlie"); + + let alice = + create_encryption_enabled_client("alice", false).instrument(alice_span.clone()).await?; + let bob = create_encryption_enabled_client("bob", false).instrument(bob_span.clone()).await?; + let charlie = create_encryption_enabled_client("charlie", false) + .await + .expect("Failed to create Charlie's client"); + + // 1. Alice creates a room with `shared` history visibility and invites Bob. + let alice_room = alice + .create_room(assign!(CreateRoomRequest::new(), { + preset: Some(RoomPreset::PublicChat), + })) + .instrument(alice_span.clone()) + .await?; + alice_room.enable_encryption().instrument(alice_span.clone()).await?; + + alice_room.invite_user_by_id(bob.user_id().unwrap()).instrument(alice_span.clone()).await?; + + bob.sync_once().instrument(bob_span.clone()).await?; + let bob_room = bob.join_room_by_id(alice_room.room_id()).instrument(bob_span.clone()).await?; + + alice.sync_once().instrument(alice_span.clone()).await?; + + // 2. Bob sends M1, which Charlie should be able to read later as Alice will + // send them a key bundle. + let event_id_a = bob_room + .send(RoomMessageEventContent::text_plain("Charlie is cool!")) + .into_future() + .instrument(bob_span.clone()) + .await? + .response + .event_id; + + // Store the session ID for later comparison. + let event_m1 = bob_room.event(&event_id_a, None).instrument(bob_span.clone()).await?; + let event_m1_session_id = event_m1 + .encryption_info() + .and_then(|info| info.session_id()) + .expect("Bob should be able to check the session ID of event M1"); + + // 3. Alice invites Charlie; Charlie joins and receives the keys for M1. + alice.sync_once().instrument(alice_span.clone()).await?; + alice_room.invite_user_by_id(charlie.user_id().unwrap()).instrument(alice_span.clone()).await?; + + let sync_response = charlie.sync_once().instrument(charlie_span.clone()).await?; + assert_received_room_key_bundle(sync_response); + + let charlie_room = + charlie.join_room_by_id(alice_room.room_id()).instrument(charlie_span.clone()).await?; + + charlie.sync_once().instrument(charlie_span.clone()).await?; + + // Sanity check: Charlie can decrypt message M1 via the bundle. + let event_a = charlie_room.event(&event_id_a, None).instrument(charlie_span.clone()).await?; + assert!( + event_a.encryption_info().is_some(), + "Charlie should be able to decrypt message M1 via the key bundle" + ); + + // 4. Charlie leaves the room. + charlie_room.leave().instrument(charlie_span.clone()).await?; + + // Bob syncs to learn about Charlie's departure, which should trigger key + // rotation. + bob.sync_once().instrument(bob_span.clone()).await?; + + // 5. Bob sends M2. Because key rotation should have been performed, this should + // be using a fresh session that hasn't been shared with Charlie. + let event_id_b = bob_room + .send(RoomMessageEventContent::text_plain("Charlie is mean!")) + .into_future() + .instrument(bob_span.clone()) + .await? + .response + .event_id; + + // Ensure the two session IDs of M1 and M2 are different + let event_b = bob_room.event(&event_id_b, None).instrument(bob_span.clone()).await?; + let event_m2_session_id = event_b + .encryption_info() + .and_then(|info| info.session_id()) + .expect("Bob should be able to check the session ID of event M2"); + + assert_ne!(event_m1_session_id, event_m2_session_id, "Session was not rotated"); + + // 6. Charlie rejoins the room via ID. + charlie.sync_once().instrument(charlie_span.clone()).await?; + let charlie_room = + charlie.join_room_by_id(alice_room.room_id()).instrument(charlie_span.clone()).await?; + + // 7. Charlie attempts to decrypt M2. He should not be able to, because the + // session was rotated after he left the room. + let event_b = charlie_room.event(&event_id_b, None).instrument(charlie_span.clone()).await?; + assert!( + event_b.encryption_info().is_none(), + "Charlie should not be able to decrypt message M2 after rejoining" + ); + + Ok(()) +} + /// Creates a new encryption-enabled client with the given username and /// settings. ///