@@ -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,172 @@ 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
434+ . event ( event_id, None )
435+ . await
436+ . expect ( & format ! ( "Should receive Bob's event with content '{}'" , expected_content) ) ;
437+ assert_decrypted_message_eq ! (
438+ event,
439+ expected_content,
440+ "The decrypted event should match the message Bob has sent"
441+ ) ;
442+ } ;
443+
444+ let bob_send_test_event = async |event_content : & str | {
445+ let bob_event_id = bob_room
446+ . send ( RoomMessageEventContent :: text_plain ( event_content) )
447+ . into_future ( )
448+ . instrument ( bob_span. clone ( ) )
449+ . await
450+ . expect ( "We should be able to send a message to the room" )
451+ . event_id ;
452+
453+ alice
454+ . sync_once ( )
455+ . instrument ( alice_span. clone ( ) )
456+ . await
457+ . expect ( "Alice should be able to sync" ) ;
458+
459+ assert_event_received ( & alice_room, & bob_event_id, event_content) . await ;
460+
461+ bob_event_id
462+ } ;
463+
464+ let event_id_1 = bob_send_test_event ( "Event 1" ) . await ;
465+
466+ // 3. Alice changes the history visibility to "joined"
467+ alice_room
468+ . send_state_event ( RoomHistoryVisibilityEventContent :: new ( HistoryVisibility :: Joined ) )
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+ . instrument ( alice_span. clone ( ) )
479+ . await ?;
480+
481+ // 5. Bob sends a second message; the key is "unshareable" because Bob still
482+ // thinks the history viz is "joined".
483+ assert_eq ! ( bob_room. history_visibility( ) , Some ( HistoryVisibility :: Joined ) ) ;
484+ let event_id_2 = bob_send_test_event ( "Event 2" ) . await ;
485+
486+ // 6. Bob syncs, and sends a third message; the key is now "shareable".
487+ bob. sync_once ( ) . instrument ( bob_span. clone ( ) ) . await ?;
488+ assert_eq ! ( bob_room. history_visibility( ) , Some ( HistoryVisibility :: Shared ) ) ;
489+ let event_id_3 = bob_send_test_event ( "Event 3" ) . await ;
490+
491+ // 7. Alice invites Charlie.
492+ alice_room. invite_user_by_id ( charlie. user_id ( ) . unwrap ( ) ) . instrument ( alice_span. clone ( ) ) . await ?;
493+
494+ // Workaround for https://github.com/matrix-org/matrix-rust-sdk/issues/5770: Charlie needs a copy of
495+ // Alice's identity.
496+ charlie
497+ . encryption ( )
498+ . request_user_identity ( alice. user_id ( ) . unwrap ( ) )
499+ . instrument ( charlie_span. clone ( ) )
500+ . await ?;
501+
502+ // 8. Charlie joins the room
503+ charlie. sync_once ( ) . instrument ( charlie_span. clone ( ) ) . await ?;
504+ let charlie_room = charlie
505+ . join_room_by_id ( alice_room. room_id ( ) )
506+ . instrument ( charlie_span. clone ( ) )
507+ . await
508+ . expect ( "Charlie should be able to accept the invitation from Alice" ) ;
509+
510+ // Events 1 and 3 should be decryptable; 2 should be "history not shared".
511+ assert_event_received ( & charlie_room, & event_id_1, "Event 1" ) . await ;
512+ assert_event_received ( & charlie_room, & event_id_3, "Event 3" ) . await ;
513+ let event = charlie_room. event ( & event_id_2, None ) . await . expect ( "Should receive Bob's event 2" ) ;
514+ assert_let ! ( TimelineEventKind :: UnableToDecrypt { utd_info, .. } = event. kind) ;
515+ assert_eq ! (
516+ utd_info. reason,
517+ MissingMegolmSession { withheld_code: Some ( WithheldCode :: HistoryNotShared ) }
518+ ) ;
519+
520+ Ok ( ( ) )
521+ }
522+
523+ async fn create_encryption_enabled_client ( username : & str ) -> Result < SyncTokenAwareClient > {
524+ let encryption_settings =
525+ EncryptionSettings { auto_enable_cross_signing : true , ..Default :: default ( ) } ;
526+
527+ let client = SyncTokenAwareClient :: new (
528+ TestClientBuilder :: new ( username)
529+ . use_sqlite ( )
530+ . encryption_settings ( encryption_settings)
531+ . enable_share_history_on_invite ( true )
532+ . build ( )
533+ . await ?,
534+ ) ;
535+
536+ client. encryption ( ) . wait_for_e2ee_initialization_tasks ( ) . await ;
537+ Ok ( client)
538+ }
0 commit comments