From 80ede4f4111df1379ba3270f1fdc038b6c1e0c27 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 12:05:58 +0200 Subject: [PATCH 01/19] session will always be true here, we want to check sessionEntry --- src/matrix/e2ee/megolm/Encryption.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/Encryption.js b/src/matrix/e2ee/megolm/Encryption.js index 0b374f48..e39280ea 100644 --- a/src/matrix/e2ee/megolm/Encryption.js +++ b/src/matrix/e2ee/megolm/Encryption.js @@ -42,7 +42,7 @@ export class Encryption { } if (!sessionEntry || this._needsToRotate(session, sessionEntry.createdAt, encryptionParams)) { // in the case of rotating, recreate a session as we already unpickled into it - if (session) { + if (sessionEntry) { session.free(); session = new this._olm.OutboundGroupSession(); } From fab58e8724353882e66676d813ea86f18c9b75a1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 12:06:26 +0200 Subject: [PATCH 02/19] first draft of megolm decryption --- src/matrix/e2ee/megolm/Decryption.js | 129 ++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 4 deletions(-) diff --git a/src/matrix/e2ee/megolm/Decryption.js b/src/matrix/e2ee/megolm/Decryption.js index 68bca6fe..6d7941a0 100644 --- a/src/matrix/e2ee/megolm/Decryption.js +++ b/src/matrix/e2ee/megolm/Decryption.js @@ -14,12 +14,97 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {DecryptionError} from "../common.js"; + +const CACHE_MAX_SIZE = 10; + export class Decryption { constructor({pickleKey, olm}) { this._pickleKey = pickleKey; this._olm = olm; } + createSessionCache() { + return new SessionCache(); + } + + async decryptNewEvent(roomId, event, sessionCache, txn) { + const {payload, messageIndex} = this._decrypt(roomId, event, sessionCache, txn); + const sessionId = event.content?.["session_id"]; + this._handleReplayAttacks(roomId, sessionId, messageIndex, event, txn); + return payload; + } + + async decryptStoredEvent(roomId, event, sessionCache, txn) { + const {payload} = this._decrypt(roomId, event, sessionCache, txn); + return payload; + } + + async _decrypt(roomId, event, sessionCache, txn) { + const senderKey = event.content?.["sender_key"]; + const sessionId = event.content?.["session_id"]; + const ciphertext = event.content?.ciphertext; + + if ( + typeof senderKey !== "string" || + typeof sessionId !== "string" || + typeof ciphertext !== "string" + ) { + throw new DecryptionError("MEGOLM_INVALID_EVENT", event); + } + + let session = sessionCache.get(roomId, senderKey, sessionId); + if (!session) { + const sessionEntry = await txn.inboundGroupSessions.get(roomId, senderKey, sessionId); + if (sessionEntry) { + session = new this._olm.InboundGroupSession(); + try { + session.unpickle(this._pickleKey, sessionEntry.session); + } catch (err) { + session.free(); + throw err; + } + sessionCache.add(roomId, senderKey, session); + } + } + if (!session) { + return; + } + const {plaintext, message_index: messageIndex} = session.decrypt(ciphertext); + let payload; + try { + payload = JSON.parse(plaintext); + } catch (err) { + throw new DecryptionError("NOT_JSON", event, {plaintext, err}); + } + if (payload.room_id !== roomId) { + throw new DecryptionError("MEGOLM_WRONG_ROOM", event, + {encryptedRoomId: payload.room_id, eventRoomId: roomId}); + } + return {payload, messageIndex}; + } + + async _handleReplayAttacks(roomId, sessionId, messageIndex, event, txn) { + const eventId = event.event_id; + const timestamp = event.origin_server_ts; + const decryption = await txn.groupSessionDecryptions.get(roomId, sessionId, messageIndex); + if (decryption && decryption.eventId !== eventId) { + // the one with the newest timestamp should be the attack + const decryptedEventIsBad = decryption.timestamp < timestamp; + const badEventId = decryptedEventIsBad ? eventId : decryption.eventId; + throw new DecryptionError("MEGOLM_REPLAY_ATTACK", event, {badEventId, otherEventId: decryption.eventId}); + } + if (!decryption) { + txn.groupSessionDecryptions.set({ + roomId, + sessionId, + messageIndex, + eventId, + timestamp + }); + } + } + async addRoomKeys(payloads, txn) { const newSessions = []; for (const {senderKey, event} of payloads) { @@ -56,13 +141,49 @@ export class Decryption { } } + // this will be passed to the Room in notifyRoomKeys return newSessions; } +} - applyRoomKeyChanges(newSessions) { - // retry decryption with the new sessions - if (newSessions.length) { - console.log(`I have ${newSessions.length} new inbound group sessions`, newSessions) +class SessionCache { + constructor() { + this._sessions = []; + } + + get(roomId, senderKey, sessionId) { + const idx = this._sessions.findIndex(s => { + return s.roomId === roomId && + s.senderKey === senderKey && + sessionId === s.session.session_id(); + }); + if (idx !== -1) { + const entry = this._sessions[idx]; + // move to top + if (idx > 0) { + this._sessions.splice(idx, 1); + this._sessions.unshift(entry); + } + return entry.session; } } + + add(roomId, senderKey, session) { + // add new at top + this._sessions.unshift({roomId, senderKey, session}); + if (this._sessions.length > CACHE_MAX_SIZE) { + // free sessions we're about to remove + for (let i = CACHE_MAX_SIZE; i < this._sessions.length; i += 1) { + this._sessions[i].session.free(); + } + this._sessions = this._sessions.slice(0, CACHE_MAX_SIZE); + } + } + + dispose() { + for (const entry of this._sessions) { + entry.session.free(); + } + + } } From 502ba5deea7c7b8937ebf42ea2e48ca2213c7013 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 12:09:19 +0200 Subject: [PATCH 03/19] first draft of decryption in Room and RoomEncryption --- src/matrix/DeviceMessageHandler.js | 17 ++++++------ src/matrix/Session.js | 4 +-- src/matrix/e2ee/RoomEncryption.js | 27 +++++++++++++++++++ src/matrix/room/Room.js | 27 ++++++++++++++++++- .../room/timeline/entries/EventEntry.js | 13 +++++++++ 5 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/matrix/DeviceMessageHandler.js b/src/matrix/DeviceMessageHandler.js index 537b948d..51a2378c 100644 --- a/src/matrix/DeviceMessageHandler.js +++ b/src/matrix/DeviceMessageHandler.js @@ -15,6 +15,7 @@ limitations under the License. */ import {OLM_ALGORITHM, MEGOLM_ALGORITHM} from "./e2ee/common.js"; +import {groupBy} from "../utils/groupBy.js"; // key to store in session store const PENDING_ENCRYPTED_EVENTS = "pendingEncryptedDeviceEvents"; @@ -44,21 +45,21 @@ export class DeviceMessageHandler { const megOlmRoomKeysPayloads = payloads.filter(p => { return p.event?.type === "m.room_key" && p.event.content?.algorithm === MEGOLM_ALGORITHM; }); - let megolmChanges; + let roomKeys; if (megOlmRoomKeysPayloads.length) { - megolmChanges = await this._megolmDecryption.addRoomKeys(megOlmRoomKeysPayloads, txn); + roomKeys = await this._megolmDecryption.addRoomKeys(megOlmRoomKeysPayloads, txn); } - return {megolmChanges}; + return {roomKeys}; } - _applyDecryptChanges({megolmChanges}) { - if (megolmChanges) { - this._megolmDecryption.applyRoomKeyChanges(megolmChanges); + _applyDecryptChanges(rooms, {roomKeys}) { + const roomKeysByRoom = groupBy(roomKeys, s => s.roomId); + for (const [roomId, roomKeys] of roomKeysByRoom) { } } // not safe to call multiple times without awaiting first call - async decryptPending() { + async decryptPending(rooms) { if (!this._olmDecryption) { return; } @@ -89,7 +90,7 @@ export class DeviceMessageHandler { throw err; } await txn.complete(); - this._applyDecryptChanges(changes); + this._applyDecryptChanges(rooms, changes); } async _getPendingEvents(txn) { diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 1d0ac73e..c3429075 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -150,7 +150,7 @@ export class Session { } await this._e2eeAccount.generateOTKsIfNeeded(this._storage); await this._e2eeAccount.uploadKeys(this._storage); - await this._deviceMessageHandler.decryptPending(); + await this._deviceMessageHandler.decryptPending(this.rooms); } } @@ -285,7 +285,7 @@ export class Session { async afterSyncCompleted() { const needsToUploadOTKs = await this._e2eeAccount.generateOTKsIfNeeded(this._storage); - const promises = [this._deviceMessageHandler.decryptPending()]; + const promises = [this._deviceMessageHandler.decryptPending(this.rooms)]; if (needsToUploadOTKs) { // TODO: we could do this in parallel with sync if it proves to be too slow // but I'm not sure how to not swallow errors in that case diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 5f0c4cc1..f5e91913 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -27,12 +27,39 @@ export class RoomEncryption { this._megolmEncryption = megolmEncryption; // content of the m.room.encryption event this._encryptionParams = encryptionParams; + + this._megolmBackfillCache = this._megolmDecryption.createSessionCache(); + this._megolmSyncCache = this._megolmDecryption.createSessionCache(); + } + + notifyTimelineClosed() { + // empty the backfill cache when closing the timeline + this._megolmBackfillCache.dispose(); + this._megolmBackfillCache = this._megolmDecryption.createSessionCache(); } async writeMemberChanges(memberChanges, txn) { return await this._deviceTracker.writeMemberChanges(this._room, memberChanges, txn); } + async decryptNewSyncEvent(id, event, txn) { + const payload = await this._megolmDecryption.decryptNewEvent( + this._room.id, event, this._megolmSyncCache, txn); + return payload; + } + + async decryptNewGapEvent(id, event, txn) { + const payload = await this._megolmDecryption.decryptNewEvent( + this._room.id, event, this._megolmBackfillCache, txn); + return payload; + } + + async decryptStoredEvent(id, event, txn) { + const payload = await this._megolmDecryption.decryptStoredEvent( + this._room.id, event, this._megolmBackfillCache, txn); + return payload; + } + async encrypt(type, content, hsApi) { const megolmResult = await this._megolmEncryption.encrypt(this._room.id, type, content, this._encryptionParams); // share the new megolm session if needed diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index d2272b3d..14579b73 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -45,6 +45,22 @@ export class Room extends EventEmitter { this._roomEncryption = null; } + async _decryptSyncEntries(entries, txn) { + await Promise.all(entries.map(async e => { + if (e.eventType === "m.room.encrypted") { + try { + const decryptedEvent = await this._roomEncryption.decryptNewSyncEvent(e.internalId, e.event, txn); + if (decryptedEvent) { + e.replaceWithDecrypted(decryptedEvent); + } + } catch (err) { + e.setDecryptionError(err); + } + } + })); + return entries; + } + /** @package */ async writeSync(roomResponse, membership, isInitialSync, txn) { const isTimelineOpen = !!this._timeline; @@ -53,7 +69,13 @@ export class Room extends EventEmitter { membership, isInitialSync, isTimelineOpen, txn); - const {entries, newLiveKey, memberChanges} = await this._syncWriter.writeSync(roomResponse, txn); + const {entries: encryptedEntries, newLiveKey, memberChanges} = + await this._syncWriter.writeSync(roomResponse, txn); + // decrypt if applicable + let entries = encryptedEntries; + if (this._roomEncryption) { + entries = await this._decryptSyncEntries(encryptedEntries, txn); + } // fetch new members while we have txn open, // but don't make any in-memory changes yet let heroChanges; @@ -341,6 +363,9 @@ export class Room extends EventEmitter { closeCallback: () => { console.log(`closing the timeline for ${this._roomId}`); this._timeline = null; + if (this._roomEncryption) { + this._roomEncryption.notifyTimelineClosed(); + } }, user: this._user, }); diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 4dce9834..2bf5d941 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -21,6 +21,7 @@ export class EventEntry extends BaseEntry { constructor(eventEntry, fragmentIdComparer) { super(fragmentIdComparer); this._eventEntry = eventEntry; + this._decryptionError = null; } get fragmentId() { @@ -31,6 +32,10 @@ export class EventEntry extends BaseEntry { return this._eventEntry.eventIndex; } + get internalId() { + return `${this.fragmentId}|${this.entryIndex}`; + } + get content() { return this._eventEntry.event.content; } @@ -66,4 +71,12 @@ export class EventEntry extends BaseEntry { get id() { return this._eventEntry.event.event_id; } + + replaceWithDecrypted(event) { + this._eventEntry.event = event; + } + + setDecryptionError(err) { + this._decryptionError = err; + } } From fe9245dd04afa6123df15e7c341bca6b44b0d939 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 12:10:12 +0200 Subject: [PATCH 04/19] first draft of retrying decryption when receiving room keys --- src/matrix/DeviceMessageHandler.js | 2 ++ src/matrix/e2ee/RoomEncryption.js | 37 ++++++++++++++++++++++++++++++ src/matrix/room/Room.js | 9 ++++++++ 3 files changed, 48 insertions(+) diff --git a/src/matrix/DeviceMessageHandler.js b/src/matrix/DeviceMessageHandler.js index 51a2378c..d8698127 100644 --- a/src/matrix/DeviceMessageHandler.js +++ b/src/matrix/DeviceMessageHandler.js @@ -55,6 +55,8 @@ export class DeviceMessageHandler { _applyDecryptChanges(rooms, {roomKeys}) { const roomKeysByRoom = groupBy(roomKeys, s => s.roomId); for (const [roomId, roomKeys] of roomKeysByRoom) { + const room = rooms.get(roomId); + room?.notifyRoomKeys(roomKeys); } } diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index f5e91913..caec39ce 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -30,6 +30,8 @@ export class RoomEncryption { this._megolmBackfillCache = this._megolmDecryption.createSessionCache(); this._megolmSyncCache = this._megolmDecryption.createSessionCache(); + // not `event_id`, but an internal event id passed in to the decrypt methods + this._eventIdsByMissingSession = new Map(); } notifyTimelineClosed() { @@ -45,21 +47,56 @@ export class RoomEncryption { async decryptNewSyncEvent(id, event, txn) { const payload = await this._megolmDecryption.decryptNewEvent( this._room.id, event, this._megolmSyncCache, txn); + if (!payload) { + this._addMissingSessionEvent(id, event); + } return payload; } async decryptNewGapEvent(id, event, txn) { const payload = await this._megolmDecryption.decryptNewEvent( this._room.id, event, this._megolmBackfillCache, txn); + if (!payload) { + this._addMissingSessionEvent(id, event); + } return payload; } async decryptStoredEvent(id, event, txn) { const payload = await this._megolmDecryption.decryptStoredEvent( this._room.id, event, this._megolmBackfillCache, txn); + if (!payload) { + this._addMissingSessionEvent(id, event); + } return payload; } + _addMissingSessionEvent(id, event) { + const senderKey = event.content?.["sender_key"]; + const sessionId = event.content?.["session_id"]; + const key = `${senderKey}|${sessionId}`; + let eventIds = this._eventIdsByMissingSession.get(key); + if (!eventIds) { + eventIds = new Set(); + this._eventIdsByMissingSession.set(key, eventIds); + } + eventIds.add(id); + } + + applyRoomKeys(roomKeys) { + // retry decryption with the new sessions + const idsToRetry = []; + for (const roomKey of roomKeys) { + const key = `${roomKey.senderKey}|${roomKey.sessionId}`; + const idsForSession = this._eventIdsByMissingSession.get(key); + if (idsForSession) { + this._eventIdsByMissingSession.delete(key); + idsToRetry.push(...Array.from(idsForSession)); + } + } + return idsToRetry; + } + async encrypt(type, content, hsApi) { const megolmResult = await this._megolmEncryption.encrypt(this._room.id, type, content, this._encryptionParams); // share the new megolm session if needed diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 14579b73..493febdb 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -45,6 +45,15 @@ export class Room extends EventEmitter { this._roomEncryption = null; } + notifyRoomKeys(roomKeys) { + if (this._roomEncryption) { + const internalIdsToRetry = this._roomEncryption.applyRoomKeys(roomKeys); + if (this._timeline) { + + } + } + } + async _decryptSyncEntries(entries, txn) { await Promise.all(entries.map(async e => { if (e.eventType === "m.room.encrypted") { From 28b46a1e5bcb9b81afe0774f0bc7c70edbb9e722 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 12:10:28 +0200 Subject: [PATCH 05/19] add some comments --- src/matrix/e2ee/megolm/Encryption.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/matrix/e2ee/megolm/Encryption.js b/src/matrix/e2ee/megolm/Encryption.js index e39280ea..9849ce55 100644 --- a/src/matrix/e2ee/megolm/Encryption.js +++ b/src/matrix/e2ee/megolm/Encryption.js @@ -36,6 +36,7 @@ export class Encryption { let roomKeyMessage; let encryptedContent; try { + // TODO: we could consider keeping the session in memory for the current room let sessionEntry = await txn.outboundGroupSessions.get(roomId); if (sessionEntry) { session.unpickle(this._pickleKey, sessionEntry.session); @@ -114,6 +115,11 @@ export class Encryption { session_id: session.session_id(), session_key: session.session_key(), algorithm: MEGOLM_ALGORITHM, + // if we need to do this, do we need to create + // the room key message after or before having encrypted + // with the new session? I guess before as we do now + // because the chain_index is where you should start decrypting? + // // chain_index: session.message_index() } } From 565fdb0f8c82ae04d3f86d56eceb4a0981ed6d61 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 12:10:36 +0200 Subject: [PATCH 06/19] use proper error codes --- src/matrix/e2ee/olm/Decryption.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/matrix/e2ee/olm/Decryption.js b/src/matrix/e2ee/olm/Decryption.js index dfde7674..d37b394f 100644 --- a/src/matrix/e2ee/olm/Decryption.js +++ b/src/matrix/e2ee/olm/Decryption.js @@ -115,13 +115,13 @@ export class Decryption { try { payload = JSON.parse(plaintext); } catch (err) { - throw new DecryptionError("Could not JSON decode plaintext", event, {plaintext, err}); + throw new DecryptionError("NOT_JSON", event, {plaintext, err}); } this._validatePayload(payload, event); return {event: payload, senderKey}; } else { - throw new DecryptionError("Didn't find any session to decrypt with", event, - {sessionIds: senderKeyDecryption.sessions.map(s => s.id)}); + throw new DecryptionError("OLM_NO_MATCHING_SESSION", event, + {knownSessionIds: senderKeyDecryption.sessions.map(s => s.id)}); } } From 62bcb277847e33057a2c4513e903b4442277a80c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 15:28:22 +0200 Subject: [PATCH 07/19] implement decryption retrying and decrypting of gap/load entries turns out we do have to always check for replay attacks because failing to decrypt doesn't prevent an item from being stored, so if you reload and then load you might be decrypting it for the first time --- src/matrix/Sync.js | 2 + src/matrix/e2ee/RoomEncryption.js | 47 ++++------- src/matrix/e2ee/megolm/Decryption.js | 24 ++---- src/matrix/room/Room.js | 112 ++++++++++++++++++++------- src/matrix/room/timeline/Timeline.js | 26 +++++++ src/observable/list/SortedArray.js | 16 ++++ 6 files changed, 151 insertions(+), 76 deletions(-) diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index 3c04f71a..09ed1824 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -133,6 +133,8 @@ export class Sync { storeNames.timelineFragments, storeNames.pendingEvents, storeNames.userIdentities, + storeNames.inboundGroupSessions, + storeNames.groupSessionDecryptions, ]); const roomChanges = []; let sessionChanges; diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index caec39ce..2fd3fc3f 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {MEGOLM_ALGORITHM} from "./common.js"; import {groupBy} from "../../utils/groupBy.js"; import {makeTxnId} from "../common.js"; @@ -44,57 +45,43 @@ export class RoomEncryption { return await this._deviceTracker.writeMemberChanges(this._room, memberChanges, txn); } - async decryptNewSyncEvent(id, event, txn) { - const payload = await this._megolmDecryption.decryptNewEvent( - this._room.id, event, this._megolmSyncCache, txn); + async decrypt(event, isSync, retryData, txn) { + if (event.content?.algorithm !== MEGOLM_ALGORITHM) { + throw new Error("Unsupported algorithm: " + event.content?.algorithm); + } + let sessionCache = isSync ? this._megolmSyncCache : this._megolmBackfillCache; + const payload = await this._megolmDecryption.decrypt( + this._room.id, event, sessionCache, txn); if (!payload) { - this._addMissingSessionEvent(id, event); + this._addMissingSessionEvent(event, isSync, retryData); } return payload; } - async decryptNewGapEvent(id, event, txn) { - const payload = await this._megolmDecryption.decryptNewEvent( - this._room.id, event, this._megolmBackfillCache, txn); - if (!payload) { - this._addMissingSessionEvent(id, event); - } - return payload; - } - - async decryptStoredEvent(id, event, txn) { - const payload = await this._megolmDecryption.decryptStoredEvent( - this._room.id, event, this._megolmBackfillCache, txn); - if (!payload) { - this._addMissingSessionEvent(id, event); - } - return payload; - } - - _addMissingSessionEvent(id, event) { + _addMissingSessionEvent(event, isSync, data) { const senderKey = event.content?.["sender_key"]; const sessionId = event.content?.["session_id"]; const key = `${senderKey}|${sessionId}`; let eventIds = this._eventIdsByMissingSession.get(key); if (!eventIds) { - eventIds = new Set(); + eventIds = new Map(); this._eventIdsByMissingSession.set(key, eventIds); } - eventIds.add(id); + eventIds.set(event.event_id, {data, isSync}); } applyRoomKeys(roomKeys) { // retry decryption with the new sessions - const idsToRetry = []; + const retryEntries = []; for (const roomKey of roomKeys) { const key = `${roomKey.senderKey}|${roomKey.sessionId}`; - const idsForSession = this._eventIdsByMissingSession.get(key); - if (idsForSession) { + const entriesForSession = this._eventIdsByMissingSession.get(key); + if (entriesForSession) { this._eventIdsByMissingSession.delete(key); - idsToRetry.push(...Array.from(idsForSession)); + retryEntries.push(...entriesForSession.values()); } } - return idsToRetry; + return retryEntries; } async encrypt(type, content, hsApi) { diff --git a/src/matrix/e2ee/megolm/Decryption.js b/src/matrix/e2ee/megolm/Decryption.js index 6d7941a0..395b03a0 100644 --- a/src/matrix/e2ee/megolm/Decryption.js +++ b/src/matrix/e2ee/megolm/Decryption.js @@ -28,19 +28,7 @@ export class Decryption { return new SessionCache(); } - async decryptNewEvent(roomId, event, sessionCache, txn) { - const {payload, messageIndex} = this._decrypt(roomId, event, sessionCache, txn); - const sessionId = event.content?.["session_id"]; - this._handleReplayAttacks(roomId, sessionId, messageIndex, event, txn); - return payload; - } - - async decryptStoredEvent(roomId, event, sessionCache, txn) { - const {payload} = this._decrypt(roomId, event, sessionCache, txn); - return payload; - } - - async _decrypt(roomId, event, sessionCache, txn) { + async decrypt(roomId, event, sessionCache, txn) { const senderKey = event.content?.["sender_key"]; const sessionId = event.content?.["session_id"]; const ciphertext = event.content?.ciphertext; @@ -75,16 +63,18 @@ export class Decryption { try { payload = JSON.parse(plaintext); } catch (err) { - throw new DecryptionError("NOT_JSON", event, {plaintext, err}); + throw new DecryptionError("PLAINTEXT_NOT_JSON", event, {plaintext, err}); } if (payload.room_id !== roomId) { throw new DecryptionError("MEGOLM_WRONG_ROOM", event, {encryptedRoomId: payload.room_id, eventRoomId: roomId}); } - return {payload, messageIndex}; + await this._handleReplayAttack(roomId, sessionId, messageIndex, event, txn); + // TODO: verify event came from said senderKey + return payload; } - async _handleReplayAttacks(roomId, sessionId, messageIndex, event, txn) { + async _handleReplayAttack(roomId, sessionId, messageIndex, event, txn) { const eventId = event.event_id; const timestamp = event.origin_server_ts; const decryption = await txn.groupSessionDecryptions.get(roomId, sessionId, messageIndex); @@ -92,7 +82,7 @@ export class Decryption { // the one with the newest timestamp should be the attack const decryptedEventIsBad = decryption.timestamp < timestamp; const badEventId = decryptedEventIsBad ? eventId : decryption.eventId; - throw new DecryptionError("MEGOLM_REPLAY_ATTACK", event, {badEventId, otherEventId: decryption.eventId}); + throw new DecryptionError("MEGOLM_REPLAYED_INDEX", event, {badEventId, otherEventId: decryption.eventId}); } if (!decryption) { txn.groupSessionDecryptions.set({ diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 493febdb..7cfe4307 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -25,6 +25,8 @@ import {WrappedError} from "../error.js" import {fetchOrLoadMembers} from "./members/load.js"; import {MemberList} from "./members/MemberList.js"; import {Heroes} from "./members/Heroes.js"; +import {EventEntry} from "./timeline/entries/EventEntry.js"; +import {EventKey} from "./timeline/EventKey.js"; export class Room extends EventEmitter { constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user, createRoomEncryption}) { @@ -45,29 +47,75 @@ export class Room extends EventEmitter { this._roomEncryption = null; } - notifyRoomKeys(roomKeys) { + async notifyRoomKeys(roomKeys) { if (this._roomEncryption) { - const internalIdsToRetry = this._roomEncryption.applyRoomKeys(roomKeys); - if (this._timeline) { - + // array of {data, source} + let retryEntries = this._roomEncryption.applyRoomKeys(roomKeys); + let decryptedEntries = []; + if (retryEntries.length) { + // groupSessionDecryptions can be written, the other stores not + const txn = await this._storage.readWriteTxn([ + this._storage.storeNames.timelineEvents, + this._storage.storeNames.inboundGroupSessions, + this._storage.storeNames.groupSessionDecryptions, + ]); + try { + for (const retryEntry of retryEntries) { + const {data: eventKey} = retryEntry; + let entry = this._timeline?.findEntry(eventKey); + if (!entry) { + const storageEntry = await txn.timelineEvents.get(this._roomId, eventKey.fragmentId, eventKey.entryIndex); + if (storageEntry) { + entry = new EventEntry(storageEntry, this._fragmentIdComparer); + } + } + if (entry) { + entry = await this._decryptEntry(entry, txn, retryEntry.isSync); + decryptedEntries.push(entry); + } + } + } catch (err) { + txn.abort(); + throw err; + } + await txn.complete(); } + if (this._timeline) { + // only adds if already present + this._timeline.replaceEntries(decryptedEntries); + } + // pass decryptedEntries to roomSummary } } - async _decryptSyncEntries(entries, txn) { - await Promise.all(entries.map(async e => { - if (e.eventType === "m.room.encrypted") { - try { - const decryptedEvent = await this._roomEncryption.decryptNewSyncEvent(e.internalId, e.event, txn); - if (decryptedEvent) { - e.replaceWithDecrypted(decryptedEvent); - } - } catch (err) { - e.setDecryptionError(err); + _enableEncryption(encryptionParams) { + this._roomEncryption = this._createRoomEncryption(this, encryptionParams); + if (this._roomEncryption) { + this._sendQueue.enableEncryption(this._roomEncryption); + this._timeline.enableEncryption(this._decryptEntries.bind(this)); + } + } + + async _decryptEntry(entry, txn, isSync) { + if (entry.eventType === "m.room.encrypted") { + try { + const {fragmentId, entryIndex} = entry; + const key = new EventKey(fragmentId, entryIndex); + const decryptedEvent = await this._roomEncryption.decrypt( + entry.event, isSync, key, txn); + if (decryptedEvent) { + entry.replaceWithDecrypted(decryptedEvent); } + } catch (err) { + console.warn("event decryption error", err, entry.event); + entry.setDecryptionError(err); } - })); - return entries; + } + return entry; + } + + async _decryptEntries(entries, txn, isSync = false) { + return await Promise.all(entries.map(async e => this._decryptEntry(e, txn, isSync))); } /** @package */ @@ -83,7 +131,7 @@ export class Room extends EventEmitter { // decrypt if applicable let entries = encryptedEntries; if (this._roomEncryption) { - entries = await this._decryptSyncEntries(encryptedEntries, txn); + entries = await this._decryptEntries(encryptedEntries, txn, true); } // fetch new members while we have txn open, // but don't make any in-memory changes yet @@ -116,12 +164,8 @@ export class Room extends EventEmitter { /** @package */ afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, memberChanges, heroChanges}) { this._syncWriter.afterSync(newLiveKey); - // encryption got enabled if (!this._summary.encryption && summaryChanges.encryption && !this._roomEncryption) { - this._roomEncryption = this._createRoomEncryption(this, summaryChanges.encryption); - if (this._roomEncryption) { - this._sendQueue.enableEncryption(this._roomEncryption); - } + this._enableEncryption(summaryChanges.encryption); } if (memberChanges.size) { if (this._changedMembersDuringSync) { @@ -170,10 +214,7 @@ export class Room extends EventEmitter { try { this._summary.load(summary); if (this._summary.encryption) { - this._roomEncryption = this._createRoomEncryption(this, this._summary.encryption); - if (this._roomEncryption) { - this._sendQueue.enableEncryption(this._roomEncryption); - } + this._enableEncryption(this._summary.encryption); } // need to load members for name? if (this._summary.needsHeroes) { @@ -231,11 +272,18 @@ export class Room extends EventEmitter { } }).response(); - const txn = await this._storage.readWriteTxn([ + let stores = [ this._storage.storeNames.pendingEvents, this._storage.storeNames.timelineEvents, this._storage.storeNames.timelineFragments, - ]); + ]; + if (this._roomEncryption) { + stores = stores.concat([ + this._storage.storeNames.inboundGroupSessions, + this._storage.storeNames.groupSessionDecryptions, + ]); + } + const txn = await this._storage.readWriteTxn(stores); let removedPendingEvents; let gapResult; try { @@ -245,9 +293,12 @@ export class Room extends EventEmitter { const gapWriter = new GapWriter({ roomId: this._roomId, storage: this._storage, - fragmentIdComparer: this._fragmentIdComparer + fragmentIdComparer: this._fragmentIdComparer, }); gapResult = await gapWriter.writeFragmentFill(fragmentEntry, response, txn); + if (this._roomEncryption) { + gapResult.entries = await this._decryptEntries(gapResult.entries, false, txn); + } } catch (err) { txn.abort(); throw err; @@ -378,6 +429,9 @@ export class Room extends EventEmitter { }, user: this._user, }); + if (this._roomEncryption) { + this._timeline.enableEncryption(this._decryptEntries.bind(this)); + } await this._timeline.load(); return this._timeline; } diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index a64be169..c2e9d0ce 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -18,6 +18,7 @@ import {SortedArray, MappedList, ConcatList} from "../../../observable/index.js" import {Direction} from "./Direction.js"; import {TimelineReader} from "./persistence/TimelineReader.js"; import {PendingEventEntry} from "./entries/PendingEventEntry.js"; +import {EventEntry} from "./entries/EventEntry.js"; export class Timeline { constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, user}) { @@ -45,6 +46,27 @@ export class Timeline { this._remoteEntries.setManySorted(entries); } + findEntry(eventKey) { + // a storage event entry has a fragmentId and eventIndex property, used for sorting, + // just like an EventKey, so this will work, but perhaps a bit brittle. + const entry = new EventEntry(eventKey, this._fragmentIdComparer); + try { + const idx = this._remoteEntries.indexOf(entry); + if (idx !== -1) { + return this._remoteEntries.get(idx); + } + } catch (err) { + // fragmentIdComparer threw, ignore + return; + } + } + + replaceEntries(entries) { + for (const entry of entries) { + this._remoteEntries.replace(entry); + } + } + // TODO: should we rather have generic methods for // - adding new entries // - updating existing entries (redaction, relations) @@ -84,4 +106,8 @@ export class Timeline { this._closeCallback = null; } } + + enableEncryption(decryptEntries) { + this._timelineReader.enableEncryption(decryptEntries); + } } diff --git a/src/observable/list/SortedArray.js b/src/observable/list/SortedArray.js index 2245dbd9..3348307b 100644 --- a/src/observable/list/SortedArray.js +++ b/src/observable/list/SortedArray.js @@ -41,6 +41,22 @@ export class SortedArray extends BaseObservableList { } } + replace(item) { + const idx = this.indexOf(item); + if (idx !== -1) { + this._items[idx] = item; + } + } + + indexOf(item) { + const idx = sortedIndex(this._items, item, this._comparator); + if (idx < this._items.length && this._comparator(this._items[idx], item) === 0) { + return idx; + } else { + return -1; + } + } + set(item, updateParams = null) { const idx = sortedIndex(this._items, item, this._comparator); if (idx >= this._items.length || this._comparator(this._items[idx], item) !== 0) { From 5a731903da46fd2cc487d0f9494a466ff7d9ac08 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 15:30:06 +0200 Subject: [PATCH 08/19] implement decrypting when loading timeline --- .../timeline/persistence/TimelineReader.js | 70 +++++++++++++------ 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/src/matrix/room/timeline/persistence/TimelineReader.js b/src/matrix/room/timeline/persistence/TimelineReader.js index 928d6b64..6b3ab23e 100644 --- a/src/matrix/room/timeline/persistence/TimelineReader.js +++ b/src/matrix/room/timeline/persistence/TimelineReader.js @@ -24,18 +24,41 @@ export class TimelineReader { this._roomId = roomId; this._storage = storage; this._fragmentIdComparer = fragmentIdComparer; + this._decryptEntries = null; + } + + enableEncryption(decryptEntries) { + this._decryptEntries = decryptEntries; } _openTxn() { - return this._storage.readTxn([ - this._storage.storeNames.timelineEvents, - this._storage.storeNames.timelineFragments, - ]); + if (this._decryptEntries) { + return this._storage.readWriteTxn([ + this._storage.storeNames.timelineEvents, + this._storage.storeNames.timelineFragments, + this._storage.storeNames.inboundGroupSessions, + this._storage.storeNames.groupSessionDecryptions, + ]); + + } else { + return this._storage.readTxn([ + this._storage.storeNames.timelineEvents, + this._storage.storeNames.timelineFragments, + ]); + } } async readFrom(eventKey, direction, amount) { const txn = await this._openTxn(); - return this._readFrom(eventKey, direction, amount, txn); + let entries; + try { + entries = await this._readFrom(eventKey, direction, amount, txn); + } catch (err) { + txn.abort(); + throw err; + } + await txn.complete(); + return entries; } async _readFrom(eventKey, direction, amount, txn) { @@ -50,7 +73,10 @@ export class TimelineReader { } else { eventsWithinFragment = await timelineStore.eventsBefore(this._roomId, eventKey, amount); } - const eventEntries = eventsWithinFragment.map(e => new EventEntry(e, this._fragmentIdComparer)); + let eventEntries = eventsWithinFragment.map(e => new EventEntry(e, this._fragmentIdComparer)); + if (this._decryptEntries) { + eventEntries = await this._decryptEntries(eventEntries, txn); + } entries = directionalConcat(entries, eventEntries, direction); // prepend or append eventsWithinFragment to entries, and wrap them in EventEntry @@ -78,22 +104,24 @@ export class TimelineReader { async readFromEnd(amount) { const txn = await this._openTxn(); - const liveFragment = await txn.timelineFragments.liveFragment(this._roomId); - // room hasn't been synced yet - if (!liveFragment) { - return []; + let entries; + try { + const liveFragment = await txn.timelineFragments.liveFragment(this._roomId); + // room hasn't been synced yet + if (!liveFragment) { + entries = []; + } else { + this._fragmentIdComparer.add(liveFragment); + const liveFragmentEntry = FragmentBoundaryEntry.end(liveFragment, this._fragmentIdComparer); + const eventKey = liveFragmentEntry.asEventKey(); + entries = await this._readFrom(eventKey, Direction.Backward, amount, txn); + entries.unshift(liveFragmentEntry); + } + } catch (err) { + txn.abort(); + throw err; } - this._fragmentIdComparer.add(liveFragment); - const liveFragmentEntry = FragmentBoundaryEntry.end(liveFragment, this._fragmentIdComparer); - const eventKey = liveFragmentEntry.asEventKey(); - const entries = await this._readFrom(eventKey, Direction.Backward, amount, txn); - entries.unshift(liveFragmentEntry); + await txn.complete(); return entries; } - - // reads distance up and down from eventId - // or just expose eventIdToKey? - readAtEventId(eventId, distance) { - return null; - } } From 32a399afec0f050243bad9c3d9a74d324c4d02c6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 15:31:00 +0200 Subject: [PATCH 09/19] implement storage changes for megolm decryption --- src/matrix/storage/common.js | 1 + src/matrix/storage/idb/Transaction.js | 6 ++++ src/matrix/storage/idb/schema.js | 5 +++ .../idb/stores/GroupSessionDecryptionStore.js | 34 +++++++++++++++++++ .../idb/stores/InboundGroupSessionStore.js | 4 +++ 5 files changed, 50 insertions(+) create mode 100644 src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js diff --git a/src/matrix/storage/common.js b/src/matrix/storage/common.js index 473b8eb6..4a4060b2 100644 --- a/src/matrix/storage/common.js +++ b/src/matrix/storage/common.js @@ -27,6 +27,7 @@ export const STORE_NAMES = Object.freeze([ "olmSessions", "inboundGroupSessions", "outboundGroupSessions", + "groupSessionDecryptions", ]); export const STORE_MAP = Object.freeze(STORE_NAMES.reduce((nameMap, name) => { diff --git a/src/matrix/storage/idb/Transaction.js b/src/matrix/storage/idb/Transaction.js index 8b42b0f7..946b06cc 100644 --- a/src/matrix/storage/idb/Transaction.js +++ b/src/matrix/storage/idb/Transaction.js @@ -29,6 +29,7 @@ import {DeviceIdentityStore} from "./stores/DeviceIdentityStore.js"; import {OlmSessionStore} from "./stores/OlmSessionStore.js"; import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore.js"; import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore.js"; +import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore.js"; export class Transaction { constructor(txn, allowedStoreNames) { @@ -105,6 +106,11 @@ export class Transaction { get outboundGroupSessions() { return this._store("outboundGroupSessions", idbStore => new OutboundGroupSessionStore(idbStore)); } + + get groupSessionDecryptions() { + return this._store("groupSessionDecryptions", idbStore => new OutboundGroupSessionStore(idbStore)); + } + complete() { return txnAsPromise(this._txn); } diff --git a/src/matrix/storage/idb/schema.js b/src/matrix/storage/idb/schema.js index 2060cb39..63458916 100644 --- a/src/matrix/storage/idb/schema.js +++ b/src/matrix/storage/idb/schema.js @@ -13,6 +13,7 @@ export const schema = [ createOlmSessionStore, createInboundGroupSessionsStore, createOutboundGroupSessionsStore, + createGroupSessionDecryptions, ]; // TODO: how to deal with git merge conflicts of this array? @@ -89,3 +90,7 @@ function createOutboundGroupSessionsStore(db) { db.createObjectStore("outboundGroupSessions", {keyPath: "roomId"}); } +//v8 +function createGroupSessionDecryptions(db) { + db.createObjectStore("groupSessionDecryptions", {keyPath: "key"}); +} diff --git a/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js b/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js new file mode 100644 index 00000000..299b1520 --- /dev/null +++ b/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js @@ -0,0 +1,34 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +function encodeKey(roomId, senderKey, sessionId) { + return `${roomId}|${senderKey}|${sessionId}`; +} + +export class GroupSessionDecryptionStore { + constructor(store) { + this._store = store; + } + + get(roomId, sessionId, messageIndex) { + return this._store.get(encodeKey(roomId, sessionId, messageIndex)); + } + + set(decryption) { + decryption.key = encodeKey(decryption.roomId, decryption.sessionId, decryption.messageIndex); + this._store.put(decryption); + } +} diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.js b/src/matrix/storage/idb/stores/InboundGroupSessionStore.js index 3de5a103..d05c67ff 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.js +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.js @@ -29,6 +29,10 @@ export class InboundGroupSessionStore { return key === fetchedKey; } + get(roomId, senderKey, sessionId) { + return this._store.get(encodeKey(roomId, senderKey, sessionId)); + } + set(session) { session.key = encodeKey(session.roomId, session.senderKey, session.sessionId); this._store.put(session); From baad4bd37f7b2a49d230b6ca09d3b92c233ba8c1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 15:31:27 +0200 Subject: [PATCH 10/19] hookup megolm decryption in session --- src/matrix/Session.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index c3429075..d48e0d00 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -49,6 +49,9 @@ export class Session { this._e2eeAccount = null; this._deviceTracker = null; this._olmEncryption = null; + this._megolmEncryption = null; + this._megolmDecryption = null; + if (olm) { this._olmUtil = new olm.Utility(); this._deviceTracker = new DeviceTracker({ @@ -92,9 +95,12 @@ export class Session { storage: this._storage, now: this._clock.now, ownDeviceId: this._sessionInfo.deviceId, - }) - const megolmDecryption = new MegOlmDecryption({pickleKey: PICKLE_KEY, olm: this._olm}); - this._deviceMessageHandler.enableEncryption({olmDecryption, megolmDecryption}); + }); + this._megolmDecryption = new MegOlmDecryption({ + pickleKey: PICKLE_KEY, + olm: this._olm, + }); + this._deviceMessageHandler.enableEncryption({olmDecryption, megolmDecryption: this._megolmDecryption}); } _createRoomEncryption(room, encryptionParams) { @@ -118,6 +124,7 @@ export class Session { deviceTracker: this._deviceTracker, olmEncryption: this._olmEncryption, megolmEncryption: this._megolmEncryption, + megolmDecryption: this._megolmDecryption, encryptionParams }); } From dc0576f2db2fa8638e45f1a0121d39363e5f0fdc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 15:31:45 +0200 Subject: [PATCH 11/19] cleanup --- src/matrix/e2ee/olm/Decryption.js | 2 +- src/matrix/room/timeline/entries/EventEntry.js | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/matrix/e2ee/olm/Decryption.js b/src/matrix/e2ee/olm/Decryption.js index d37b394f..dde9522c 100644 --- a/src/matrix/e2ee/olm/Decryption.js +++ b/src/matrix/e2ee/olm/Decryption.js @@ -115,7 +115,7 @@ export class Decryption { try { payload = JSON.parse(plaintext); } catch (err) { - throw new DecryptionError("NOT_JSON", event, {plaintext, err}); + throw new DecryptionError("PLAINTEXT_NOT_JSON", event, {plaintext, err}); } this._validatePayload(payload, event); return {event: payload, senderKey}; diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 2bf5d941..23296367 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -22,6 +22,7 @@ export class EventEntry extends BaseEntry { super(fragmentIdComparer); this._eventEntry = eventEntry; this._decryptionError = null; + this._isEncrypted = false; } get fragmentId() { @@ -32,10 +33,6 @@ export class EventEntry extends BaseEntry { return this._eventEntry.eventIndex; } - get internalId() { - return `${this.fragmentId}|${this.entryIndex}`; - } - get content() { return this._eventEntry.event.content; } @@ -74,6 +71,7 @@ export class EventEntry extends BaseEntry { replaceWithDecrypted(event) { this._eventEntry.event = event; + this._isEncrypted = true; } setDecryptionError(err) { From 9b771120e48f3e632c582aed50f3153adafda24a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 16:27:14 +0200 Subject: [PATCH 12/19] actually accept megolm decryption dep --- src/matrix/e2ee/RoomEncryption.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 2fd3fc3f..729e1bee 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -21,11 +21,12 @@ import {makeTxnId} from "../common.js"; const ENCRYPTED_TYPE = "m.room.encrypted"; export class RoomEncryption { - constructor({room, deviceTracker, olmEncryption, megolmEncryption, encryptionParams}) { + constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams}) { this._room = room; this._deviceTracker = deviceTracker; this._olmEncryption = olmEncryption; this._megolmEncryption = megolmEncryption; + this._megolmDecryption = megolmDecryption; // content of the m.room.encryption event this._encryptionParams = encryptionParams; From 1af118a44323fb1c6b1f77392fed324ee24d4db9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 16:27:39 +0200 Subject: [PATCH 13/19] don't assume we have a timeline --- src/matrix/room/Room.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 7cfe4307..6339b62c 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -92,7 +92,9 @@ export class Room extends EventEmitter { this._roomEncryption = this._createRoomEncryption(this, encryptionParams); if (this._roomEncryption) { this._sendQueue.enableEncryption(this._roomEncryption); - this._timeline.enableEncryption(this._decryptEntries.bind(this)); + if (this._timeline) { + this._timeline.enableEncryption(this._decryptEntries.bind(this)); + } } } From e06cb1eb5f2292694cfadb896a29becf437df7f9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 16:29:20 +0200 Subject: [PATCH 14/19] fix param order --- src/matrix/room/Room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 6339b62c..4ec6e43c 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -299,7 +299,7 @@ export class Room extends EventEmitter { }); gapResult = await gapWriter.writeFragmentFill(fragmentEntry, response, txn); if (this._roomEncryption) { - gapResult.entries = await this._decryptEntries(gapResult.entries, false, txn); + gapResult.entries = await this._decryptEntries(gapResult.entries, txn, false); } } catch (err) { txn.abort(); From 8e5d5db32b37aea54e4f6696aa7150056d073e2a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 16:40:15 +0200 Subject: [PATCH 15/19] add event prop on entry --- src/matrix/room/timeline/entries/EventEntry.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 23296367..f305cb09 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -25,6 +25,10 @@ export class EventEntry extends BaseEntry { this._isEncrypted = false; } + get event() { + return this._eventEntry.event; + } + get fragmentId() { return this._eventEntry.fragmentId; } From f31efe3e8759c29ac5faac1173e83a0d561f7886 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 16:40:39 +0200 Subject: [PATCH 16/19] encode key with proper names --- src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js b/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js index 299b1520..99ededb9 100644 --- a/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js +++ b/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -function encodeKey(roomId, senderKey, sessionId) { - return `${roomId}|${senderKey}|${sessionId}`; +function encodeKey(roomId, sessionId, messageIndex) { + return `${roomId}|${sessionId}|${messageIndex}`; } export class GroupSessionDecryptionStore { From 7bfcfc9eede1d8e8fc8fe03ae9b021399ccec06b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 16:40:51 +0200 Subject: [PATCH 17/19] correct store name --- src/matrix/storage/idb/Transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/Transaction.js b/src/matrix/storage/idb/Transaction.js index 946b06cc..e0982c54 100644 --- a/src/matrix/storage/idb/Transaction.js +++ b/src/matrix/storage/idb/Transaction.js @@ -108,7 +108,7 @@ export class Transaction { } get groupSessionDecryptions() { - return this._store("groupSessionDecryptions", idbStore => new OutboundGroupSessionStore(idbStore)); + return this._store("groupSessionDecryptions", idbStore => new GroupSessionDecryptionStore(idbStore)); } complete() { From a817a9aaf9b9b240982a411b60a2c20536c64516 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 16:41:03 +0200 Subject: [PATCH 18/19] return decrypted type and content --- src/matrix/room/timeline/entries/EventEntry.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index f305cb09..d6d2f335 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -22,7 +22,7 @@ export class EventEntry extends BaseEntry { super(fragmentIdComparer); this._eventEntry = eventEntry; this._decryptionError = null; - this._isEncrypted = false; + this._decryptedEvent = null; } get event() { @@ -38,7 +38,7 @@ export class EventEntry extends BaseEntry { } get content() { - return this._eventEntry.event.content; + return this._decryptedEvent?.content || this._eventEntry.event.content; } get prevContent() { @@ -46,7 +46,7 @@ export class EventEntry extends BaseEntry { } get eventType() { - return this._eventEntry.event.type; + return this._decryptedEvent?.type || this._eventEntry.event.type; } get stateKey() { @@ -74,8 +74,7 @@ export class EventEntry extends BaseEntry { } replaceWithDecrypted(event) { - this._eventEntry.event = event; - this._isEncrypted = true; + this._decryptedEvent = event; } setDecryptionError(err) { From fbb534fa1671b58f929fe6496a4f16bfed17b874 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Sep 2020 16:46:13 +0200 Subject: [PATCH 19/19] add todo --- src/matrix/e2ee/megolm/Decryption.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/e2ee/megolm/Decryption.js b/src/matrix/e2ee/megolm/Decryption.js index 395b03a0..79a58ffa 100644 --- a/src/matrix/e2ee/megolm/Decryption.js +++ b/src/matrix/e2ee/megolm/Decryption.js @@ -111,6 +111,7 @@ export class Decryption { return; } + // TODO: compare first_known_index to see which session to keep const hasSession = await txn.inboundGroupSessions.has(roomId, senderKey, sessionId); if (!hasSession) { const session = new this._olm.InboundGroupSession();