diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 652ac18e..c1a26ded 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -23,6 +23,7 @@ import {DeviceMessageHandler} from "./DeviceMessageHandler.js"; import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption.js"; import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js"; import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption.js"; +import {RoomEncryption} from "./e2ee/RoomEncryption.js"; import {DeviceTracker} from "./e2ee/DeviceTracker.js"; import {LockMap} from "../utils/LockMap.js"; @@ -56,6 +57,7 @@ export class Session { ownDeviceId: sessionInfo.deviceId, }); } + this._createRoomEncryption = this._createRoomEncryption.bind(this); } // called once this._e2eeAccount is assigned @@ -85,6 +87,26 @@ export class Session { this._deviceMessageHandler.enableEncryption({olmDecryption, megolmDecryption}); } + _createRoomEncryption(room, encryptionEventContent) { + // TODO: this will actually happen when users start using the e2ee version for the first time + + // this should never happen because either a session was already synced once + // and thus an e2ee account was created as well and _setupEncryption is called from load + // OR + // this is a new session and loading it will load zero rooms, thus not calling this method. + // in this case _setupEncryption is called from beforeFirstSync, right after load, + // so any incoming synced rooms won't be there yet + if (!this._olmEncryption) { + throw new Error("creating room encryption before encryption got globally enabled"); + } + return new RoomEncryption({ + room, + deviceTracker: this._deviceTracker, + olmEncryption: this._olmEncryption, + encryptionEventContent + }); + } + // called after load async beforeFirstSync(isNewLogin) { if (this._olm) { @@ -202,6 +224,7 @@ export class Session { sendScheduler: this._sendScheduler, pendingEvents, user: this._user, + createRoomEncryption: this._createRoomEncryption }); this._rooms.add(roomId, room); return room; @@ -222,12 +245,6 @@ export class Session { changes.syncInfo = syncInfo; } if (this._deviceTracker) { - for (const {room, changes} of roomChanges) { - // TODO: move this so the room passes this to it's "encryption" object in its own writeSync method? - if (room.isTrackingMembers && changes.memberChanges?.size) { - await this._deviceTracker.writeMemberChanges(room, changes.memberChanges, txn); - } - } const deviceLists = syncResponse.device_lists; if (deviceLists) { await this._deviceTracker.writeDeviceChanges(deviceLists, txn); diff --git a/src/matrix/common.js b/src/matrix/common.js new file mode 100644 index 00000000..3c893234 --- /dev/null +++ b/src/matrix/common.js @@ -0,0 +1,22 @@ +/* +Copyright 2020 Bruno Windels +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. +*/ + +export function makeTxnId() { + const n = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + const str = n.toString(16); + return "t" + "0".repeat(14 - str.length) + str; +} \ No newline at end of file diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js new file mode 100644 index 00000000..32bd061c --- /dev/null +++ b/src/matrix/e2ee/RoomEncryption.js @@ -0,0 +1,60 @@ +/* +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. +*/ + +import {groupBy} from "../../utils/groupBy.js"; +import {makeTxnId} from "../common.js"; + + +export class RoomEncryption { + constructor({room, deviceTracker, olmEncryption, encryptionEventContent}) { + this._room = room; + this._deviceTracker = deviceTracker; + this._olmEncryption = olmEncryption; + // content of the m.room.encryption event + this._encryptionEventContent = encryptionEventContent; + } + + async writeMemberChanges(memberChanges, txn) { + return await this._deviceTracker.writeMemberChanges(this._room, memberChanges, txn); + } + + async encrypt(type, content, hsApi) { + await this._deviceTracker.trackRoom(this._room); + const devices = await this._deviceTracker.deviceIdentitiesForTrackedRoom(this._room.id, hsApi); + const messages = await this._olmEncryption.encrypt("m.foo", {body: "hello at " + new Date()}, devices, hsApi); + await this._sendMessagesToDevices("m.room.encrypted", messages, hsApi); + return {type, content}; + // return { + // type: "m.room.encrypted", + // content: encryptedContent, + // } + } + + async _sendMessagesToDevices(type, messages, hsApi) { + const messagesByUser = groupBy(messages, message => message.device.userId); + const payload = { + messages: Array.from(messagesByUser.entries()).reduce((userMap, [userId, messages]) => { + userMap[userId] = messages.reduce((deviceMap, message) => { + deviceMap[message.device.deviceId] = message.content; + return deviceMap; + }, {}); + return userMap; + }, {}) + }; + const txnId = makeTxnId(); + await hsApi.sendToDevice(type, payload, txnId).response(); + } +} diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 9ea7dc26..b1a89634 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -172,6 +172,10 @@ export class HomeServerApi { return this._post("/keys/claim", null, payload, options); } + sendToDevice(type, payload, txnId, options = null) { + return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, null, payload, options); + } + get mediaRepository() { return this._mediaRepository; } diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 98cde3a5..dbf1e5e3 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -27,7 +27,7 @@ import {MemberList} from "./members/MemberList.js"; import {Heroes} from "./members/Heroes.js"; export class Room extends EventEmitter { - constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user}) { + constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user, createRoomEncryption}) { super(); this._roomId = roomId; this._storage = storage; @@ -41,6 +41,8 @@ export class Room extends EventEmitter { this._user = user; this._changedMembersDuringSync = null; this._memberList = null; + this._createRoomEncryption = createRoomEncryption; + this._roomEncryption = null; } /** @package */ @@ -62,6 +64,10 @@ export class Room extends EventEmitter { } heroChanges = await this._heroes.calculateChanges(summaryChanges.heroes, memberChanges, txn); } + // pass member changes to device tracker + if (this._roomEncryption && this.isTrackingMembers && memberChanges?.size) { + await this._roomEncryption.writeMemberChanges(memberChanges, txn); + } let removedPendingEvents; if (roomResponse.timeline && roomResponse.timeline.events) { removedPendingEvents = this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn); @@ -79,6 +85,10 @@ 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 (memberChanges.size) { if (this._changedMembersDuringSync) { for (const [userId, memberChange] of memberChanges.entries()) { @@ -125,6 +135,9 @@ export class Room extends EventEmitter { async load(summary, txn) { try { this._summary.load(summary); + if (this._summary.encryption) { + this._roomEncryption = this._createRoomEncryption(this, this._summary.encryption); + } // need to load members for name? if (this._summary.needsHeroes) { this._heroes = new Heroes(this._roomId); diff --git a/src/matrix/room/sending/SendQueue.js b/src/matrix/room/sending/SendQueue.js index ba215e04..9a094798 100644 --- a/src/matrix/room/sending/SendQueue.js +++ b/src/matrix/room/sending/SendQueue.js @@ -17,12 +17,7 @@ limitations under the License. import {SortedArray} from "../../../observable/list/SortedArray.js"; import {ConnectionError} from "../../error.js"; import {PendingEvent} from "./PendingEvent.js"; - -function makeTxnId() { - const n = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); - const str = n.toString(16); - return "t" + "0".repeat(14 - str.length) + str; -} +import {makeTxnId} from "../../common.js"; export class SendQueue { constructor({roomId, storage, sendScheduler, pendingEvents}) {