@@ -88,6 +88,7 @@ describe("RTCEncryptionManager", () => {
88
88
} ) ;
89
89
90
90
it ( "Should distribute keys to members on join" , async ( ) => {
91
+ jest . useFakeTimers ( ) ;
91
92
const members = [
92
93
aCallMembership ( "@bob:example.org" , "BOBDEVICE" ) ,
93
94
aCallMembership ( "@bob:example.org" , "BOBDEVICE2" ) ,
@@ -165,7 +166,7 @@ describe("RTCEncryptionManager", () => {
165
166
) ;
166
167
} ) ;
167
168
168
- it ( "Should not rotate key when a user join" , async ( ) => {
169
+ it ( "Should not rotate key when a user join within the rotation grace period " , async ( ) => {
169
170
jest . useFakeTimers ( ) ;
170
171
171
172
const members = [
@@ -174,8 +175,9 @@ describe("RTCEncryptionManager", () => {
174
175
] ;
175
176
getMembershipMock . mockReturnValue ( members ) ;
176
177
178
+ const gracePeriod = 15_000 ; // 15 seconds
177
179
// initial rollout
178
- encryptionManager . join ( undefined ) ;
180
+ encryptionManager . join ( { keyRotationGracePeriodMs : gracePeriod } ) ;
179
181
encryptionManager . onMembershipsUpdate ( [ ] ) ;
180
182
await jest . runOnlyPendingTimersAsync ( ) ;
181
183
@@ -196,6 +198,8 @@ describe("RTCEncryptionManager", () => {
196
198
] ;
197
199
getMembershipMock . mockReturnValue ( updatedMembers ) ;
198
200
201
+ await jest . advanceTimersByTimeAsync ( 8_000 ) ;
202
+
199
203
encryptionManager . onMembershipsUpdate ( updatedMembers ) ;
200
204
201
205
await jest . runOnlyPendingTimersAsync ( ) ;
@@ -214,6 +218,104 @@ describe("RTCEncryptionManager", () => {
214
218
expect ( statistics . counters . roomEventEncryptionKeysSent ) . toBe ( 2 ) ;
215
219
} ) ;
216
220
221
+ it ( "Should rotate key when a user join past the rotation grace period" , async ( ) => {
222
+ jest . useFakeTimers ( ) ;
223
+
224
+ const members = [
225
+ aCallMembership ( "@bob:example.org" , "BOBDEVICE" ) ,
226
+ aCallMembership ( "@bob:example.org" , "BOBDEVICE2" ) ,
227
+ ] ;
228
+ getMembershipMock . mockReturnValue ( members ) ;
229
+
230
+ const gracePeriod = 15_000 ; // 15 seconds
231
+ // initial rollout
232
+ encryptionManager . join ( { keyRotationGracePeriodMs : gracePeriod } ) ;
233
+ encryptionManager . onMembershipsUpdate ( [ ] ) ;
234
+ await jest . runOnlyPendingTimersAsync ( ) ;
235
+
236
+ onEncryptionKeysChanged . mockClear ( ) ;
237
+ mockTransport . sendKey . mockClear ( ) ;
238
+
239
+ const updatedMembers = [
240
+ aCallMembership ( "@bob:example.org" , "BOBDEVICE" ) ,
241
+ aCallMembership ( "@bob:example.org" , "BOBDEVICE2" ) ,
242
+ aCallMembership ( "@carl:example.org" , "CARLDEVICE" ) ,
243
+ ] ;
244
+ getMembershipMock . mockReturnValue ( updatedMembers ) ;
245
+
246
+ await jest . advanceTimersByTimeAsync ( gracePeriod + 1000 ) ;
247
+
248
+ encryptionManager . onMembershipsUpdate ( updatedMembers ) ;
249
+
250
+ await jest . runOnlyPendingTimersAsync ( ) ;
251
+
252
+ expect ( mockTransport . sendKey ) . toHaveBeenCalledWith (
253
+ expect . any ( String ) ,
254
+ // It should have incremented the key index
255
+ 1 ,
256
+ // And send it to everyone
257
+ [
258
+ expect . objectContaining ( { userId : "@bob:example.org" , deviceId : "BOBDEVICE" } ) ,
259
+ expect . objectContaining ( { userId : "@bob:example.org" , deviceId : "BOBDEVICE2" } ) ,
260
+ expect . objectContaining ( { userId : "@carl:example.org" , deviceId : "CARLDEVICE" } ) ,
261
+ ] ,
262
+ ) ;
263
+
264
+ expect ( onEncryptionKeysChanged ) . toHaveBeenCalled ( ) ;
265
+ await jest . advanceTimersByTimeAsync ( 1000 ) ;
266
+ expect ( statistics . counters . roomEventEncryptionKeysSent ) . toBe ( 2 ) ;
267
+ } ) ;
268
+
269
+ it ( "Should rotate key when several users join within the rotation grace period" , async ( ) => {
270
+ jest . useFakeTimers ( ) ;
271
+
272
+ const members = [
273
+ aCallMembership ( "@bob:example.org" , "BOBDEVICE" ) ,
274
+ aCallMembership ( "@bob:example.org" , "BOBDEVICE2" ) ,
275
+ ] ;
276
+ getMembershipMock . mockReturnValue ( members ) ;
277
+
278
+ // initial rollout
279
+ encryptionManager . join ( undefined ) ;
280
+ encryptionManager . onMembershipsUpdate ( [ ] ) ;
281
+ await jest . runOnlyPendingTimersAsync ( ) ;
282
+
283
+ onEncryptionKeysChanged . mockClear ( ) ;
284
+ mockTransport . sendKey . mockClear ( ) ;
285
+
286
+ const newJoiners = [
287
+ aCallMembership ( "@carl:example.org" , "CARLDEVICE" ) ,
288
+ aCallMembership ( "@dave:example.org" , "DAVEDEVICE" ) ,
289
+ aCallMembership ( "@eve:example.org" , "EVEDEVICE" ) ,
290
+ aCallMembership ( "@frank:example.org" , "FRANKDEVICE" ) ,
291
+ aCallMembership ( "@george:example.org" , "GEORGEDEVICE" ) ,
292
+ ] ;
293
+
294
+ for ( const newJoiner of newJoiners ) {
295
+ members . push ( newJoiner ) ;
296
+ getMembershipMock . mockReturnValue ( members ) ;
297
+ await jest . advanceTimersByTimeAsync ( 1_000 ) ;
298
+ encryptionManager . onMembershipsUpdate ( members ) ;
299
+ await jest . runOnlyPendingTimersAsync ( ) ;
300
+ }
301
+
302
+ expect ( mockTransport . sendKey ) . toHaveBeenCalledTimes ( newJoiners . length ) ;
303
+
304
+ for ( const newJoiner of newJoiners ) {
305
+ expect ( mockTransport . sendKey ) . toHaveBeenCalledWith (
306
+ expect . any ( String ) ,
307
+ // It should not have incremented the key index
308
+ 0 ,
309
+ // And send it to the new joiners only
310
+ expect . arrayContaining ( [
311
+ expect . objectContaining ( { userId : newJoiner . sender , deviceId : newJoiner . deviceId } ) ,
312
+ ] ) ,
313
+ ) ;
314
+ }
315
+
316
+ expect ( onEncryptionKeysChanged ) . not . toHaveBeenCalled ( ) ;
317
+ } ) ;
318
+
217
319
it ( "Should not resend keys when no changes" , async ( ) => {
218
320
jest . useFakeTimers ( ) ;
219
321
0 commit comments