add RoomEncryption to room

This commit is contained in:
Bruno Windels 2020-09-03 15:36:17 +02:00
parent 2a40c89a24
commit 5cafef96f5
6 changed files with 124 additions and 13 deletions

View file

@ -23,6 +23,7 @@ import {DeviceMessageHandler} from "./DeviceMessageHandler.js";
import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption.js"; import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption.js";
import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js"; import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js";
import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption.js"; import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption.js";
import {RoomEncryption} from "./e2ee/RoomEncryption.js";
import {DeviceTracker} from "./e2ee/DeviceTracker.js"; import {DeviceTracker} from "./e2ee/DeviceTracker.js";
import {LockMap} from "../utils/LockMap.js"; import {LockMap} from "../utils/LockMap.js";
@ -56,6 +57,7 @@ export class Session {
ownDeviceId: sessionInfo.deviceId, ownDeviceId: sessionInfo.deviceId,
}); });
} }
this._createRoomEncryption = this._createRoomEncryption.bind(this);
} }
// called once this._e2eeAccount is assigned // called once this._e2eeAccount is assigned
@ -85,6 +87,26 @@ export class Session {
this._deviceMessageHandler.enableEncryption({olmDecryption, megolmDecryption}); 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 // called after load
async beforeFirstSync(isNewLogin) { async beforeFirstSync(isNewLogin) {
if (this._olm) { if (this._olm) {
@ -202,6 +224,7 @@ export class Session {
sendScheduler: this._sendScheduler, sendScheduler: this._sendScheduler,
pendingEvents, pendingEvents,
user: this._user, user: this._user,
createRoomEncryption: this._createRoomEncryption
}); });
this._rooms.add(roomId, room); this._rooms.add(roomId, room);
return room; return room;
@ -222,12 +245,6 @@ export class Session {
changes.syncInfo = syncInfo; changes.syncInfo = syncInfo;
} }
if (this._deviceTracker) { 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; const deviceLists = syncResponse.device_lists;
if (deviceLists) { if (deviceLists) {
await this._deviceTracker.writeDeviceChanges(deviceLists, txn); await this._deviceTracker.writeDeviceChanges(deviceLists, txn);

22
src/matrix/common.js Normal file
View file

@ -0,0 +1,22 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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;
}

View file

@ -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();
}
}

View file

@ -172,6 +172,10 @@ export class HomeServerApi {
return this._post("/keys/claim", null, payload, options); 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() { get mediaRepository() {
return this._mediaRepository; return this._mediaRepository;
} }

View file

@ -27,7 +27,7 @@ import {MemberList} from "./members/MemberList.js";
import {Heroes} from "./members/Heroes.js"; import {Heroes} from "./members/Heroes.js";
export class Room extends EventEmitter { export class Room extends EventEmitter {
constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user}) { constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user, createRoomEncryption}) {
super(); super();
this._roomId = roomId; this._roomId = roomId;
this._storage = storage; this._storage = storage;
@ -41,6 +41,8 @@ export class Room extends EventEmitter {
this._user = user; this._user = user;
this._changedMembersDuringSync = null; this._changedMembersDuringSync = null;
this._memberList = null; this._memberList = null;
this._createRoomEncryption = createRoomEncryption;
this._roomEncryption = null;
} }
/** @package */ /** @package */
@ -62,6 +64,10 @@ export class Room extends EventEmitter {
} }
heroChanges = await this._heroes.calculateChanges(summaryChanges.heroes, memberChanges, txn); 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; let removedPendingEvents;
if (roomResponse.timeline && roomResponse.timeline.events) { if (roomResponse.timeline && roomResponse.timeline.events) {
removedPendingEvents = this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn); removedPendingEvents = this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn);
@ -79,6 +85,10 @@ export class Room extends EventEmitter {
/** @package */ /** @package */
afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, memberChanges, heroChanges}) { afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, memberChanges, heroChanges}) {
this._syncWriter.afterSync(newLiveKey); 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 (memberChanges.size) {
if (this._changedMembersDuringSync) { if (this._changedMembersDuringSync) {
for (const [userId, memberChange] of memberChanges.entries()) { for (const [userId, memberChange] of memberChanges.entries()) {
@ -125,6 +135,9 @@ export class Room extends EventEmitter {
async load(summary, txn) { async load(summary, txn) {
try { try {
this._summary.load(summary); this._summary.load(summary);
if (this._summary.encryption) {
this._roomEncryption = this._createRoomEncryption(this, this._summary.encryption);
}
// need to load members for name? // need to load members for name?
if (this._summary.needsHeroes) { if (this._summary.needsHeroes) {
this._heroes = new Heroes(this._roomId); this._heroes = new Heroes(this._roomId);

View file

@ -17,12 +17,7 @@ limitations under the License.
import {SortedArray} from "../../../observable/list/SortedArray.js"; import {SortedArray} from "../../../observable/list/SortedArray.js";
import {ConnectionError} from "../../error.js"; import {ConnectionError} from "../../error.js";
import {PendingEvent} from "./PendingEvent.js"; import {PendingEvent} from "./PendingEvent.js";
import {makeTxnId} from "../../common.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;
}
export class SendQueue { export class SendQueue {
constructor({roomId, storage, sendScheduler, pendingEvents}) { constructor({roomId, storage, sendScheduler, pendingEvents}) {