Discard room key if someone leaves a room for a non-leave reason e.g. ban

This commit is contained in:
Andy Balaam
2026-04-16 15:06:29 +01:00
committed by Andy Balaam
parent e278a99a20
commit d49736455b
2 changed files with 75 additions and 4 deletions
+9 -2
View File
@@ -2054,8 +2054,15 @@ impl Encryption {
return;
};
if !matches!(ev.membership_change(), MembershipChange::Left) || ev.sender == user_id {
// We can ignore non-leave events and those that we sent.
if matches!(
ev.membership_change(),
MembershipChange::Joined |
MembershipChange::Invited |
MembershipChange::KnockAccepted |
MembershipChange::InvitationAccepted |
MembershipChange::ProfileChanged { .. }
) || ev.sender == user_id {
// We can ignore events that did not remove us, and those that we sent.
return;
}
@@ -1133,7 +1133,7 @@ async fn test_history_share_on_invite_respects_history_visibility() -> Result<()
/// 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<()> {
async fn test_room_key_is_rotated_on_leave() -> Result<()> {
let alice_span = tracing::info_span!("alice");
let bob_span = tracing::info_span!("bob");
let charlie_span = tracing::info_span!("charlie");
@@ -1193,6 +1193,70 @@ async fn test_history_share_on_invite_room_key_rotation() -> Result<()> {
Ok(())
}
/// Test that when a user iis banned from 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_room_key_is_rotated_on_ban() -> 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, bob_room) =
create_room_and_invite_bob(&alice, &bob, &alice_span, &bob_span).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, event_m1_session_id) = send_m1(&bob_room, &bob_span).await?;
// 3. Alice invites Charlie; Charlie joins and receives the keys for M1.
invite_charlie_and_he_joins(
&alice,
&charlie,
&alice_room,
&event_id_a,
&alice_span,
&charlie_span,
)
.await?;
// 4. Charlie is banned.
alice_room.ban_user(charlie.user_id().unwrap(), None).instrument(alice_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, event_m2_session_id) = send_m2(&bob_room, &bob_span).await?;
assert_ne!(event_m1_session_id, event_m2_session_id, "Session was not rotated");
// 6. Charlie is unbanned and rejoins the room via ID.
alice_room.unban_user(charlie.user_id().unwrap(), None).await?;
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(())
}
async fn create_room_and_invite_bob(
alice: &SyncTokenAwareClient,
bob: &SyncTokenAwareClient,
@@ -1259,7 +1323,7 @@ async fn invite_charlie_and_he_joins(
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?;
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"