1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

Handle late-arriving m.room_key.withheld messages (#4310)

* Restructure eventsPendingKey to remove sender key

For withheld notices, we don't necessarily receive the sender key, so we'll
jhave to do without it.

* Re-decrypt events when we receive a withheld notice

* Extend test to cover late-arriving withheld notices

* update unit tests
This commit is contained in:
Richard van der Hoff
2024-07-29 13:11:37 +01:00
committed by GitHub
parent d32f398345
commit dc1cccfecc
4 changed files with 60 additions and 27 deletions

View File

@@ -2343,13 +2343,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
])( ])(
"Decryption fails with withheld error if a withheld notice with code '%s' is received", "Decryption fails with withheld error if a withheld notice with code '%s' is received",
(withheldCode, expectedMessage, expectedErrorCode) => { (withheldCode, expectedMessage, expectedErrorCode) => {
// TODO: test arrival after the event too. it.each(["before", "after"])("%s the event", async (when) => {
it.each(["before"])("%s the event", async (when) => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync(); await startClientAndAwaitFirstSync();
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails. // A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted); let awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
// Send Alice an encrypted room event which looks like it was encrypted with a megolm session // Send Alice an encrypted room event which looks like it was encrypted with a megolm session
async function sendEncryptedEvent() { async function sendEncryptedEvent() {
@@ -2393,6 +2392,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
await sendEncryptedEvent(); await sendEncryptedEvent();
} else { } else {
await sendEncryptedEvent(); await sendEncryptedEvent();
// Make sure that the first attempt to decrypt has happened before the withheld arrives
await awaitDecryption;
awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
await sendWithheldMessage(); await sendWithheldMessage();
} }

View File

@@ -95,6 +95,7 @@ describe("initRustCrypto", () => {
deleteSecretsFromInbox: jest.fn(), deleteSecretsFromInbox: jest.fn(),
registerReceiveSecretCallback: jest.fn(), registerReceiveSecretCallback: jest.fn(),
registerDevicesUpdatedCallback: jest.fn(), registerDevicesUpdatedCallback: jest.fn(),
registerRoomKeysWithheldCallback: jest.fn(),
outgoingRequests: jest.fn(), outgoingRequests: jest.fn(),
isBackupEnabled: jest.fn().mockResolvedValue(false), isBackupEnabled: jest.fn().mockResolvedValue(false),
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }), verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),

View File

@@ -174,6 +174,9 @@ async function initOlmMachine(
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) => await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
rustCrypto.onRoomKeysUpdated(sessions), rustCrypto.onRoomKeysUpdated(sessions),
); );
await olmMachine.registerRoomKeysWithheldCallback((withheld: RustSdkCryptoJs.RoomKeyWithheldInfo[]) =>
rustCrypto.onRoomKeysWithheld(withheld),
);
await olmMachine.registerUserIdentityUpdatedCallback((userId: RustSdkCryptoJs.UserId) => await olmMachine.registerUserIdentityUpdatedCallback((userId: RustSdkCryptoJs.UserId) =>
rustCrypto.onUserIdentityUpdated(userId), rustCrypto.onUserIdentityUpdated(userId),
); );

View File

@@ -1486,7 +1486,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
this.logger.debug( this.logger.debug(
`Got update for session ${key.senderKey.toBase64()}|${key.sessionId} in ${key.roomId.toString()}`, `Got update for session ${key.senderKey.toBase64()}|${key.sessionId} in ${key.roomId.toString()}`,
); );
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(key); const pendingList = this.eventDecryptor.getEventsPendingRoomKey(key.roomId.toString(), key.sessionId);
if (pendingList.length === 0) return; if (pendingList.length === 0) return;
this.logger.debug( this.logger.debug(
@@ -1507,6 +1507,37 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
} }
} }
/**
* Callback for `OlmMachine.registerRoomKeyWithheldCallback`.
*
* Called by the rust sdk whenever we are told that a key has been withheld. We see if we had any events that
* failed to decrypt for the given session, and update their status if so.
*
* @param withheld - Details of the withheld sessions.
*/
public async onRoomKeysWithheld(withheld: RustSdkCryptoJs.RoomKeyWithheldInfo[]): Promise<void> {
for (const session of withheld) {
this.logger.debug(`Got withheld message for session ${session.sessionId} in ${session.roomId.toString()}`);
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(
session.roomId.toString(),
session.sessionId,
);
if (pendingList.length === 0) return;
// The easiest way to update the status of the event is to have another go at decrypting it.
this.logger.debug(
"Retrying decryption on events:",
pendingList.map((e) => `${e.getId()}`),
);
for (const ev of pendingList) {
ev.attemptDecryption(this, { isRetry: true }).catch((_e) => {
// It's somewhat expected that we still can't decrypt here.
});
}
}
}
/** /**
* Callback for `OlmMachine.registerUserIdentityUpdatedCallback` * Callback for `OlmMachine.registerUserIdentityUpdatedCallback`
* *
@@ -1683,7 +1714,7 @@ class EventDecryptor {
/** /**
* Events which we couldn't decrypt due to unknown sessions / indexes. * Events which we couldn't decrypt due to unknown sessions / indexes.
* *
* Map from senderKey to sessionId to Set of MatrixEvents * Map from roomId to sessionId to Set of MatrixEvents
*/ */
private eventsPendingKey = new MapWithDefault<string, MapWithDefault<string, Set<MatrixEvent>>>( private eventsPendingKey = new MapWithDefault<string, MapWithDefault<string, Set<MatrixEvent>>>(
() => new MapWithDefault<string, Set<MatrixEvent>>(() => new Set()), () => new MapWithDefault<string, Set<MatrixEvent>>(() => new Set()),
@@ -1843,30 +1874,27 @@ class EventDecryptor {
* Look for events which are waiting for a given megolm session * Look for events which are waiting for a given megolm session
* *
* Returns a list of events which were encrypted by `session` and could not be decrypted * Returns a list of events which were encrypted by `session` and could not be decrypted
*
* @param session -
*/ */
public getEventsPendingRoomKey(session: RustSdkCryptoJs.RoomKeyInfo): MatrixEvent[] { public getEventsPendingRoomKey(roomId: string, sessionId: string): MatrixEvent[] {
const senderPendingEvents = this.eventsPendingKey.get(session.senderKey.toBase64()); const roomPendingEvents = this.eventsPendingKey.get(roomId);
if (!senderPendingEvents) return []; if (!roomPendingEvents) return [];
const sessionPendingEvents = senderPendingEvents.get(session.sessionId); const sessionPendingEvents = roomPendingEvents.get(sessionId);
if (!sessionPendingEvents) return []; if (!sessionPendingEvents) return [];
const roomId = session.roomId.toString(); return [...sessionPendingEvents];
return [...sessionPendingEvents].filter((ev) => ev.getRoomId() === roomId);
} }
/** /**
* Add an event to the list of those awaiting their session keys. * Add an event to the list of those awaiting their session keys.
*/ */
private addEventToPendingList(event: MatrixEvent): void { private addEventToPendingList(event: MatrixEvent): void {
const content = event.getWireContent(); const roomId = event.getRoomId();
const senderKey = content.sender_key; // We shouldn't have events without a room id here.
const sessionId = content.session_id; if (!roomId) return;
const senderPendingEvents = this.eventsPendingKey.getOrCreate(senderKey); const roomPendingEvents = this.eventsPendingKey.getOrCreate(roomId);
const sessionPendingEvents = senderPendingEvents.getOrCreate(sessionId); const sessionPendingEvents = roomPendingEvents.getOrCreate(event.getWireContent().session_id);
sessionPendingEvents.add(event); sessionPendingEvents.add(event);
} }
@@ -1874,23 +1902,22 @@ class EventDecryptor {
* Remove an event from the list of those awaiting their session keys. * Remove an event from the list of those awaiting their session keys.
*/ */
private removeEventFromPendingList(event: MatrixEvent): void { private removeEventFromPendingList(event: MatrixEvent): void {
const content = event.getWireContent(); const roomId = event.getRoomId();
const senderKey = content.sender_key; if (!roomId) return;
const sessionId = content.session_id;
const senderPendingEvents = this.eventsPendingKey.get(senderKey); const roomPendingEvents = this.eventsPendingKey.getOrCreate(roomId);
if (!senderPendingEvents) return; if (!roomPendingEvents) return;
const sessionPendingEvents = senderPendingEvents.get(sessionId); const sessionPendingEvents = roomPendingEvents.get(event.getWireContent().session_id);
if (!sessionPendingEvents) return; if (!sessionPendingEvents) return;
sessionPendingEvents.delete(event); sessionPendingEvents.delete(event);
// also clean up the higher-level maps if they are now empty // also clean up the higher-level maps if they are now empty
if (sessionPendingEvents.size === 0) { if (sessionPendingEvents.size === 0) {
senderPendingEvents.delete(sessionId); roomPendingEvents.delete(event.getWireContent().session_id);
if (senderPendingEvents.size === 0) { if (roomPendingEvents.size === 0) {
this.eventsPendingKey.delete(senderKey); this.eventsPendingKey.delete(roomId);
} }
} }
} }