create key share operations for invitees when history visibility=invited

This commit is contained in:
Bruno Windels 2022-07-20 15:20:23 +02:00
parent c9bca52e82
commit d79e5f7806
4 changed files with 132 additions and 13 deletions

View file

@ -19,13 +19,23 @@ import {groupEventsBySession} from "./megolm/decryption/utils";
import {mergeMap} from "../../utils/mergeMap"; import {mergeMap} from "../../utils/mergeMap";
import {groupBy} from "../../utils/groupBy"; import {groupBy} from "../../utils/groupBy";
import {makeTxnId} from "../common.js"; import {makeTxnId} from "../common.js";
import {iterateResponseStateEvents} from "../room/common";
const ENCRYPTED_TYPE = "m.room.encrypted"; const ENCRYPTED_TYPE = "m.room.encrypted";
const ROOM_HISTORY_VISIBILITY_TYPE = "m.room.history_visibility";
// how often ensureMessageKeyIsShared can check if it needs to // how often ensureMessageKeyIsShared can check if it needs to
// create a new outbound session // create a new outbound session
// note that encrypt could still create a new session // note that encrypt could still create a new session
const MIN_PRESHARE_INTERVAL = 60 * 1000; // 1min 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 // 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 { export class RoomEncryption {
constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, keyBackup, notifyMissingMegolmSession, clock}) { constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, keyBackup, notifyMissingMegolmSession, clock}) {
@ -45,6 +55,7 @@ export class RoomEncryption {
this._isFlushingRoomKeyShares = false; this._isFlushingRoomKeyShares = false;
this._lastKeyPreShareTime = null; this._lastKeyPreShareTime = null;
this._keySharePromise = null; this._keySharePromise = null;
this._historyVisibility = undefined;
this._disposed = false; this._disposed = false;
} }
@ -77,7 +88,13 @@ export class RoomEncryption {
this._senderDeviceCache = new Map(); // purge the sender device cache 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; let shouldFlush = false;
const memberChangesArray = Array.from(memberChanges.values()); const memberChangesArray = Array.from(memberChanges.values());
// this also clears our session if we leave the room ourselves // 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); this._megolmEncryption.discardOutboundSession(this._room.id, txn);
} }
if (memberChangesArray.some(m => m.hasJoined)) { 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); 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) { async prepareDecryptAll(events, newKeys, source, txn) {
@ -288,8 +330,7 @@ export class RoomEncryption {
await this._processShareRoomKeyOperation(operation, hsApi, log); await this._processShareRoomKeyOperation(operation, hsApi, log);
} }
async _addShareRoomKeyOperationForNewMembers(memberChangesArray, txn, log) { async _addShareRoomKeyOperationForMembers(userIds, txn, log) {
const userIds = memberChangesArray.filter(m => m.hasJoined).map(m => m.userId);
const roomKeyMessage = await this._megolmEncryption.createRoomKeyMessage( const roomKeyMessage = await this._megolmEncryption.createRoomKeyMessage(
this._room.id, txn); this._room.id, txn);
if (roomKeyMessage) { if (roomKeyMessage) {

View file

@ -139,11 +139,11 @@ export class Room extends BaseRoom {
} }
log.set("newEntries", newEntries.length); log.set("newEntries", newEntries.length);
log.set("updatedEntries", updatedEntries.length); log.set("updatedEntries", updatedEntries.length);
let shouldFlushKeyShares = false; let encryptionChanges;
// pass member changes to device tracker // pass member changes to device tracker
if (roomEncryption && this.isTrackingMembers && memberChanges?.size) { if (roomEncryption) {
shouldFlushKeyShares = await roomEncryption.writeMemberChanges(memberChanges, txn, log); encryptionChanges = await roomEncryption.writeSync(roomResponse, memberChanges, txn, log);
log.set("shouldFlushKeyShares", shouldFlushKeyShares); log.set("shouldFlushKeyShares", encryptionChanges.shouldFlush);
} }
const allEntries = newEntries.concat(updatedEntries); const allEntries = newEntries.concat(updatedEntries);
// also apply (decrypted) timeline entries to the summary changes // also apply (decrypted) timeline entries to the summary changes
@ -188,7 +188,7 @@ export class Room extends BaseRoom {
memberChanges, memberChanges,
heroChanges, heroChanges,
powerLevelsEvent, powerLevelsEvent,
shouldFlushKeyShares, encryptionChanges,
}; };
} }
@ -201,11 +201,14 @@ export class Room extends BaseRoom {
const { const {
summaryChanges, newEntries, updatedEntries, newLiveKey, summaryChanges, newEntries, updatedEntries, newLiveKey,
removedPendingEvents, memberChanges, powerLevelsEvent, removedPendingEvents, memberChanges, powerLevelsEvent,
heroChanges, roomEncryption heroChanges, roomEncryption, encryptionChanges
} = changes; } = changes;
log.set("id", this.id); log.set("id", this.id);
this._syncWriter.afterSync(newLiveKey); this._syncWriter.afterSync(newLiveKey);
this._setEncryption(roomEncryption); this._setEncryption(roomEncryption);
if (this._roomEncryption) {
this._roomEncryption.afterSync(encryptionChanges);
}
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()) {
@ -288,8 +291,8 @@ export class Room extends BaseRoom {
} }
} }
needsAfterSyncCompleted({shouldFlushKeyShares}) { needsAfterSyncCompleted({encryptionChanges}) {
return shouldFlushKeyShares; return encryptionChanges?.shouldFlush;
} }
/** /**

View file

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import type {StateEvent} from "../storage/types";
export function getPrevContentFromStateEvent(event) { export function getPrevContentFromStateEvent(event) {
// where to look for prev_content is a bit of a mess, // 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 // 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, Private,
Public 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");
});
}
}
}

View file

@ -137,6 +137,10 @@ export class MemberChange {
return this.member.membership; return this.member.membership;
} }
get wasInvited() {
return this.previousMembership === "invite" && this.membership !== "invite";
}
get hasLeft() { get hasLeft() {
return this.previousMembership === "join" && this.membership !== "join"; return this.previousMembership === "join" && this.membership !== "join";
} }