diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 70374743..13b00f6f 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -400,6 +400,18 @@ export class RoomEncryption { await hsApi.sendToDevice(type, payload, txnId, {log}).response(); } + filterEventEntriesForKeys(entries, keys) { + return entries.filter(entry => { + const {event} = entry; + if (event) { + const senderKey = event.content?.["sender_key"]; + const sessionId = event.content?.["session_id"]; + return keys.some(key => senderKey === key.senderKey && sessionId === key.sessionId); + } + return false; + }); + } + dispose() { this._disposed = true; this._megolmBackfillCache.dispose(); diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 9ea34381..0cc45248 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -189,10 +189,22 @@ export class Room extends EventEmitter { roomEncryption = this._createRoomEncryption(this, summaryChanges.encryption); } + let retryEntries; let decryptPreparation; if (roomEncryption) { // also look for events in timeline here let events = roomResponse?.timeline?.events || []; + // when new keys arrive, also see if any events currently loaded in the timeline + // can now be retried to decrypt + if (this._timeline && newKeys) { + retryEntries = roomEncryption.filterEventEntriesForKeys( + this._timeline.remoteEntries, newKeys); + if (retryEntries.length) { + log.set("retry", retryEntries.length); + events = events.concat(retryEntries.map(entry => entry.event)); + } + } + if (events.length) { const eventsToDecrypt = events.filter(event => { return event?.type === EVENT_ENCRYPTED_TYPE; @@ -207,6 +219,7 @@ export class Room extends EventEmitter { summaryChanges, decryptPreparation, decryptChanges: null, + retryEntries }; } @@ -221,13 +234,24 @@ export class Room extends EventEmitter { } /** @package */ - async writeSync(roomResponse, isInitialSync, {summaryChanges, decryptChanges, roomEncryption}, txn, log) { + async writeSync(roomResponse, isInitialSync, {summaryChanges, decryptChanges, roomEncryption, retryEntries}, txn, log) { log.set("id", this.id); const {entries, newLiveKey, memberChanges} = await log.wrap("syncWriter", log => this._syncWriter.writeSync(roomResponse, txn, log), log.level.Detail); if (decryptChanges) { const decryption = await decryptChanges.write(txn); decryption.applyToEntries(entries); + // now prepend updated copies of retryEntries + // so the originals don't get modified by the decryption + // (not needed for entries as they were just created by syncWriter) + if (retryEntries?.length) { + // TODO: this will modify existing timeline entries (which we should not do in writeSync), + // but it is a temporary way of reattempting decryption while timeline is open + // won't need copies when tracking missing sessions properly + const updatedEntries = decryption.applyToEntries(retryEntries); + // prepend the retried entries, as we know they are older (not that it should matter much for the summary) + entries.unshift(...updatedEntries); + } } // pass member changes to device tracker if (roomEncryption && this.isTrackingMembers && memberChanges?.size) { @@ -255,7 +279,7 @@ export class Room extends EventEmitter { return { summaryChanges, roomEncryption, - newTimelineEntries: entries, + newAndUpdatedEntries: entries, newLiveKey, removedPendingEvents, memberChanges, @@ -268,7 +292,7 @@ export class Room extends EventEmitter { * Called with the changes returned from `writeSync` to apply them and emit changes. * No storage or network operations should be done here. */ - afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, memberChanges, heroChanges, roomEncryption}, log) { + afterSync({summaryChanges, newAndUpdatedEntries, newLiveKey, removedPendingEvents, memberChanges, heroChanges, roomEncryption}, log) { log.set("id", this.id); this._syncWriter.afterSync(newLiveKey); this._setEncryption(roomEncryption); @@ -301,10 +325,10 @@ export class Room extends EventEmitter { this._emitUpdate(); } if (this._timeline) { - this._timeline.appendLiveEntries(newTimelineEntries); + this._timeline.appendLiveEntries(newAndUpdatedEntries); } if (this._observedEvents) { - this._observedEvents.updateEvents(newTimelineEntries); + this._observedEvents.updateEvents(newAndUpdatedEntries); } if (removedPendingEvents) { this._sendQueue.emitRemovals(removedPendingEvents); @@ -522,6 +546,10 @@ export class Room extends EventEmitter { return !!this._summary.data.encryption; } + get membership() { + return this._summary.data.membership; + } + enableSessionBackup(sessionBackup) { this._roomEncryption?.enableSessionBackup(sessionBackup); } diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index a737728c..e5592526 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -116,6 +116,14 @@ export class Timeline { return this._allEntries; } + /** + * @internal + * @return {Array} remote event entries, should not be modified + */ + get remoteEntries() { + return this._remoteEntries.array; + } + /** @public */ dispose() { if (this._closeCallback) {