create key share operations for invitees when history visibility=invited
This commit is contained in:
parent
c9bca52e82
commit
d79e5f7806
4 changed files with 132 additions and 13 deletions
|
@ -19,13 +19,23 @@ import {groupEventsBySession} from "./megolm/decryption/utils";
|
|||
import {mergeMap} from "../../utils/mergeMap";
|
||||
import {groupBy} from "../../utils/groupBy";
|
||||
import {makeTxnId} from "../common.js";
|
||||
import {iterateResponseStateEvents} from "../room/common";
|
||||
|
||||
const ENCRYPTED_TYPE = "m.room.encrypted";
|
||||
const ROOM_HISTORY_VISIBILITY_TYPE = "m.room.history_visibility";
|
||||
// how often ensureMessageKeyIsShared can check if it needs to
|
||||
// create a new outbound session
|
||||
// note that encrypt could still create a new session
|
||||
const MIN_PRESHARE_INTERVAL = 60 * 1000; // 1min
|
||||
|
||||
// Use enum when converting to TS
|
||||
const HistoryVisibility = Object.freeze({
|
||||
Joined: "joined",
|
||||
Invited: "invited",
|
||||
WorldReadable: "world_readable",
|
||||
Shared: "shared",
|
||||
});
|
||||
|
||||
// TODO: this class is a good candidate for splitting up into encryption and decryption, there doesn't seem to be much overlap
|
||||
export class RoomEncryption {
|
||||
constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, keyBackup, notifyMissingMegolmSession, clock}) {
|
||||
|
@ -45,6 +55,7 @@ export class RoomEncryption {
|
|||
this._isFlushingRoomKeyShares = false;
|
||||
this._lastKeyPreShareTime = null;
|
||||
this._keySharePromise = null;
|
||||
this._historyVisibility = undefined;
|
||||
this._disposed = false;
|
||||
}
|
||||
|
||||
|
@ -77,7 +88,13 @@ export class RoomEncryption {
|
|||
this._senderDeviceCache = new Map(); // purge the sender device cache
|
||||
}
|
||||
|
||||
async writeMemberChanges(memberChanges, txn, log) {
|
||||
async writeSync(roomResponse, memberChanges, txn, log) {
|
||||
let historyVisibility = this._historyVisibility;
|
||||
iterateResponseStateEvents(roomResponse, event => {
|
||||
if(event.state_key === "" && event.type === ROOM_HISTORY_VISIBILITY_TYPE) {
|
||||
historyVisibility = event?.content?.history_visibility;
|
||||
}
|
||||
});
|
||||
let shouldFlush = false;
|
||||
const memberChangesArray = Array.from(memberChanges.values());
|
||||
// this also clears our session if we leave the room ourselves
|
||||
|
@ -89,10 +106,35 @@ export class RoomEncryption {
|
|||
this._megolmEncryption.discardOutboundSession(this._room.id, txn);
|
||||
}
|
||||
if (memberChangesArray.some(m => m.hasJoined)) {
|
||||
shouldFlush = await this._addShareRoomKeyOperationForNewMembers(memberChangesArray, txn, log);
|
||||
const userIds = memberChangesArray.filter(m => m.hasJoined).map(m => m.userId);
|
||||
shouldFlush = await this._addShareRoomKeyOperationForMembers(userIds, txn, log)
|
||||
|| shouldFlush;
|
||||
}
|
||||
if (memberChangesArray.some(m => m.wasInvited)) {
|
||||
historyVisibility = await this._loadHistoryVisibilityIfNeeded(historyVisibility, txn);
|
||||
if (historyVisibility === HistoryVisibility.Invited) {
|
||||
const userIds = memberChangesArray.filter(m => m.wasInvited).map(m => m.userId);
|
||||
shouldFlush = await this._addShareRoomKeyOperationForMembers(userIds, txn, log)
|
||||
|| shouldFlush;
|
||||
}
|
||||
}
|
||||
|
||||
await this._deviceTracker.writeMemberChanges(this._room, memberChanges, txn);
|
||||
return shouldFlush;
|
||||
return {shouldFlush, historyVisibility};
|
||||
}
|
||||
|
||||
afterSync({historyVisibility}) {
|
||||
this._historyVisibility = historyVisibility;
|
||||
}
|
||||
|
||||
async _loadHistoryVisibilityIfNeeded(historyVisibility, txn) {
|
||||
if (!historyVisibility) {
|
||||
const visibilityEntry = await txn.roomState.get(this.id, ROOM_HISTORY_VISIBILITY_TYPE, "");
|
||||
if (visibilityEntry) {
|
||||
return event?.content?.history_visibility;
|
||||
}
|
||||
}
|
||||
return historyVisibility;
|
||||
}
|
||||
|
||||
async prepareDecryptAll(events, newKeys, source, txn) {
|
||||
|
@ -288,8 +330,7 @@ export class RoomEncryption {
|
|||
await this._processShareRoomKeyOperation(operation, hsApi, log);
|
||||
}
|
||||
|
||||
async _addShareRoomKeyOperationForNewMembers(memberChangesArray, txn, log) {
|
||||
const userIds = memberChangesArray.filter(m => m.hasJoined).map(m => m.userId);
|
||||
async _addShareRoomKeyOperationForMembers(userIds, txn, log) {
|
||||
const roomKeyMessage = await this._megolmEncryption.createRoomKeyMessage(
|
||||
this._room.id, txn);
|
||||
if (roomKeyMessage) {
|
||||
|
|
|
@ -139,11 +139,11 @@ export class Room extends BaseRoom {
|
|||
}
|
||||
log.set("newEntries", newEntries.length);
|
||||
log.set("updatedEntries", updatedEntries.length);
|
||||
let shouldFlushKeyShares = false;
|
||||
let encryptionChanges;
|
||||
// pass member changes to device tracker
|
||||
if (roomEncryption && this.isTrackingMembers && memberChanges?.size) {
|
||||
shouldFlushKeyShares = await roomEncryption.writeMemberChanges(memberChanges, txn, log);
|
||||
log.set("shouldFlushKeyShares", shouldFlushKeyShares);
|
||||
if (roomEncryption) {
|
||||
encryptionChanges = await roomEncryption.writeSync(roomResponse, memberChanges, txn, log);
|
||||
log.set("shouldFlushKeyShares", encryptionChanges.shouldFlush);
|
||||
}
|
||||
const allEntries = newEntries.concat(updatedEntries);
|
||||
// also apply (decrypted) timeline entries to the summary changes
|
||||
|
@ -188,7 +188,7 @@ export class Room extends BaseRoom {
|
|||
memberChanges,
|
||||
heroChanges,
|
||||
powerLevelsEvent,
|
||||
shouldFlushKeyShares,
|
||||
encryptionChanges,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -201,11 +201,14 @@ export class Room extends BaseRoom {
|
|||
const {
|
||||
summaryChanges, newEntries, updatedEntries, newLiveKey,
|
||||
removedPendingEvents, memberChanges, powerLevelsEvent,
|
||||
heroChanges, roomEncryption
|
||||
heroChanges, roomEncryption, encryptionChanges
|
||||
} = changes;
|
||||
log.set("id", this.id);
|
||||
this._syncWriter.afterSync(newLiveKey);
|
||||
this._setEncryption(roomEncryption);
|
||||
if (this._roomEncryption) {
|
||||
this._roomEncryption.afterSync(encryptionChanges);
|
||||
}
|
||||
if (memberChanges.size) {
|
||||
if (this._changedMembersDuringSync) {
|
||||
for (const [userId, memberChange] of memberChanges.entries()) {
|
||||
|
@ -288,8 +291,8 @@ export class Room extends BaseRoom {
|
|||
}
|
||||
}
|
||||
|
||||
needsAfterSyncCompleted({shouldFlushKeyShares}) {
|
||||
return shouldFlushKeyShares;
|
||||
needsAfterSyncCompleted({encryptionChanges}) {
|
||||
return encryptionChanges?.shouldFlush;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type {StateEvent} from "../storage/types";
|
||||
|
||||
export function getPrevContentFromStateEvent(event) {
|
||||
// where to look for prev_content is a bit of a mess,
|
||||
// see https://matrix.to/#/!NasysSDfxKxZBzJJoE:matrix.org/$DvrAbZJiILkOmOIuRsNoHmh2v7UO5CWp_rYhlGk34fQ?via=matrix.org&via=pixie.town&via=amorgan.xyz
|
||||
|
@ -40,3 +42,72 @@ export enum RoomType {
|
|||
Private,
|
||||
Public
|
||||
}
|
||||
|
||||
type RoomResponse = {
|
||||
state?: {
|
||||
events?: Array<StateEvent>
|
||||
},
|
||||
timeline?: {
|
||||
events?: Array<StateEvent>
|
||||
}
|
||||
}
|
||||
|
||||
/** iterates over any state events in a sync room response, in the order that they should be applied (from older to younger events) */
|
||||
export function iterateResponseStateEvents(roomResponse: RoomResponse, callback: (StateEvent) => void) {
|
||||
// first iterate over state events, they precede the timeline
|
||||
const stateEvents = roomResponse.state?.events;
|
||||
if (stateEvents) {
|
||||
for (let i = 0; i < stateEvents.length; i++) {
|
||||
callback(stateEvents[i]);
|
||||
}
|
||||
}
|
||||
// now see if there are any state events within the timeline
|
||||
let timelineEvents = roomResponse.timeline?.events;
|
||||
if (timelineEvents) {
|
||||
for (let i = 0; i < timelineEvents.length; i++) {
|
||||
const event = timelineEvents[i];
|
||||
if (typeof event.state_key === "string") {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function tests() {
|
||||
return {
|
||||
"test iterateResponseStateEvents with both state and timeline sections": assert => {
|
||||
const roomResponse = {
|
||||
state: {
|
||||
events: [
|
||||
{type: "m.room.member", state_key: "1"},
|
||||
{type: "m.room.member", state_key: "2", content: {a: 1}},
|
||||
]
|
||||
},
|
||||
timeline: {
|
||||
events: [
|
||||
{type: "m.room.message"},
|
||||
{type: "m.room.member", state_key: "3"},
|
||||
{type: "m.room.message"},
|
||||
{type: "m.room.member", state_key: "2", content: {a: 2}},
|
||||
]
|
||||
}
|
||||
} as unknown as RoomResponse;
|
||||
const expectedStateKeys = ["1", "2", "3", "2"];
|
||||
const expectedAForMember2 = [1, 2];
|
||||
iterateResponseStateEvents(roomResponse, event => {
|
||||
assert.strictEqual(event.type, "m.room.member");
|
||||
assert.strictEqual(expectedStateKeys.shift(), event.state_key);
|
||||
if (event.state_key === "2") {
|
||||
assert.strictEqual(expectedAForMember2.shift(), event.content.a);
|
||||
}
|
||||
});
|
||||
assert.strictEqual(expectedStateKeys.length, 0);
|
||||
assert.strictEqual(expectedAForMember2.length, 0);
|
||||
},
|
||||
"test iterateResponseStateEvents with empty response": assert => {
|
||||
iterateResponseStateEvents({}, () => {
|
||||
assert.fail("no events expected");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,6 +137,10 @@ export class MemberChange {
|
|||
return this.member.membership;
|
||||
}
|
||||
|
||||
get wasInvited() {
|
||||
return this.previousMembership === "invite" && this.membership !== "invite";
|
||||
}
|
||||
|
||||
get hasLeft() {
|
||||
return this.previousMembership === "join" && this.membership !== "join";
|
||||
}
|
||||
|
|
Reference in a new issue