1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-07 23:02:56 +03:00

Retry event decryption failures on first failure (#4346)

* Retry event decryption failures on first failure

* Suggestion from code review

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
This commit is contained in:
Hugh Nimmo-Smith
2024-08-19 14:35:45 +01:00
committed by GitHub
parent d6080398db
commit c408c0d1d5
3 changed files with 107 additions and 1 deletions

View File

@@ -102,6 +102,7 @@ describe("MatrixRTCSessionManager", () => {
getContent: jest.fn().mockReturnValue({}), getContent: jest.fn().mockReturnValue({}),
getSender: jest.fn().mockReturnValue("@mock:user.example"), getSender: jest.fn().mockReturnValue("@mock:user.example"),
getRoomId: jest.fn().mockReturnValue("!room:id"), getRoomId: jest.fn().mockReturnValue("!room:id"),
isDecryptionFailure: jest.fn().mockReturnValue(false),
sender: { sender: {
userId: "@mock:user.example", userId: "@mock:user.example",
}, },
@@ -110,4 +111,93 @@ describe("MatrixRTCSessionManager", () => {
await new Promise(process.nextTick); await new Promise(process.nextTick);
expect(onCallEncryptionMock).toHaveBeenCalled(); 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();
}
});
});
}); });

View File

@@ -73,5 +73,6 @@ export function mockRTCEvent(membershipData: MembershipData, roomId: string): Ma
sender: { sender: {
userId: "@mock:user.example", userId: "@mock:user.example",
}, },
isDecryptionFailure: jest.fn().mockReturnValue(false),
} as unknown as MatrixEvent; } as unknown as MatrixEvent;
} }

View File

@@ -98,8 +98,23 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
return this.roomSessions.get(room.roomId)!; return this.roomSessions.get(room.roomId)!;
} }
private async consumeCallEncryptionEvent(event: MatrixEvent): Promise<void> { private async consumeCallEncryptionEvent(event: MatrixEvent, isRetry = false): Promise<void> {
await this.client.decryptEventIfNeeded(event); 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(); if (event.getType() !== EventType.CallEncryptionKeysPrefix) return Promise.resolve();
const room = this.client.getRoom(event.getRoomId()); const room = this.client.getRoom(event.getRoomId());