1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

Merge pull request #1684 from matrix-org/gsouquet/cache-decrypt

This commit is contained in:
Germain
2021-05-12 12:19:59 +01:00
committed by GitHub
7 changed files with 109 additions and 27 deletions

View File

@@ -484,8 +484,9 @@ describe("megolm", function() {
return aliceTestClient.flushSync().then(() => { return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync(); return aliceTestClient.flushSync();
}); });
}).then(function() { }).then(async function() {
const room = aliceTestClient.client.getRoom(ROOM_ID); const room = aliceTestClient.client.getRoom(ROOM_ID);
await room.decryptCriticalEvents();
const event = room.getLiveTimeline().getEvents()[0]; const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42'); expect(event.getContent().body).toEqual('42');
}); });
@@ -933,8 +934,9 @@ describe("megolm", function() {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync(); return aliceTestClient.flushSync();
}).then(function() { }).then(async function() {
const room = aliceTestClient.client.getRoom(ROOM_ID); const room = aliceTestClient.client.getRoom(ROOM_ID);
await room.decryptCriticalEvents();
const event = room.getLiveTimeline().getEvents()[0]; const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42'); expect(event.getContent().body).toEqual('42');

View File

@@ -212,10 +212,12 @@ MockStorageApi.prototype = {
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted * @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
*/ */
export function awaitDecryption(event) { export function awaitDecryption(event) {
if (!event.isBeingDecrypted()) { // An event is not always decrypted ahead of time
return Promise.resolve(event); // getClearContent is a good signal to know whether an event has been decrypted
} // already
if (event.getClearContent() !== null) {
return event;
} else {
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`); logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -225,6 +227,7 @@ export function awaitDecryption(event) {
}); });
}); });
} }
}
export function HttpResponse( export function HttpResponse(

View File

@@ -5554,8 +5554,9 @@ function _resolve(callback, resolve, res) {
resolve(res); resolve(res);
} }
function _PojoToMatrixEventMapper(client, options) { function _PojoToMatrixEventMapper(client, options = {}) {
const preventReEmit = Boolean(options && options.preventReEmit); const preventReEmit = Boolean(options.preventReEmit);
const decrypt = options.decrypt !== false;
function mapper(plainOldJsObject) { function mapper(plainOldJsObject) {
const event = new MatrixEvent(plainOldJsObject); const event = new MatrixEvent(plainOldJsObject);
if (event.isEncrypted()) { if (event.isEncrypted()) {
@@ -5564,8 +5565,10 @@ function _PojoToMatrixEventMapper(client, options) {
"Event.decrypted", "Event.decrypted",
]); ]);
} }
if (decrypt) {
event.attemptDecryption(client._crypto); event.attemptDecryption(client._crypto);
} }
}
if (!preventReEmit) { if (!preventReEmit) {
client.reEmitter.reEmit(event, ["Event.replaced"]); client.reEmitter.reEmit(event, ["Event.replaced"]);
} }
@@ -5577,6 +5580,7 @@ function _PojoToMatrixEventMapper(client, options) {
/** /**
* @param {object} [options] * @param {object} [options]
* @param {bool} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client * @param {bool} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client
* @param {bool} options.decrypt decrypt event proactively
* @return {Function} * @return {Function}
*/ */
MatrixClient.prototype.getEventMapper = function(options = undefined) { MatrixClient.prototype.getEventMapper = function(options = undefined) {

View File

@@ -1692,7 +1692,7 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
await Promise.all([...pending].map(async (ev) => { await Promise.all([...pending].map(async (ev) => {
try { try {
await ev.attemptDecryption(this._crypto, true); await ev.attemptDecryption(this._crypto, { isRetry: true });
} catch (e) { } catch (e) {
// don't die if something goes wrong // don't die if something goes wrong
} }

View File

@@ -399,6 +399,12 @@ utils.extend(MatrixEvent.prototype, {
this._clearEvent.content.msgtype === "m.bad.encrypted"; this._clearEvent.content.msgtype === "m.bad.encrypted";
}, },
shouldAttemptDecryption: function() {
return this.isEncrypted()
&& !this.isBeingDecrypted()
&& this.getClearContent() === null;
},
/** /**
* Start the process of trying to decrypt this event. * Start the process of trying to decrypt this event.
* *
@@ -407,12 +413,22 @@ utils.extend(MatrixEvent.prototype, {
* @internal * @internal
* *
* @param {module:crypto} crypto crypto module * @param {module:crypto} crypto crypto module
* @param {bool} isRetry True if this is a retry (enables more logging) * @param {object} options
* @param {bool} options.isRetry True if this is a retry (enables more logging)
* @param {bool} options.emit Emits "event.decrypted" if set to true
* *
* @returns {Promise} promise which resolves (to undefined) when the decryption * @returns {Promise} promise which resolves (to undefined) when the decryption
* attempt is completed. * attempt is completed.
*/ */
attemptDecryption: async function(crypto, isRetry) { attemptDecryption: async function(crypto, options = {}) {
// For backwards compatibility purposes
// The function signature used to be attemptDecryption(crypto, isRetry)
if (typeof options === "boolean") {
options = {
isRetry: options,
};
}
// start with a couple of sanity checks. // start with a couple of sanity checks.
if (!this.isEncrypted()) { if (!this.isEncrypted()) {
throw new Error("Attempt to decrypt event which isn't encrypted"); throw new Error("Attempt to decrypt event which isn't encrypted");
@@ -442,7 +458,7 @@ utils.extend(MatrixEvent.prototype, {
return this._decryptionPromise; return this._decryptionPromise;
} }
this._decryptionPromise = this._decryptionLoop(crypto, isRetry); this._decryptionPromise = this._decryptionLoop(crypto, options);
return this._decryptionPromise; return this._decryptionPromise;
}, },
@@ -487,7 +503,7 @@ utils.extend(MatrixEvent.prototype, {
return recipients; return recipients;
}, },
_decryptionLoop: async function(crypto, isRetry) { _decryptionLoop: async function(crypto, options = {}) {
// make sure that this method never runs completely synchronously. // make sure that this method never runs completely synchronously.
// (doing so would mean that we would clear _decryptionPromise *before* // (doing so would mean that we would clear _decryptionPromise *before*
// it is set in attemptDecryption - and hence end up with a stuck // it is set in attemptDecryption - and hence end up with a stuck
@@ -504,7 +520,7 @@ utils.extend(MatrixEvent.prototype, {
res = this._badEncryptedMessage("Encryption not enabled"); res = this._badEncryptedMessage("Encryption not enabled");
} else { } else {
res = await crypto.decryptEvent(this); res = await crypto.decryptEvent(this);
if (isRetry) { if (options.isRetry === true) {
logger.info(`Decrypted event on retry (id=${this.getId()})`); logger.info(`Decrypted event on retry (id=${this.getId()})`);
} }
} }
@@ -512,7 +528,7 @@ utils.extend(MatrixEvent.prototype, {
if (e.name !== "DecryptionError") { if (e.name !== "DecryptionError") {
// not a decryption error: log the whole exception as an error // not a decryption error: log the whole exception as an error
// (and don't bother with a retry) // (and don't bother with a retry)
const re = isRetry ? 're' : ''; const re = options.isRetry ? 're' : '';
logger.error( logger.error(
`Error ${re}decrypting event ` + `Error ${re}decrypting event ` +
`(id=${this.getId()}): ${e.stack || e}`, `(id=${this.getId()}): ${e.stack || e}`,
@@ -578,7 +594,9 @@ utils.extend(MatrixEvent.prototype, {
// pick up the wrong contents. // pick up the wrong contents.
this.setPushActions(null); this.setPushActions(null);
if (options.emit !== false) {
this.emit("Event.decrypted", this, err); this.emit("Event.decrypted", this, err);
}
return; return;
} }

View File

@@ -228,6 +228,51 @@ function pendingEventsKey(roomId) {
utils.inherits(Room, EventEmitter); utils.inherits(Room, EventEmitter);
/**
* Bulk decrypt critical events in a room
*
* Critical events represents the minimal set of events to decrypt
* for a typical UI to function properly
*
* - Last event of every room (to generate likely message preview)
* - All events up to the read receipt (to calculate an accurate notification count)
*
* @returns {Promise} Signals when all events have been decrypted
*/
Room.prototype.decryptCriticalEvents = function() {
const readReceiptEventId = this.getEventReadUpTo(this._client.getUserId(), true);
const events = this.getLiveTimeline().getEvents();
const readReceiptTimelineIndex = events.findIndex(matrixEvent => {
return matrixEvent.event.event_id === readReceiptEventId;
});
const decryptionPromises = events
.slice(readReceiptTimelineIndex)
.filter(event => event.shouldAttemptDecryption())
.reverse()
.map(event => event.attemptDecryption(this._client._crypto, { isRetry: true }));
return Promise.allSettled(decryptionPromises);
};
/**
* Bulk decrypt events in a room
*
* @returns {Promise} Signals when all events have been decrypted
*/
Room.prototype.decryptAllEvents = function() {
const decryptionPromises = this
.getUnfilteredTimelineSet()
.getLiveTimeline()
.getEvents()
.filter(event => event.shouldAttemptDecryption())
.reverse()
.map(event => event.attemptDecryption(this._client._crypto, { isRetry: true }));
return Promise.allSettled(decryptionPromises);
};
/** /**
* Gets the version of the room * Gets the version of the room
* @returns {string} The version of the room, or null if it could not be determined * @returns {string} The version of the room, or null if it could not be determined

View File

@@ -1158,10 +1158,15 @@ SyncApi.prototype._processSyncResponse = async function(
await utils.promiseMapSeries(joinRooms, async function(joinObj) { await utils.promiseMapSeries(joinRooms, async function(joinObj) {
const room = joinObj.room; const room = joinObj.room;
const stateEvents = self._mapSyncEventsFormat(joinObj.state, room); const stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room); // Prevent events from being decrypted ahead of time
// this helps large account to speed up faster
// room::decryptCriticalEvent is in charge of decrypting all the events
// required for a client to function properly
const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room, false);
const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral); const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral);
const accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data); const accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data);
const encrypted = client.isRoomEncrypted(room.roomId);
// we do this first so it's correct when any of the events fire // we do this first so it's correct when any of the events fire
if (joinObj.unread_notifications) { if (joinObj.unread_notifications) {
room.setUnreadNotificationCount( room.setUnreadNotificationCount(
@@ -1172,7 +1177,6 @@ SyncApi.prototype._processSyncResponse = async function(
// bother setting it here. We trust our calculations better than the // bother setting it here. We trust our calculations better than the
// server's for this case, and therefore will assume that our non-zero // server's for this case, and therefore will assume that our non-zero
// count is accurate. // count is accurate.
const encrypted = client.isRoomEncrypted(room.roomId);
if (!encrypted if (!encrypted
|| (encrypted && room.getUnreadNotificationCount('highlight') <= 0)) { || (encrypted && room.getUnreadNotificationCount('highlight') <= 0)) {
room.setUnreadNotificationCount( room.setUnreadNotificationCount(
@@ -1294,6 +1298,11 @@ SyncApi.prototype._processSyncResponse = async function(
}); });
room.updateMyMembership("join"); room.updateMyMembership("join");
// Decrypt only the last message in all rooms to make sure we can generate a preview
// And decrypt all events after the recorded read receipt to ensure an accurate
// notification count
room.decryptCriticalEvents();
}); });
// Handle leaves (e.g. kicked rooms) // Handle leaves (e.g. kicked rooms)
@@ -1516,13 +1525,14 @@ SyncApi.prototype._mapSyncResponseToRoomArray = function(obj) {
/** /**
* @param {Object} obj * @param {Object} obj
* @param {Room} room * @param {Room} room
* @param {bool} decrypt
* @return {MatrixEvent[]} * @return {MatrixEvent[]}
*/ */
SyncApi.prototype._mapSyncEventsFormat = function(obj, room) { SyncApi.prototype._mapSyncEventsFormat = function(obj, room, decrypt = true) {
if (!obj || !utils.isArray(obj.events)) { if (!obj || !utils.isArray(obj.events)) {
return []; return [];
} }
const mapper = this.client.getEventMapper(); const mapper = this.client.getEventMapper({ decrypt });
return obj.events.map(function(e) { return obj.events.map(function(e) {
if (room) { if (room) {
e.room_id = room.roomId; e.room_id = room.roomId;