@@ -68,6 +68,7 @@ afterEach(() => {
6868const ROOM_ID = "!room:example.com" ;
6969const ALICE_HOMESERVER_URL = "https://alice-server.com" ;
7070const BOB_HOMESERVER_URL = "https://bob-server.com" ;
71+ const CHARLIE_HOMESERVER_URL = "https://charlie-server.com" ;
7172
7273async function createAndInitClient ( homeserverUrl : string , userId : string , setupNewCrossSigning = true ) {
7374 mockInitialApiRequests ( homeserverUrl , userId ) ;
@@ -91,6 +92,8 @@ describe("History Sharing", () => {
9192 let aliceSyncResponder : SyncResponder ;
9293 let bobClient : MatrixClient ;
9394 let bobSyncResponder : SyncResponder ;
95+ let charlieClient : MatrixClient ;
96+ let charlieSyncResponder : SyncResponder ;
9497
9598 beforeEach ( async ( ) => {
9699 // anything that we don't have a specific matcher for silently returns a 404
@@ -100,6 +103,7 @@ describe("History Sharing", () => {
100103
101104 const aliceId = TEST_USER_ID ;
102105 const bobId = "@bob:xyz" ;
106+ const charlieId = "@charlie:zyx" ;
103107
104108 const aliceKeyReceiver = new E2EKeyReceiver ( ALICE_HOMESERVER_URL , "alice-" ) ;
105109 const aliceKeyResponder = new E2EKeyResponder ( ALICE_HOMESERVER_URL ) ;
@@ -108,23 +112,39 @@ describe("History Sharing", () => {
108112
109113 const bobKeyReceiver = new E2EKeyReceiver ( BOB_HOMESERVER_URL , "bob-" ) ;
110114 const bobKeyResponder = new E2EKeyResponder ( BOB_HOMESERVER_URL ) ;
115+ const bobKeyClaimResponder = new E2EOTKClaimResponder ( BOB_HOMESERVER_URL ) ;
111116 bobSyncResponder = new SyncResponder ( BOB_HOMESERVER_URL ) ;
112117
118+ const charlieKeyReceiver = new E2EKeyReceiver ( CHARLIE_HOMESERVER_URL , "charlie-" ) ;
119+ const charlieKeyResponder = new E2EKeyResponder ( CHARLIE_HOMESERVER_URL ) ;
120+ charlieSyncResponder = new SyncResponder ( CHARLIE_HOMESERVER_URL ) ;
121+
113122 aliceKeyResponder . addKeyReceiver ( aliceId , aliceKeyReceiver ) ;
114123 aliceKeyResponder . addKeyReceiver ( bobId , bobKeyReceiver ) ;
124+ aliceKeyResponder . addKeyReceiver ( charlieId , charlieKeyReceiver ) ;
115125 bobKeyResponder . addKeyReceiver ( aliceId , aliceKeyReceiver ) ;
116126 bobKeyResponder . addKeyReceiver ( bobId , bobKeyReceiver ) ;
127+ bobKeyResponder . addKeyReceiver ( charlieId , charlieKeyReceiver ) ;
128+ charlieKeyResponder . addKeyReceiver ( aliceId , aliceKeyReceiver ) ;
129+ charlieKeyResponder . addKeyReceiver ( bobId , bobKeyReceiver ) ;
130+ charlieKeyResponder . addKeyReceiver ( charlieId , charlieKeyReceiver ) ;
117131
118132 aliceClient = await createAndInitClient ( ALICE_HOMESERVER_URL , aliceId ) ;
119133 bobClient = await createAndInitClient ( BOB_HOMESERVER_URL , bobId ) ;
134+ charlieClient = await createAndInitClient ( CHARLIE_HOMESERVER_URL , charlieId ) ;
120135
121136 aliceKeyClaimResponder . addKeyReceiver ( bobId , bobClient . deviceId ! , bobKeyReceiver ) ;
137+ aliceKeyClaimResponder . addKeyReceiver ( charlieId , charlieClient . deviceId ! , charlieKeyReceiver ) ;
138+ bobKeyClaimResponder . addKeyReceiver ( aliceId , aliceClient . deviceId ! , aliceKeyReceiver ) ;
122139
123140 aliceSyncResponder . sendOrQueueSyncResponse ( { } ) ;
124141 await syncPromise ( aliceClient ) ;
125142
126143 bobSyncResponder . sendOrQueueSyncResponse ( { } ) ;
127144 await syncPromise ( bobClient ) ;
145+
146+ charlieSyncResponder . sendOrQueueSyncResponse ( { } ) ;
147+ await syncPromise ( charlieClient ) ;
128148 } ) ;
129149
130150 test ( "Room keys are successfully shared on invite" , async ( ) => {
@@ -676,6 +696,305 @@ describe("History Sharing", () => {
676696 ) . toBeFalsy ( ) ;
677697 } ) ;
678698
699+ test . each ( [ false , true ] ) (
700+ "Room key is rotated after a member joins and leaves the room (gappy sync = %s)" ,
701+ async ( gappySync ) => {
702+ // Alice and Bob are in an encrypted room
703+ let syncResponse = getSyncResponse (
704+ [ aliceClient . getSafeUserId ( ) , bobClient . getSafeUserId ( ) ] ,
705+ HistoryVisibility . Shared ,
706+ ROOM_ID ,
707+ ) ;
708+ aliceSyncResponder . sendOrQueueSyncResponse ( syncResponse ) ;
709+ bobSyncResponder . sendOrQueueSyncResponse ( syncResponse ) ;
710+
711+ await syncPromise ( aliceClient ) ;
712+ await syncPromise ( bobClient ) ;
713+
714+ // Bob sends a message M1, which both he and Alice receive.
715+ let msgProm = expectSendRoomEvent ( BOB_HOMESERVER_URL , "m.room.encrypted" ) ;
716+ let toDeviceMessageProm = expectSendToDeviceMessage ( BOB_HOMESERVER_URL , "m.room.encrypted" ) ;
717+ await bobClient . sendEvent ( ROOM_ID , EventType . RoomMessage , {
718+ msgtype : MsgType . Text ,
719+ body : "Charlie should be able to read" ,
720+ } ) ;
721+ const bobEventM1Content = await msgProm ;
722+ let sentToDeviceRequest = await toDeviceMessageProm ;
723+ expect ( sentToDeviceRequest ) . toBeDefined ( ) ;
724+ let aliceToDeviceMessage = sentToDeviceRequest [ aliceClient . getSafeUserId ( ) ] [ aliceClient . deviceId ! ] ;
725+
726+ // Alice receives the message down sync.
727+ syncResponse = getSyncResponse (
728+ [ aliceClient . getSafeUserId ( ) , bobClient . getSafeUserId ( ) ] ,
729+ HistoryVisibility . Shared ,
730+ ROOM_ID ,
731+ ) ;
732+ syncResponse . rooms . join [ ROOM_ID ] . timeline . events . push (
733+ mkEventCustom ( {
734+ type : "m.room.encrypted" ,
735+ sender : bobClient . getSafeUserId ( ) ,
736+ content : bobEventM1Content ,
737+ event_id : "$event_id_m1" ,
738+ } ) as any ,
739+ ) ;
740+ syncResponse . to_device = {
741+ events : [
742+ {
743+ type : "m.room.encrypted" ,
744+ sender : bobClient . getSafeUserId ( ) ,
745+ content : aliceToDeviceMessage ,
746+ } ,
747+ ] ,
748+ } ;
749+ aliceSyncResponder . sendOrQueueSyncResponse ( syncResponse ) ;
750+ await syncPromise ( aliceClient ) ;
751+
752+ // Alice checks she can read M1.
753+ const aliceRoom = aliceClient . getRoom ( ROOM_ID ) ;
754+ const aliceM1 = aliceRoom ! . getLastLiveEvent ( ) ! ;
755+ await aliceM1 . getDecryptionPromise ( ) ;
756+ expect ( aliceM1 . getType ( ) ) . toEqual ( "m.room.message" ) ;
757+ expect ( aliceM1 . getContent ( ) . body ) . toEqual ( "Charlie should be able to read" ) ;
758+
759+ // Alice invites and sends a key bundle to Charlie
760+ const uploadProm = expectUploadRequest ( ) ;
761+ toDeviceMessageProm = expectSendToDeviceMessage ( ALICE_HOMESERVER_URL , "m.room.encrypted" ) ;
762+ fetchMock . postOnce (
763+ `${ ALICE_HOMESERVER_URL } /_matrix/client/v3/rooms/${ encodeURIComponent ( ROOM_ID ) } /invite` ,
764+ { } ,
765+ ) ;
766+ await aliceClient . invite ( ROOM_ID , charlieClient . getSafeUserId ( ) , { shareEncryptedHistory : true } ) ;
767+ const uploadedBlob = await uploadProm ;
768+ sentToDeviceRequest = await toDeviceMessageProm ;
769+ debug ( `Alice sent encrypted to-device events: ${ JSON . stringify ( sentToDeviceRequest ) } ` ) ;
770+ const charlieToDeviceMessage = sentToDeviceRequest [ charlieClient . getSafeUserId ( ) ] [ charlieClient . deviceId ! ] ;
771+ expect ( charlieToDeviceMessage ) . toBeDefined ( ) ;
772+
773+ /// Charlie receives the invite ...
774+ const inviteEvent = mkInviteEvent ( aliceClient , charlieClient ) ;
775+ charlieSyncResponder . sendOrQueueSyncResponse ( {
776+ rooms : { invite : { [ ROOM_ID ] : { invite_state : { events : [ inviteEvent ] } } } } ,
777+ to_device : {
778+ events : [
779+ {
780+ type : "m.room.encrypted" ,
781+ sender : aliceClient . getSafeUserId ( ) ,
782+ content : charlieToDeviceMessage ,
783+ } ,
784+ ] ,
785+ } ,
786+ } ) ;
787+ await syncPromise ( charlieClient ) ;
788+
789+ const charlieRoom = charlieClient . getRoom ( ROOM_ID ) ;
790+ expect ( charlieRoom ) . toBeTruthy ( ) ;
791+ expect ( charlieRoom ?. getMyMembership ( ) ) . toEqual ( KnownMembership . Invite ) ;
792+
793+ // ... and subsequently joins.
794+ fetchMock . postOnce ( `${ CHARLIE_HOMESERVER_URL } /_matrix/client/v3/join/${ encodeURIComponent ( ROOM_ID ) } ` , {
795+ room_id : ROOM_ID ,
796+ } ) ;
797+ fetchMock . getOnce ( `begin:${ CHARLIE_HOMESERVER_URL } /_matrix/client/v1/media/download/alice-server/here` , {
798+ body : uploadedBlob ,
799+ } ) ;
800+ await charlieClient . joinRoom ( ROOM_ID , { acceptSharedHistory : true } ) ;
801+
802+ // Charlie syncs to receive M1 and ensure he can read it.
803+ syncResponse = getSyncResponse (
804+ [ aliceClient . getSafeUserId ( ) , bobClient . getSafeUserId ( ) , charlieClient . getSafeUserId ( ) ] ,
805+ HistoryVisibility . Shared ,
806+ ROOM_ID ,
807+ ) ;
808+ syncResponse . rooms . join [ ROOM_ID ] . timeline . events . push (
809+ mkEventCustom ( {
810+ type : "m.room.encrypted" ,
811+ sender : bobClient . getSafeUserId ( ) ,
812+ content : bobEventM1Content ,
813+ event_id : "$event_id_m1" ,
814+ } ) as any ,
815+ ) ;
816+ charlieSyncResponder . sendOrQueueSyncResponse ( syncResponse ) ;
817+ await syncPromise ( charlieClient ) ;
818+
819+ const charlieEventM1 = charlieRoom !
820+ . getLiveTimeline ( )
821+ . getEvents ( )
822+ . find ( ( e ) => e . getId ( ) === "$event_id_m1" ) ;
823+
824+ await charlieEventM1 ! . getDecryptionPromise ( ) ;
825+ expect ( charlieEventM1 ! . getType ( ) ) . toEqual ( "m.room.message" ) ;
826+ expect ( charlieEventM1 ! . getContent ( ) . body ) . toEqual ( "Charlie should be able to read" ) ;
827+
828+ // Charlie then immediately leaves.
829+ const charlieSyncResponse = {
830+ next_batch : "1" ,
831+ rooms : {
832+ leave : {
833+ [ ROOM_ID ] : {
834+ state : { events : [ ] } ,
835+ timeline : {
836+ events : [
837+ mkEventCustom ( {
838+ content : { membership : KnownMembership . Leave } ,
839+ type : EventType . RoomMember ,
840+ sender : charlieClient . getSafeUserId ( ) ,
841+ state_key : charlieClient . getSafeUserId ( ) ,
842+ } ) ,
843+ ] ,
844+ prev_batch : "" ,
845+ } ,
846+ account_data : { events : [ ] } ,
847+ } ,
848+ } ,
849+ invite : { } ,
850+ join : { } ,
851+ knock : { } ,
852+ } ,
853+ account_data : { events : [ ] } ,
854+ } ;
855+ charlieSyncResponder . sendOrQueueSyncResponse ( charlieSyncResponse ) ;
856+ await syncPromise ( charlieClient ) ;
857+
858+ syncResponse = {
859+ next_batch : "2" ,
860+ rooms : {
861+ join : {
862+ [ ROOM_ID ] : {
863+ timeline : {
864+ events : [ ] ,
865+ } ,
866+ } ,
867+ } ,
868+ } ,
869+ } as any ;
870+ if ( gappySync ) {
871+ // In case of a gappy sync, the timeline is limited and we only see the leave event.
872+ syncResponse . rooms . join [ ROOM_ID ] . timeline . limited = true ;
873+ syncResponse . rooms . join [ ROOM_ID ] . state = {
874+ events : [
875+ mkEventCustom ( {
876+ content : { membership : KnownMembership . Leave } ,
877+ type : EventType . RoomMember ,
878+ sender : charlieClient . getSafeUserId ( ) ,
879+ state_key : charlieClient . getSafeUserId ( ) ,
880+ } ) as any ,
881+ ] ,
882+ } ;
883+ } else {
884+ syncResponse . rooms . join [ ROOM_ID ] . timeline . events . push (
885+ mkEventCustom ( {
886+ content : { membership : KnownMembership . Join } ,
887+ type : EventType . RoomMember ,
888+ sender : charlieClient . getSafeUserId ( ) ,
889+ state_key : charlieClient . getSafeUserId ( ) ,
890+ } ) as any ,
891+ mkEventCustom ( {
892+ content : { membership : KnownMembership . Leave } ,
893+ type : EventType . RoomMember ,
894+ sender : charlieClient . getSafeUserId ( ) ,
895+ state_key : charlieClient . getSafeUserId ( ) ,
896+ } ) as any ,
897+ ) ;
898+ }
899+ // Bob syncs to learn about Charlie's leaving (and joining if non-gappy).
900+ bobSyncResponder . sendOrQueueSyncResponse ( syncResponse ) ;
901+ await syncPromise ( bobClient ) ;
902+
903+ // Bob then sends M2, sharing a new room key with Alice.
904+ msgProm = expectSendRoomEvent ( BOB_HOMESERVER_URL , "m.room.encrypted" ) ;
905+ toDeviceMessageProm = expectSendToDeviceMessage ( BOB_HOMESERVER_URL , "m.room.encrypted" ) ;
906+ await bobClient . sendEvent ( ROOM_ID , EventType . RoomMessage , {
907+ msgtype : MsgType . Text ,
908+ body : "Charlie should not be able to read" ,
909+ } ) ;
910+ const bobEventM2Content = await msgProm ;
911+ sentToDeviceRequest = await toDeviceMessageProm ;
912+ expect ( sentToDeviceRequest ) . toBeDefined ( ) ;
913+ aliceToDeviceMessage = sentToDeviceRequest [ aliceClient . getSafeUserId ( ) ] [ aliceClient . deviceId ! ] ;
914+
915+ // Charlie should not receive the room key
916+ expect ( sentToDeviceRequest [ charlieClient . getSafeUserId ( ) ] ) . toBeUndefined ( ) ;
917+
918+ debug ( `Bob sent encrypted room event: ${ JSON . stringify ( bobEventM2Content ) } ` ) ;
919+
920+ // Sync the message to Alice along with the to-device message, and check she can decrypt it.
921+ syncResponse = {
922+ next_batch : "3" ,
923+ rooms : {
924+ join : {
925+ [ ROOM_ID ] : {
926+ timeline : {
927+ events : [
928+ mkEventCustom ( {
929+ type : "m.room.encrypted" ,
930+ sender : bobClient . getSafeUserId ( ) ,
931+ content : bobEventM2Content ,
932+ event_id : "$event_id_m2" ,
933+ } ) as any ,
934+ ] ,
935+ } ,
936+ } ,
937+ } ,
938+ } ,
939+ to_device : {
940+ events : [
941+ {
942+ type : "m.room.encrypted" ,
943+ sender : bobClient . getSafeUserId ( ) ,
944+ content : aliceToDeviceMessage ,
945+ } ,
946+ ] ,
947+ } ,
948+ } as any ;
949+ aliceSyncResponder . sendOrQueueSyncResponse ( syncResponse ) ;
950+ await syncPromise ( aliceClient ) ;
951+
952+ const aliceEventM2 = aliceRoom ! . getLastLiveEvent ( ) ! ;
953+ await aliceEventM2 . getDecryptionPromise ( ) ;
954+ expect ( aliceEventM2 . getType ( ) ) . toEqual ( "m.room.message" ) ;
955+ expect ( aliceEventM2 . getContent ( ) . body ) . toEqual ( "Charlie should not be able to read" ) ;
956+
957+ // Charlie rejoins the room by ID, receives M2, which he should not be able to decrypt.
958+ fetchMock . postOnce ( `${ CHARLIE_HOMESERVER_URL } /_matrix/client/v3/join/${ encodeURIComponent ( ROOM_ID ) } ` , {
959+ room_id : ROOM_ID ,
960+ } ) ;
961+ await charlieClient . joinRoom ( ROOM_ID , { acceptSharedHistory : true } ) ;
962+ syncResponse = {
963+ next_batch : "4" ,
964+ rooms : {
965+ join : {
966+ [ ROOM_ID ] : {
967+ timeline : {
968+ events : [
969+ mkEventCustom ( {
970+ type : "m.room.encrypted" ,
971+ sender : bobClient . getSafeUserId ( ) ,
972+ content : bobEventM2Content ,
973+ event_id : "$event_id_m2" ,
974+ } ) as any ,
975+ ] ,
976+ } ,
977+ } ,
978+ } ,
979+ } ,
980+ } as any ;
981+ charlieSyncResponder . sendOrQueueSyncResponse ( syncResponse ) ;
982+ await syncPromise ( charlieClient ) ;
983+
984+ const events = charlieRoom ! . getLiveTimeline ( ) . getEvents ( ) ;
985+ expect ( events . length ) . toBeGreaterThanOrEqual ( 2 ) ;
986+
987+ const charlieM2 = charlieRoom !
988+ . getLiveTimeline ( )
989+ . getEvents ( )
990+ . find ( ( e ) => e . getId ( ) === "$event_id_m2" ) ;
991+
992+ await charlieM2 ! . getDecryptionPromise ( ) ;
993+ expect ( charlieM2 ! . isDecryptionFailure ( ) ) . toBeTruthy ( ) ;
994+ } ,
995+ 60e3 ,
996+ ) ;
997+
679998 afterEach ( async ( ) => {
680999 vitest . useRealTimers ( ) ;
6811000 bobClient . stopClient ( ) ;
0 commit comments