diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index c2e6fc52f..bbc9c9f7e 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -102,6 +102,7 @@ describe("MatrixRTCSessionManager", () => { getContent: jest.fn().mockReturnValue({}), getSender: jest.fn().mockReturnValue("@mock:user.example"), getRoomId: jest.fn().mockReturnValue("!room:id"), + isDecryptionFailure: jest.fn().mockReturnValue(false), sender: { userId: "@mock:user.example", }, @@ -110,4 +111,93 @@ describe("MatrixRTCSessionManager", () => { await new Promise(process.nextTick); expect(onCallEncryptionMock).toHaveBeenCalled(); }); + + describe("event decryption", () => { + it("Retries decryption and processes success", async () => { + try { + jest.useFakeTimers(); + const room1 = makeMockRoom([membershipTemplate]); + jest.spyOn(client, "getRooms").mockReturnValue([room1]); + jest.spyOn(client, "getRoom").mockReturnValue(room1); + + client.emit(ClientEvent.Room, room1); + const onCallEncryptionMock = jest.fn(); + client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock; + let isDecryptionFailure = true; + client.decryptEventIfNeeded = jest + .fn() + .mockReturnValueOnce(Promise.resolve()) + .mockImplementation(() => { + isDecryptionFailure = false; + return Promise.resolve(); + }); + const timelineEvent = { + getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix), + getContent: jest.fn().mockReturnValue({}), + getSender: jest.fn().mockReturnValue("@mock:user.example"), + getRoomId: jest.fn().mockReturnValue("!room:id"), + isDecryptionFailure: jest.fn().mockImplementation(() => isDecryptionFailure), + getId: jest.fn().mockReturnValue("event_id"), + sender: { + userId: "@mock:user.example", + }, + } as unknown as MatrixEvent; + client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData); + + expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(1); + expect(onCallEncryptionMock).toHaveBeenCalledTimes(0); + + // should retry after one second: + await jest.advanceTimersByTimeAsync(1500); + + expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2); + expect(onCallEncryptionMock).toHaveBeenCalledTimes(1); + } finally { + jest.useRealTimers(); + } + }); + + it("Retries decryption and processes failure", async () => { + try { + jest.useFakeTimers(); + const room1 = makeMockRoom([membershipTemplate]); + jest.spyOn(client, "getRooms").mockReturnValue([room1]); + jest.spyOn(client, "getRoom").mockReturnValue(room1); + + client.emit(ClientEvent.Room, room1); + const onCallEncryptionMock = jest.fn(); + client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock; + client.decryptEventIfNeeded = jest.fn().mockReturnValue(Promise.resolve()); + const timelineEvent = { + getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix), + getContent: jest.fn().mockReturnValue({}), + getSender: jest.fn().mockReturnValue("@mock:user.example"), + getRoomId: jest.fn().mockReturnValue("!room:id"), + isDecryptionFailure: jest.fn().mockReturnValue(true), // always fail + getId: jest.fn().mockReturnValue("event_id"), + sender: { + userId: "@mock:user.example", + }, + } as unknown as MatrixEvent; + client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData); + + expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(1); + expect(onCallEncryptionMock).toHaveBeenCalledTimes(0); + + // should retry after one second: + await jest.advanceTimersByTimeAsync(1500); + + expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2); + expect(onCallEncryptionMock).toHaveBeenCalledTimes(0); + + // doesn't retry again: + await jest.advanceTimersByTimeAsync(1500); + + expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2); + expect(onCallEncryptionMock).toHaveBeenCalledTimes(0); + } finally { + jest.useRealTimers(); + } + }); + }); }); diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index ac5831b0f..57863dc2c 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -73,5 +73,6 @@ export function mockRTCEvent(membershipData: MembershipData, roomId: string): Ma sender: { userId: "@mock:user.example", }, + isDecryptionFailure: jest.fn().mockReturnValue(false), } as unknown as MatrixEvent; } diff --git a/src/matrixrtc/MatrixRTCSessionManager.ts b/src/matrixrtc/MatrixRTCSessionManager.ts index 2fd5d2583..22480cffc 100644 --- a/src/matrixrtc/MatrixRTCSessionManager.ts +++ b/src/matrixrtc/MatrixRTCSessionManager.ts @@ -98,8 +98,23 @@ export class MatrixRTCSessionManager extends TypedEventEmitter { + private async consumeCallEncryptionEvent(event: MatrixEvent, isRetry = false): Promise { await this.client.decryptEventIfNeeded(event); + if (event.isDecryptionFailure()) { + if (!isRetry) { + logger.warn( + `Decryption failed for event ${event.getId()}: ${event.decryptionFailureReason} will retry once only`, + ); + // retry after 1 second. After this we give up. + setTimeout(() => this.consumeCallEncryptionEvent(event, true), 1000); + } else { + logger.warn(`Decryption failed for event ${event.getId()}: ${event.decryptionFailureReason}`); + } + return; + } else if (isRetry) { + logger.info(`Decryption succeeded for event ${event.getId()} after retry`); + } + if (event.getType() !== EventType.CallEncryptionKeysPrefix) return Promise.resolve(); const room = this.client.getRoom(event.getRoomId());