@@ -5,19 +5,26 @@ use assert_matches2::{assert_let, assert_matches};
55use assign:: assign;
66use futures:: { FutureExt , StreamExt , future, pin_mut} ;
77use matrix_sdk:: {
8- assert_decrypted_message_eq, assert_next_with_timeout,
8+ Room , assert_decrypted_message_eq, assert_next_with_timeout,
99 deserialized_responses:: TimelineEventKind ,
1010 encryption:: EncryptionSettings ,
11+ room:: power_levels:: RoomPowerLevelChanges ,
1112 ruma:: {
13+ EventId ,
1214 api:: client:: {
1315 room:: create_room:: v3:: { Request as CreateRoomRequest , RoomPreset } ,
1416 uiaa:: Password ,
1517 } ,
16- events:: room:: message:: RoomMessageEventContent ,
18+ events:: room:: {
19+ history_visibility:: { HistoryVisibility , RoomHistoryVisibilityEventContent } ,
20+ message:: RoomMessageEventContent ,
21+ } ,
1722 } ,
1823 timeout:: timeout,
1924} ;
20- use matrix_sdk_common:: deserialized_responses:: ProcessedToDeviceEvent ;
25+ use matrix_sdk_common:: deserialized_responses:: {
26+ ProcessedToDeviceEvent , UnableToDecryptReason :: MissingMegolmSession , WithheldCode ,
27+ } ;
2128use matrix_sdk_ui:: sync_service:: SyncService ;
2229use similar_asserts:: assert_eq;
2330use tracing:: { Instrument , info} ;
@@ -360,3 +367,173 @@ async fn test_history_share_on_invite_pin_violation() -> Result<()> {
360367
361368 Ok ( ( ) )
362369}
370+
371+ /// Test history sharing where some sessions are withheld.
372+ ///
373+ /// In this scenario we have three separate users:
374+ ///
375+ /// 1. Alice and Bob share a room, where the history visibility is set to
376+ /// "shared".
377+ /// 2. Bob sends a message. This will be "shareable".
378+ /// 3. Alice changes the history viz to "joined".
379+ /// 4. Alice changes the history viz back to "shared", but Bob doesn't (yet)
380+ /// receive the memo.
381+ /// 5. Bob sends a second message; the key is "unshareable" because Bob still
382+ /// thinks the history viz is "joined".
383+ /// 6. Bob syncs, and sends a third message; the key is now "shareable".
384+ /// 7. Alice invites Charlie.
385+ /// 8. Charlie joins the room. He should see Bob's first message; the second
386+ /// should have an appropriate withheld code from Alice; the third should be
387+ /// decryptable.
388+ ///
389+ /// This tests correct "withheld" code handling.
390+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 4 ) ]
391+ async fn test_transitive_history_share_with_withhelds ( ) -> Result < ( ) > {
392+ let alice_span = tracing:: info_span!( "alice" ) ;
393+ let bob_span = tracing:: info_span!( "bob" ) ;
394+ let charlie_span = tracing:: info_span!( "charlie" ) ;
395+
396+ let alice = create_encryption_enabled_client ( "alice" ) . instrument ( alice_span. clone ( ) ) . await ?;
397+ let bob = create_encryption_enabled_client ( "bob" ) . instrument ( bob_span. clone ( ) ) . await ?;
398+ let charlie =
399+ create_encryption_enabled_client ( "charlie" ) . instrument ( charlie_span. clone ( ) ) . await ?;
400+
401+ // 1. Alice creates a room, and enables encryption
402+ let alice_room = alice
403+ . create_room ( assign ! ( CreateRoomRequest :: new( ) , {
404+ preset: Some ( RoomPreset :: PublicChat ) ,
405+ } ) )
406+ . instrument ( alice_span. clone ( ) )
407+ . await ?;
408+ alice_room. enable_encryption ( ) . instrument ( alice_span. clone ( ) ) . await ?;
409+ // Allow regular users to send invites
410+ alice. sync_once ( ) . instrument ( alice_span. clone ( ) ) . await ?;
411+ alice_room
412+ . apply_power_level_changes ( RoomPowerLevelChanges { invite : Some ( 0 ) , ..Default :: default ( ) } )
413+ . instrument ( alice_span. clone ( ) )
414+ . await
415+ . expect ( "Should be able to set power levels" ) ;
416+
417+ info ! ( room_id = ?alice_room. room_id( ) , "Alice has created and enabled encryption in the room" ) ;
418+
419+ // ... and invites Bob to the room
420+ alice_room. invite_user_by_id ( bob. user_id ( ) . unwrap ( ) ) . instrument ( alice_span. clone ( ) ) . await ?;
421+
422+ // Bob joins
423+ bob. sync_once ( ) . instrument ( bob_span. clone ( ) ) . await ?;
424+
425+ let bob_room = bob
426+ . join_room_by_id ( alice_room. room_id ( ) )
427+ . instrument ( bob_span. clone ( ) )
428+ . await
429+ . expect ( "Bob should be able to accept the invitation from Alice" ) ;
430+
431+ // 2. Bob sends a message, which Alice should receive
432+ let assert_event_received = async |room : & Room , event_id : & EventId , expected_content : & str | {
433+ let event = room. event ( event_id, None ) . await . unwrap_or_else ( |err| {
434+ panic ! ( "Should receive Bob's event with content '{expected_content}': {err:?}" )
435+ } ) ;
436+ assert_decrypted_message_eq ! (
437+ event,
438+ expected_content,
439+ "The decrypted event should match the message Bob has sent"
440+ ) ;
441+ } ;
442+
443+ let bob_send_test_event = async |event_content : & str | {
444+ let bob_event_id = bob_room
445+ . send ( RoomMessageEventContent :: text_plain ( event_content) )
446+ . into_future ( )
447+ . instrument ( bob_span. clone ( ) )
448+ . await
449+ . expect ( "We should be able to send a message to the room" )
450+ . event_id ;
451+
452+ alice
453+ . sync_once ( )
454+ . instrument ( alice_span. clone ( ) )
455+ . await
456+ . expect ( "Alice should be able to sync" ) ;
457+
458+ assert_event_received ( & alice_room, & bob_event_id, event_content) . await ;
459+
460+ bob_event_id
461+ } ;
462+
463+ let event_id_1 = bob_send_test_event ( "Event 1" ) . await ;
464+
465+ // 3. Alice changes the history visibility to "joined"
466+ alice_room
467+ . send_state_event ( RoomHistoryVisibilityEventContent :: new ( HistoryVisibility :: Joined ) )
468+ . into_future ( )
469+ . instrument ( alice_span. clone ( ) )
470+ . await ?;
471+ bob. sync_once ( ) . instrument ( bob_span. clone ( ) ) . await ?;
472+ assert_eq ! ( bob_room. history_visibility( ) , Some ( HistoryVisibility :: Joined ) ) ;
473+
474+ // 4. Alice changes the history visibility back to "shared", but Bob doesn't
475+ // know about it.
476+ alice_room
477+ . send_state_event ( RoomHistoryVisibilityEventContent :: new ( HistoryVisibility :: Shared ) )
478+ . into_future ( )
479+ . instrument ( alice_span. clone ( ) )
480+ . await ?;
481+
482+ // 5. Bob sends a second message; the key is "unshareable" because Bob still
483+ // thinks the history viz is "joined".
484+ assert_eq ! ( bob_room. history_visibility( ) , Some ( HistoryVisibility :: Joined ) ) ;
485+ let event_id_2 = bob_send_test_event ( "Event 2" ) . await ;
486+
487+ // 6. Bob syncs, and sends a third message; the key is now "shareable".
488+ bob. sync_once ( ) . instrument ( bob_span. clone ( ) ) . await ?;
489+ assert_eq ! ( bob_room. history_visibility( ) , Some ( HistoryVisibility :: Shared ) ) ;
490+ let event_id_3 = bob_send_test_event ( "Event 3" ) . await ;
491+
492+ // 7. Alice invites Charlie.
493+ alice_room. invite_user_by_id ( charlie. user_id ( ) . unwrap ( ) ) . instrument ( alice_span. clone ( ) ) . await ?;
494+
495+ // Workaround for https://github.com/matrix-org/matrix-rust-sdk/issues/5770: Charlie needs a copy of
496+ // Alice's identity.
497+ charlie
498+ . encryption ( )
499+ . request_user_identity ( alice. user_id ( ) . unwrap ( ) )
500+ . instrument ( charlie_span. clone ( ) )
501+ . await ?;
502+
503+ // 8. Charlie joins the room
504+ charlie. sync_once ( ) . instrument ( charlie_span. clone ( ) ) . await ?;
505+ let charlie_room = charlie
506+ . join_room_by_id ( alice_room. room_id ( ) )
507+ . instrument ( charlie_span. clone ( ) )
508+ . await
509+ . expect ( "Charlie should be able to accept the invitation from Alice" ) ;
510+
511+ // Events 1 and 3 should be decryptable; 2 should be "history not shared".
512+ assert_event_received ( & charlie_room, & event_id_1, "Event 1" ) . await ;
513+ assert_event_received ( & charlie_room, & event_id_3, "Event 3" ) . await ;
514+ let event = charlie_room. event ( & event_id_2, None ) . await . expect ( "Should receive Bob's event 2" ) ;
515+ assert_let ! ( TimelineEventKind :: UnableToDecrypt { utd_info, .. } = event. kind) ;
516+ assert_eq ! (
517+ utd_info. reason,
518+ MissingMegolmSession { withheld_code: Some ( WithheldCode :: HistoryNotShared ) }
519+ ) ;
520+
521+ Ok ( ( ) )
522+ }
523+
524+ async fn create_encryption_enabled_client ( username : & str ) -> Result < SyncTokenAwareClient > {
525+ let encryption_settings =
526+ EncryptionSettings { auto_enable_cross_signing : true , ..Default :: default ( ) } ;
527+
528+ let client = SyncTokenAwareClient :: new (
529+ TestClientBuilder :: new ( username)
530+ . use_sqlite ( )
531+ . encryption_settings ( encryption_settings)
532+ . enable_share_history_on_invite ( true )
533+ . build ( )
534+ . await ?,
535+ ) ;
536+
537+ client. encryption ( ) . wait_for_e2ee_initialization_tasks ( ) . await ;
538+ Ok ( client)
539+ }
0 commit comments