@@ -37,6 +37,7 @@ use matrix_sdk_ui::{
3737 } ,
3838} ;
3939use similar_asserts:: assert_eq;
40+ use tempfile:: tempdir;
4041use tracing:: { Instrument , info} ;
4142
4243use crate :: {
@@ -1127,6 +1128,137 @@ async fn test_history_share_on_invite_respects_history_visibility() -> Result<()
11271128 Ok ( ( ) )
11281129}
11291130
1131+ /// Test that when a user leaves a room that uses history sharing, the room key
1132+ /// is rotated so they cannot decrypt future messages.
1133+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 4 ) ]
1134+ async fn test_history_sharing_room_key_rotation ( ) -> Result < ( ) > {
1135+ let alice_span = tracing:: info_span!( "alice" ) ;
1136+ let bob_span = tracing:: info_span!( "bob" ) ;
1137+ let charlie_span = tracing:: info_span!( "charlie" ) ;
1138+
1139+ let alice =
1140+ create_encryption_enabled_client ( "alice" , false ) . instrument ( alice_span. clone ( ) ) . await ?;
1141+ let bob = create_encryption_enabled_client ( "bob" , false ) . instrument ( bob_span. clone ( ) ) . await ?;
1142+
1143+ // We create Charlie's client manually to allow us to recreate their client
1144+ // later.
1145+ let charlie_sqlite_dir = tempdir ( ) . unwrap ( ) ;
1146+ let charlie = SyncTokenAwareClient :: new (
1147+ TestClientBuilder :: new ( "charlie" )
1148+ . use_sqlite_dir ( charlie_sqlite_dir. path ( ) )
1149+ . encryption_settings ( EncryptionSettings {
1150+ auto_enable_cross_signing : true ,
1151+ ..Default :: default ( )
1152+ } )
1153+ . enable_share_history_on_invite ( true )
1154+ . build ( )
1155+ . await ?,
1156+ ) ;
1157+
1158+ // 1. Alice creates a room with `shared` history visibility and invites Bob.
1159+ let alice_room = alice
1160+ . create_room ( assign ! ( CreateRoomRequest :: new( ) , {
1161+ preset: Some ( RoomPreset :: PublicChat ) ,
1162+ } ) )
1163+ . instrument ( alice_span. clone ( ) )
1164+ . await ?;
1165+ alice_room. enable_encryption ( ) . instrument ( alice_span. clone ( ) ) . await ?;
1166+
1167+ // Allow regular users to send invites so Bob can invite Charlie.
1168+ alice. sync_once ( ) . instrument ( alice_span. clone ( ) ) . await ?;
1169+ alice_room
1170+ . apply_power_level_changes ( RoomPowerLevelChanges { invite : Some ( 0 ) , ..Default :: default ( ) } )
1171+ . instrument ( alice_span. clone ( ) )
1172+ . await ?;
1173+
1174+ alice_room. invite_user_by_id ( bob. user_id ( ) . unwrap ( ) ) . instrument ( alice_span. clone ( ) ) . await ?;
1175+
1176+ bob. sync_once ( ) . instrument ( bob_span. clone ( ) ) . await ?;
1177+ let bob_room = bob. join_room_by_id ( alice_room. room_id ( ) ) . instrument ( bob_span. clone ( ) ) . await ?;
1178+
1179+ alice. sync_once ( ) . instrument ( alice_span. clone ( ) ) . await ?;
1180+
1181+ // 2. Alice sends message A, which Charlie should be able to read later as Bob
1182+ // will send them a key bundle.
1183+ let event_id_a = alice_room
1184+ . send ( RoomMessageEventContent :: text_plain ( "Charlie is cool!" ) )
1185+ . into_future ( )
1186+ . instrument ( alice_span. clone ( ) )
1187+ . await ?
1188+ . response
1189+ . event_id ;
1190+
1191+ // 3. Bob invites Charlie; Charlie joins and receives message A via the bundle.
1192+ bob. sync_once ( ) . instrument ( bob_span. clone ( ) ) . await ?;
1193+ bob_room. invite_user_by_id ( charlie. user_id ( ) . unwrap ( ) ) . instrument ( bob_span. clone ( ) ) . await ?;
1194+
1195+ let sync_response = charlie. sync_once ( ) . instrument ( charlie_span. clone ( ) ) . await ?;
1196+ assert_received_room_key_bundle ( sync_response) ;
1197+
1198+ let charlie_room =
1199+ charlie. join_room_by_id ( alice_room. room_id ( ) ) . instrument ( charlie_span. clone ( ) ) . await ?;
1200+
1201+ charlie. sync_once ( ) . instrument ( charlie_span. clone ( ) ) . await ?;
1202+
1203+ // Sanity check: Charlie can decrypt message A via the bundle.
1204+ let event_a = charlie_room. event ( & event_id_a, None ) . instrument ( charlie_span. clone ( ) ) . await ?;
1205+ assert ! (
1206+ event_a. encryption_info( ) . is_some( ) ,
1207+ "Charlie should be able to decrypt message A via the key bundle"
1208+ ) ;
1209+
1210+ // 4. Charlie leaves the room.
1211+ charlie_room. leave ( ) . instrument ( charlie_span. clone ( ) ) . await ?;
1212+
1213+ // Alice syncs to learn about Charlie's departure, which should trigger key
1214+ // rotation.
1215+ alice. sync_once ( ) . instrument ( alice_span. clone ( ) ) . await ?;
1216+
1217+ // 5. Alice sends message B. Because key rotation should have been performed,
1218+ // this should be using a fresh session that hasn't been shared with Charlie.
1219+ let event_id_b = alice_room
1220+ . send ( RoomMessageEventContent :: text_plain ( "Charlie is mean!" ) )
1221+ . into_future ( )
1222+ . instrument ( alice_span. clone ( ) )
1223+ . await ?
1224+ . response
1225+ . event_id ;
1226+
1227+ // 6. We re-create Charlie, since we can't disable history sharing on an
1228+ // existing client.
1229+ let charlie = SyncTokenAwareClient :: new (
1230+ TestClientBuilder :: new ( "charlie" )
1231+ . use_sqlite_dir ( charlie_sqlite_dir. path ( ) )
1232+ . encryption_settings ( EncryptionSettings {
1233+ auto_enable_cross_signing : true ,
1234+ ..Default :: default ( )
1235+ } )
1236+ // Explicitly disable history sharing so we test only against keys received
1237+ // in the first key bundle.
1238+ . enable_share_history_on_invite ( false )
1239+ . duplicate ( & * charlie)
1240+ . await ?,
1241+ ) ;
1242+
1243+ // 7. Bob re-invites Charlie to the room, who joins.
1244+ bob. sync_once ( ) . instrument ( bob_span. clone ( ) ) . await ?;
1245+ bob_room. invite_user_by_id ( charlie. user_id ( ) . unwrap ( ) ) . instrument ( bob_span. clone ( ) ) . await ?;
1246+
1247+ charlie. sync_once ( ) . instrument ( charlie_span. clone ( ) ) . await ?;
1248+ let charlie_room =
1249+ charlie. join_room_by_id ( alice_room. room_id ( ) ) . instrument ( charlie_span. clone ( ) ) . await ?;
1250+
1251+ // 8. Charlie attempts to decrypt message B. He should not be able to, because
1252+ // the session was rotated after he left the room.
1253+ let event_b = charlie_room. event ( & event_id_b, None ) . instrument ( charlie_span. clone ( ) ) . await ?;
1254+ assert ! (
1255+ event_b. encryption_info( ) . is_none( ) ,
1256+ "Charlie should not be able to decrypt message B after being re-invited"
1257+ ) ;
1258+
1259+ Ok ( ( ) )
1260+ }
1261+
11301262/// Creates a new encryption-enabled client with the given username and
11311263/// settings.
11321264///
0 commit comments