diff --git a/package.json b/package.json index 88668ba57..0a8d7438f 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "eslint-plugin-unicorn": "^45.0.0", "exorcist": "^2.0.0", "fake-indexeddb": "^4.0.0", + "fetch-mock-jest": "^1.5.1", "jest": "^29.0.0", "jest-environment-jsdom": "^29.0.0", "jest-localstorage-mock": "^2.4.6", diff --git a/spec/integ/crypto.spec.ts b/spec/integ/crypto.spec.ts index 910987b45..eb39f4fcf 100644 --- a/spec/integ/crypto.spec.ts +++ b/spec/integ/crypto.spec.ts @@ -633,6 +633,35 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm (%s)", (backend: string, expect(event.getContent().body).toEqual("42"); }); + oldBackendOnly("prepareToEncrypt", async () => { + aliceTestClient.expectKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); + await aliceTestClient.start(); + aliceTestClient.client.setGlobalErrorOnUnknownDevices(false); + + // tell alice she is sharing a room with bob + aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse(["@bob:xyz"])); + await aliceTestClient.flushSync(); + + // we expect alice first to query bob's keys... + aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz")); + aliceTestClient.httpBackend.flush("/keys/query", 1); + + // ... and then claim one of his OTKs + aliceTestClient.httpBackend.when("POST", "/keys/claim").respond(200, getTestKeysClaimResponse("@bob:xyz")); + aliceTestClient.httpBackend.flush("/keys/claim", 1); + + // fire off the prepare request + const room = aliceTestClient.client.getRoom(ROOM_ID); + expect(room).toBeTruthy(); + const p = aliceTestClient.client.prepareToEncrypt(room!); + + // we expect to get a room key message + await expectSendRoomKey(aliceTestClient.httpBackend, "@bob:xyz", testOlmAccount); + + // the prepare request should complete successfully. + await p; + }); + oldBackendOnly("Alice sends a megolm message", async () => { aliceTestClient.expectKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await aliceTestClient.start(); diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index e78d2eab1..45a584c06 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -1415,7 +1415,7 @@ describe("MatrixClient", function () { expect(getRoomId).toEqual(roomId); return mockRoom; }; - client.crypto = { + client.crypto = client["cryptoBackend"] = { // mock crypto encryptEvent: () => new Promise(() => {}), stop: jest.fn(), @@ -1437,8 +1437,9 @@ describe("MatrixClient", function () { it("should cancel an event which is encrypting", async () => { // @ts-ignore protected method access - client.encryptAndSendEvent(null, event); + client.encryptAndSendEvent(mockRoom, event); await testUtils.emitPromise(event, "Event.status"); + expect(event.status).toBe(EventStatus.ENCRYPTING); client.cancelPendingEvent(event); assertCancelled(); }); diff --git a/spec/unit/rust-crypto/KeyClaimManager.spec.ts b/spec/unit/rust-crypto/KeyClaimManager.spec.ts new file mode 100644 index 000000000..e448feabe --- /dev/null +++ b/spec/unit/rust-crypto/KeyClaimManager.spec.ts @@ -0,0 +1,158 @@ +/* +Copyright 2023 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 * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; +import fetchMock from "fetch-mock-jest"; +import { Mocked } from "jest-mock"; +import { KeysClaimRequest, UserId } from "@matrix-org/matrix-sdk-crypto-js"; + +import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor"; +import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager"; +import { TypedEventEmitter } from "../../../src/models/typed-event-emitter"; +import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src"; + +afterEach(() => { + fetchMock.mockReset(); +}); + +describe("KeyClaimManager", () => { + /* for these tests, we connect a KeyClaimManager to a mock OlmMachine, and a real OutgoingRequestProcessor + * (which is connected to a mock fetch implementation) + */ + + /** the KeyClaimManager implementation under test */ + let keyClaimManager: KeyClaimManager; + + /** a mocked-up OlmMachine which the OutgoingRequestProcessor and KeyClaimManager are connected to */ + let olmMachine: Mocked; + + beforeEach(async () => { + const dummyEventEmitter = new TypedEventEmitter(); + const httpApi = new MatrixHttpApi(dummyEventEmitter, { + baseUrl: "https://example.com", + prefix: "/_matrix", + onlyData: true, + }); + + olmMachine = { + getMissingSessions: jest.fn(), + markRequestAsSent: jest.fn(), + } as unknown as Mocked; + + const outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, httpApi); + + keyClaimManager = new KeyClaimManager(olmMachine, outgoingRequestProcessor); + }); + + /** + * Returns a promise which resolve once olmMachine.markRequestAsSent is called. + * + * The call itself will block initially. + * + * The promise returned by this function yields a callback function, which should be called to unblock the + * markRequestAsSent call. + */ + function awaitCallToMarkRequestAsSent(): Promise<() => void> { + return new Promise<() => void>((resolveCalledPromise, _reject) => { + olmMachine.markRequestAsSent.mockImplementationOnce(async () => { + // the mock implementation returns a promise... + const completePromise = new Promise((resolveCompletePromise, _reject) => { + // ... and we now resolve the original promise with the resolver for that second promise. + resolveCalledPromise(resolveCompletePromise); + }); + return completePromise; + }); + }); + } + + it("should claim missing keys", async () => { + const u1 = new UserId("@alice:example.com"); + const u2 = new UserId("@bob:example.com"); + + // stub out olmMachine.getMissingSessions(), with a result indicating that it needs a keyclaim + const keysClaimRequest = new KeysClaimRequest("1234", '{ "k1": "v1" }'); + olmMachine.getMissingSessions.mockResolvedValueOnce(keysClaimRequest); + + // have the claim request return a 200 + fetchMock.postOnce("https://example.com/_matrix/client/v3/keys/claim", '{ "k": "v" }'); + + // also stub out olmMachine.markRequestAsSent + olmMachine.markRequestAsSent.mockResolvedValueOnce(undefined); + + // fire off the request + await keyClaimManager.ensureSessionsForUsers([u1, u2]); + + // check that all the calls were made + expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u1, u2]); + expect(fetchMock).toHaveFetched("https://example.com/_matrix/client/v3/keys/claim", { + method: "POST", + body: { k1: "v1" }, + }); + expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", keysClaimRequest.type, '{ "k": "v" }'); + }); + + it("should wait for previous claims to complete before making another", async () => { + const u1 = new UserId("@alice:example.com"); + const u2 = new UserId("@bob:example.com"); + + // stub out olmMachine.getMissingSessions(), with a result indicating that it needs a keyclaim + const keysClaimRequest = new KeysClaimRequest("1234", '{ "k1": "v1" }'); + olmMachine.getMissingSessions.mockResolvedValue(keysClaimRequest); + + // have the claim request return a 200 + fetchMock.post("https://example.com/_matrix/client/v3/keys/claim", '{ "k": "v" }'); + + // stub out olmMachine.markRequestAsSent, and have it block + let markRequestAsSentPromise = awaitCallToMarkRequestAsSent(); + + // fire off two requests, and keep track of whether their promises resolve + let req1Resolved = false; + keyClaimManager.ensureSessionsForUsers([u1]).then(() => { + req1Resolved = true; + }); + let req2Resolved = false; + const req2 = keyClaimManager.ensureSessionsForUsers([u2]).then(() => { + req2Resolved = true; + }); + + // now: wait for the (first) call to OlmMachine.markRequestAsSent + let resolveMarkRequestAsSentCallback = await markRequestAsSentPromise; + + // at this point, there should have been a single call to getMissingSessions, and a single fetch; and neither + // call to ensureSessionsAsUsers should have completed + expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u1]); + expect(olmMachine.getMissingSessions).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(req1Resolved).toBe(false); + expect(req2Resolved).toBe(false); + + // await the next call to markRequestAsSent, and release the first one + markRequestAsSentPromise = awaitCallToMarkRequestAsSent(); + resolveMarkRequestAsSentCallback(); + resolveMarkRequestAsSentCallback = await markRequestAsSentPromise; + + // the first request should now have completed, and we should have more calls and fetches + expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u2]); + expect(olmMachine.getMissingSessions).toHaveBeenCalledTimes(2); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(req1Resolved).toBe(true); + expect(req2Resolved).toBe(false); + + // finally, release the second call to markRequestAsSent and check that the second request completes + resolveMarkRequestAsSentCallback(); + await req2; + }); +}); diff --git a/src/client.ts b/src/client.ts index a48d58e5d..88e17e9ac 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2185,7 +2185,11 @@ export class MatrixClient extends TypedEventEmitter; + /** * Decrypt a received event * @@ -117,6 +138,20 @@ export interface SyncCryptoCallbacks { */ preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise; + /** + * Called by the /sync loop whenever an m.room.encryption event is received. + * + * This is called before RoomStateEvents are emitted for any of the events in the /sync + * response (even if the other events technically happened first). This works around a problem + * if the client uses a RoomStateEvent (typically a membership event) as a trigger to send a message + * in a new room (or one where encryption has been newly enabled): that would otherwise leave the + * crypto layer confused because it expects crypto to be set up, but it has not yet been. + * + * @param room - in which the event was received + * @param event - encryption event to be processed + */ + onCryptoEvent(room: Room, event: MatrixEvent): Promise; + /** * Called by the /sync loop after each /sync response is processed. * diff --git a/src/crypto/index.ts b/src/crypto/index.ts index b0a5783b3..58ac18d06 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -2808,11 +2808,7 @@ export class Crypto extends TypedEventEmitter { - if (!room) { - throw new Error("Cannot send encrypted messages in unknown rooms"); - } - + public async encryptEvent(event: MatrixEvent, room: Room): Promise { const roomId = event.getRoomId()!; const alg = this.roomEncryptors.get(roomId); diff --git a/src/rust-crypto/KeyClaimManager.ts b/src/rust-crypto/KeyClaimManager.ts new file mode 100644 index 000000000..0479bfa43 --- /dev/null +++ b/src/rust-crypto/KeyClaimManager.ts @@ -0,0 +1,72 @@ +/* +Copyright 2023 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 { OlmMachine, UserId } from "@matrix-org/matrix-sdk-crypto-js"; + +import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; + +/** + * KeyClaimManager: linearises calls to OlmMachine.getMissingSessions to avoid races + * + * We have one of these per `RustCrypto` (and hence per `MatrixClient`). + */ +export class KeyClaimManager { + private currentClaimPromise: Promise; + private stopped = false; + + public constructor( + private readonly olmMachine: OlmMachine, + private readonly outgoingRequestProcessor: OutgoingRequestProcessor, + ) { + this.currentClaimPromise = Promise.resolve(); + } + + /** + * Tell the KeyClaimManager to immediately stop processing requests. + * + * Any further calls, and any still in the queue, will fail with an error. + */ + public stop(): void { + this.stopped = true; + } + + /** + * Given a list of users, attempt to ensure that we have Olm Sessions active with each of their devices + * + * If we don't have an active olm session, we will claim a one-time key and start one. + * + * @param userList - list of userIDs to claim + */ + public ensureSessionsForUsers(userList: Array): Promise { + // The Rust-SDK requires that we only have one getMissingSessions process in flight at once. This little dance + // ensures that, by only having one call to ensureSessionsForUsersInner active at once (and making them + // queue up in order). + const prom = this.currentClaimPromise.finally(() => this.ensureSessionsForUsersInner(userList)); + this.currentClaimPromise = prom; + return prom; + } + + private async ensureSessionsForUsersInner(userList: Array): Promise { + // bail out quickly if we've been stopped. + if (this.stopped) { + throw new Error(`Cannot ensure Olm sessions: shutting down`); + } + const claimRequest = await this.olmMachine.getMissingSessions(userList); + if (claimRequest) { + await this.outgoingRequestProcessor.makeOutgoingRequest(claimRequest); + } + } +} diff --git a/src/rust-crypto/RoomEncryptor.ts b/src/rust-crypto/RoomEncryptor.ts new file mode 100644 index 000000000..acd0e9ff0 --- /dev/null +++ b/src/rust-crypto/RoomEncryptor.ts @@ -0,0 +1,130 @@ +/* +Copyright 2023 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 { EncryptionSettings, OlmMachine, RoomId, UserId } from "@matrix-org/matrix-sdk-crypto-js"; + +import { EventType } from "../@types/event"; +import { IContent, MatrixEvent } from "../models/event"; +import { Room } from "../models/room"; +import { logger, PrefixedLogger } from "../logger"; +import { KeyClaimManager } from "./KeyClaimManager"; +import { RoomMember } from "../models/room-member"; + +/** + * RoomEncryptor: responsible for encrypting messages to a given room + */ +export class RoomEncryptor { + private readonly prefixedLogger: PrefixedLogger; + + /** + * @param olmMachine - The rust-sdk's OlmMachine + * @param keyClaimManager - Our KeyClaimManager, which manages the queue of one-time-key claim requests + * @param room - The room we want to encrypt for + * @param encryptionSettings - body of the m.room.encryption event currently in force in this room + */ + public constructor( + private readonly olmMachine: OlmMachine, + private readonly keyClaimManager: KeyClaimManager, + private readonly room: Room, + private encryptionSettings: IContent, + ) { + this.prefixedLogger = logger.withPrefix(`[${room.roomId} encryption]`); + } + + /** + * Handle a new `m.room.encryption` event in this room + * + * @param config - The content of the encryption event + */ + public onCryptoEvent(config: IContent): void { + if (JSON.stringify(this.encryptionSettings) != JSON.stringify(config)) { + this.prefixedLogger.error(`Ignoring m.room.encryption event which requests a change of config`); + } + } + + /** + * Handle a new `m.room.member` event in this room + * + * @param member - new membership state + */ + public onRoomMembership(member: RoomMember): void { + this.prefixedLogger.debug(`${member.membership} event for ${member.userId}`); + + if ( + member.membership == "join" || + (member.membership == "invite" && this.room.shouldEncryptForInvitedMembers()) + ) { + // make sure we are tracking the deviceList for this user + this.prefixedLogger.debug(`starting to track devices for: ${member.userId}`); + this.olmMachine.updateTrackedUsers([new UserId(member.userId)]); + } + + // TODO: handle leaves (including our own) + } + + /** + * Prepare to encrypt events in this room. + * + * This ensures that we have a megolm session ready to use and that we have shared its key with all the devices + * in the room. + */ + public async ensureEncryptionSession(): Promise { + if (this.encryptionSettings.algorithm !== "m.megolm.v1.aes-sha2") { + throw new Error( + `Cannot encrypt in ${this.room.roomId} for unsupported algorithm '${this.encryptionSettings.algorithm}'`, + ); + } + + const members = await this.room.getEncryptionTargetMembers(); + this.prefixedLogger.debug( + `Encrypting for users (shouldEncryptForInvitedMembers: ${this.room.shouldEncryptForInvitedMembers()}):`, + members.map((u) => `${u.userId} (${u.membership})`), + ); + + const userList = members.map((u) => new UserId(u.userId)); + await this.keyClaimManager.ensureSessionsForUsers(userList); + + const rustEncryptionSettings = new EncryptionSettings(); + /* FIXME historyVisibility, rotation, etc */ + + await this.olmMachine.shareRoomKey(new RoomId(this.room.roomId), userList, rustEncryptionSettings); + } + + /** + * Encrypt an event for this room + * + * This will ensure that we have a megolm session for this room, share it with the devices in the room, and + * then encrypt the event using the session. + * + * @param event - Event to be encrypted. + */ + public async encryptEvent(event: MatrixEvent): Promise { + await this.ensureEncryptionSession(); + + const encryptedContent = await this.olmMachine.encryptRoomEvent( + new RoomId(this.room.roomId), + event.getType(), + JSON.stringify(event.getContent()), + ); + + event.makeEncrypted( + EventType.RoomMessageEncrypted, + JSON.parse(encryptedContent), + this.olmMachine.identityKeys.curve25519.toBase64(), + this.olmMachine.identityKeys.ed25519.toBase64(), + ); + } +} diff --git a/src/rust-crypto/index.ts b/src/rust-crypto/index.ts index 4c826078f..7faeff158 100644 --- a/src/rust-crypto/index.ts +++ b/src/rust-crypto/index.ts @@ -18,7 +18,6 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; import { RustCrypto } from "./rust-crypto"; import { logger } from "../logger"; -import { CryptoBackend } from "../common-crypto/CryptoBackend"; import { RUST_SDK_STORE_PREFIX } from "./constants"; import { IHttpOpts, MatrixHttpApi } from "../http-api"; @@ -26,7 +25,7 @@ export async function initRustCrypto( http: MatrixHttpApi, userId: string, deviceId: string, -): Promise { +): Promise { // initialise the rust matrix-sdk-crypto-js, if it hasn't already been done await RustSdkCryptoJs.initAsync(); diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 287ab71f4..2377b8a21 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -20,11 +20,15 @@ import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypt import type { IToDeviceEvent } from "../sync-accumulator"; import type { IEncryptedEventInfo } from "../crypto/api"; import { MatrixEvent } from "../models/event"; +import { Room } from "../models/room"; +import { RoomMember } from "../models/room-member"; import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; import { logger } from "../logger"; import { IHttpOpts, MatrixHttpApi } from "../http-api"; import { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning"; +import { RoomEncryptor } from "./RoomEncryptor"; import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; +import { KeyClaimManager } from "./KeyClaimManager"; /** * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. @@ -39,6 +43,10 @@ export class RustCrypto implements CryptoBackend { /** whether {@link outgoingRequestLoop} is currently running */ private outgoingRequestLoopRunning = false; + /** mapping of roomId → encryptor class */ + private roomEncryptors: Record = {}; + + private keyClaimManager: KeyClaimManager; private outgoingRequestProcessor: OutgoingRequestProcessor; public constructor( @@ -48,8 +56,15 @@ export class RustCrypto implements CryptoBackend { _deviceId: string, ) { this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http); + this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor); } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // CryptoBackend implementation + // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + public stop(): void { // stop() may be called multiple times, but attempting to close() the OlmMachine twice // will cause an error. @@ -58,12 +73,33 @@ export class RustCrypto implements CryptoBackend { } this.stopped = true; + this.keyClaimManager.stop(); + // make sure we close() the OlmMachine; doing so means that all the Rust objects will be // cleaned up; in particular, the indexeddb connections will be closed, which means they // can then be deleted. this.olmMachine.close(); } + public prepareToEncrypt(room: Room): void { + const encryptor = this.roomEncryptors[room.roomId]; + + if (encryptor) { + encryptor.ensureEncryptionSession(); + } + } + + public async encryptEvent(event: MatrixEvent, _room: Room): Promise { + const roomId = event.getRoomId()!; + const encryptor = this.roomEncryptors[roomId]; + + if (!encryptor) { + throw new Error(`Cannot encrypt event in unconfigured room ${roomId}`); + } + + await encryptor.encryptEvent(event); + } + public async decryptEvent(event: MatrixEvent): Promise { const roomId = event.getRoomId(); if (!roomId) { @@ -156,6 +192,30 @@ export class RustCrypto implements CryptoBackend { return JSON.parse(result); } + /** called by the sync loop on m.room.encrypted events + * + * @param room - in which the event was received + * @param event - encryption event to be processed + */ + public async onCryptoEvent(room: Room, event: MatrixEvent): Promise { + const config = event.getContent(); + + const existingEncryptor = this.roomEncryptors[room.roomId]; + if (existingEncryptor) { + existingEncryptor.onCryptoEvent(config); + } else { + this.roomEncryptors[room.roomId] = new RoomEncryptor(this.olmMachine, this.keyClaimManager, room, config); + } + + // start tracking devices for any users already known to be in this room. + const members = await room.getEncryptionTargetMembers(); + logger.debug( + `[${room.roomId} encryption] starting to track devices for: `, + members.map((u) => `${u.userId} (${u.membership})`), + ); + await this.olmMachine.updateTrackedUsers(members.map((u) => new RustSdkCryptoJs.UserId(u.userId))); + } + /** called by the sync loop after processing each sync. * * TODO: figure out something equivalent for sliding sync. @@ -168,6 +228,27 @@ export class RustCrypto implements CryptoBackend { this.outgoingRequestLoop(); } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // Other public functions + // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** called by the MatrixClient on a room membership event + * + * @param event - The matrix event which caused this event to fire. + * @param member - The member whose RoomMember.membership changed. + * @param oldMembership - The previous membership state. Null if it's a new member. + */ + public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void { + const enc = this.roomEncryptors[event.getRoomId()!]; + if (!enc) { + // not encrypting in this room + return; + } + enc.onRoomMembership(member); + } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Outgoing requests diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 91ff9d7a7..f63a30460 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -736,8 +736,8 @@ export class SlidingSyncSdk { const processRoomEvent = async (e: MatrixEvent): Promise => { client.emit(ClientEvent.Event, e); - if (e.isState() && e.getType() == EventType.RoomEncryption && this.syncOpts.crypto) { - await this.syncOpts.crypto.onCryptoEvent(room, e); + if (e.isState() && e.getType() == EventType.RoomEncryption && this.syncOpts.cryptoCallbacks) { + await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e); } }; diff --git a/src/sync.ts b/src/sync.ts index 21f27f957..46b7367ca 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1409,10 +1409,10 @@ export class SyncApi { // avoids a race condition if the application tries to send a message after the // state event is processed, but before crypto is enabled, which then causes the // crypto layer to complain. - if (this.syncOpts.crypto) { + if (this.syncOpts.cryptoCallbacks) { for (const e of stateEvents.concat(events)) { if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") { - await this.syncOpts.crypto.onCryptoEvent(room, e); + await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e); } } } diff --git a/yarn.lock b/yarn.lock index d9cc1ac4a..089afda3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,7 +63,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.10.tgz#9d92fa81b87542fff50e848ed585b4212c1d34ec" integrity sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg== -"@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.7.5": +"@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.7.5": version "7.20.12" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== @@ -988,7 +988,7 @@ pirates "^4.0.5" source-map-support "^0.5.16" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": version "7.20.13" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== @@ -2840,6 +2840,11 @@ core-js@^2.4.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== +core-js@^3.0.0: + version "3.27.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.27.2.tgz#85b35453a424abdcacb97474797815f4d62ebbf7" + integrity sha512-9ashVQskuh5AZEZ1JdQWp1GqSoC1e1G87MzRqg2gIfVAQ7Qn9K+uFj8EcniUFA4P2NLZfV+TOlX1SzoKfo+s7w== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -3694,6 +3699,29 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fetch-mock-jest@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/fetch-mock-jest/-/fetch-mock-jest-1.5.1.tgz#0e13df990d286d9239e284f12b279ed509bf53cd" + integrity sha512-+utwzP8C+Pax1GSka3nFXILWMY3Er2L+s090FOgqVNrNCPp0fDqgXnAHAJf12PLHi0z4PhcTaZNTz8e7K3fjqQ== + dependencies: + fetch-mock "^9.11.0" + +fetch-mock@^9.11.0: + version "9.11.0" + resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-9.11.0.tgz#371c6fb7d45584d2ae4a18ee6824e7ad4b637a3f" + integrity sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q== + dependencies: + "@babel/core" "^7.0.0" + "@babel/runtime" "^7.0.0" + core-js "^3.0.0" + debug "^4.1.1" + glob-to-regexp "^0.4.0" + is-subset "^0.1.1" + lodash.isequal "^4.5.0" + path-to-regexp "^2.2.1" + querystring "^0.2.0" + whatwg-url "^6.5.0" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -3873,6 +3901,11 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob-to-regexp@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + glob@^7.1.0, glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -4366,6 +4399,11 @@ is-string@^1.0.5, is-string@^1.0.7: dependencies: has-tostringtag "^1.0.0" +is-subset@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" + integrity sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw== + is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" @@ -5127,6 +5165,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -5673,6 +5716,11 @@ path-platform@~0.11.15: resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" integrity sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg== +path-to-regexp@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz#35ce7f333d5616f1c1e1bfe266c3aba2e5b2e704" + integrity sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -5953,6 +6001,11 @@ querystring@0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== +querystring@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" + integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== + querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -6764,6 +6817,13 @@ tough-cookie@^4.1.2: universalify "^0.2.0" url-parse "^1.5.3" +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== + dependencies: + punycode "^2.1.0" + tr46@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" @@ -7227,6 +7287,15 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +whatwg-url@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-url@^8.4.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77"