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 {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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue