diff --git a/package.json b/package.json index 337cb1d20aa..1b6eb295b87 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^4.0.1", + "@matrix-org/matrix-sdk-crypto-wasm": "^4.1.0", "another-json": "^0.2.0", "bs58": "^5.0.0", "content-type": "^1.0.4", diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index e819cb7cb18..ed3af3b8e8e 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -29,6 +29,7 @@ import { getSyncResponse, InitCrypto, mkEventCustom, + mkMembershipCustom, syncPromise, } from "../../test-utils/test-utils"; import * as testData from "../../test-utils/test-data"; @@ -38,6 +39,7 @@ import { BOB_TEST_USER_ID, SIGNED_CROSS_SIGNING_KEYS_DATA, SIGNED_TEST_DEVICE_DATA, + TEST_ROOM_ID, TEST_ROOM_ID as ROOM_ID, TEST_USER_ID, } from "../../test-utils/test-data"; @@ -230,9 +232,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, /** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */ let keyReceiver: E2EKeyReceiver; - /** an object which intercepts `/keys/query` requests on the test homeserver */ - let keyResponder: E2EKeyResponder; - /** an object which intercepts `/sync` requests from {@link #aliceClient} */ let syncResponder: ISyncResponder; @@ -368,6 +367,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, accessToken: "akjgkrgjs", deviceId: "xzcvb", cryptoCallbacks: createCryptoCallbacks(), + logger: logger.getChild("aliceClient"), }); /* set up listeners for /keys/upload and /sync */ @@ -701,7 +701,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, it("prepareToEncrypt", async () => { const homeserverUrl = aliceClient.getHomeserverUrl(); - keyResponder = new E2EKeyResponder(homeserverUrl); + const keyResponder = new E2EKeyResponder(homeserverUrl); keyResponder.addKeyReceiver("@alice:localhost", keyReceiver); const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID"); @@ -732,7 +732,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, it("Alice sends a megolm message with GlobalErrorOnUnknownDevices=false", async () => { aliceClient.setGlobalErrorOnUnknownDevices(false); const homeserverUrl = aliceClient.getHomeserverUrl(); - keyResponder = new E2EKeyResponder(homeserverUrl); + const keyResponder = new E2EKeyResponder(homeserverUrl); keyResponder.addKeyReceiver("@alice:localhost", keyReceiver); const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID"); @@ -760,7 +760,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, it("We should start a new megolm session after forceDiscardSession", async () => { aliceClient.setGlobalErrorOnUnknownDevices(false); const homeserverUrl = aliceClient.getHomeserverUrl(); - keyResponder = new E2EKeyResponder(homeserverUrl); + const keyResponder = new E2EKeyResponder(homeserverUrl); keyResponder.addKeyReceiver("@alice:localhost", keyReceiver); const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID"); @@ -2070,7 +2070,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, it("Sending an event initiates a member list sync", async () => { const homeserverUrl = aliceClient.getHomeserverUrl(); - keyResponder = new E2EKeyResponder(homeserverUrl); + const keyResponder = new E2EKeyResponder(homeserverUrl); keyResponder.addKeyReceiver("@alice:localhost", keyReceiver); const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID"); @@ -2093,7 +2093,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, it("loading the membership list inhibits a later load", async () => { const homeserverUrl = aliceClient.getHomeserverUrl(); - keyResponder = new E2EKeyResponder(homeserverUrl); + const keyResponder = new E2EKeyResponder(homeserverUrl); keyResponder.addKeyReceiver("@alice:localhost", keyReceiver); const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID"); @@ -2903,7 +2903,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, // anything that we don't have a specific matcher for silently returns a 404 fetchMock.catch(404); - keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl()); + const keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl()); keyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA); keyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA); keyResponder.addKeyReceiver(BOB_TEST_USER_ID, keyReceiver); @@ -2939,4 +2939,180 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expect(hasCrossSigningKeysForUser).toBe(false); }); }); + + /** Guards against downgrade attacks from servers hiding or manipulating the crypto settings. */ + describe("Persistent encryption settings", () => { + let persistentStoreClient: MatrixClient; + let client2: MatrixClient; + + beforeEach(async () => { + const homeserverurl = "https://alice-server.com"; + const userId = "@alice:localhost"; + + const keyResponder = new E2EKeyResponder(homeserverurl); + keyResponder.addKeyReceiver(userId, keyReceiver); + + // For legacy crypto, these tests only work properly with a proper (indexeddb-based) CryptoStore, so + // rather than using the existing `aliceClient`, create a new client. Once we drop legacy crypto, we can + // just use `aliceClient` here. + persistentStoreClient = await makeNewClient(homeserverurl, userId, "persistentStoreClient"); + await persistentStoreClient.startClient({}); + }); + + afterEach(async () => { + persistentStoreClient.stopClient(); + client2?.stopClient(); + }); + + test("Sending a message in a room where the server is hiding the state event does not send a plaintext event", async () => { + // Alice is in an encrypted room + const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2" }); + syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState])); + await syncPromise(persistentStoreClient); + + // Send a message, and expect to get an `m.room.encrypted` event. + await Promise.all([persistentStoreClient.sendTextMessage(ROOM_ID, "test"), expectEncryptedSendMessage()]); + + // We now replace the client, and allow the new one to resync, *without* the encryption event. + client2 = await replaceClient(persistentStoreClient); + syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([])); + await client2.startClient({}); + await syncPromise(client2); + logger.log(client2.getUserId() + ": restarted"); + + await expectSendMessageToFail(client2); + }); + + test("Changes to the rotation period should be ignored", async () => { + // Alice is in an encrypted room, where the rotation period is set to 2 messages + const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 }); + syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState])); + await syncPromise(persistentStoreClient); + + // Send a message, and expect to get an `m.room.encrypted` event. + const [, msg1Content] = await Promise.all([ + persistentStoreClient.sendTextMessage(ROOM_ID, "test1"), + expectEncryptedSendMessage(), + ]); + + // Replace the state with one which bumps the rotation period. This should be ignored, though it's not + // clear that is correct behaviour (see https://github.com/element-hq/element-meta/issues/69) + const encryptionState2 = mkEncryptionEvent({ + algorithm: "m.megolm.v1.aes-sha2", + rotation_period_msgs: 100, + }); + syncResponder.sendOrQueueSyncResponse({ + next_batch: "1", + rooms: { join: { [TEST_ROOM_ID]: { timeline: { events: [encryptionState2], prev_batch: "" } } } }, + }); + await syncPromise(persistentStoreClient); + + // Send two more messages. The first should use the same megolm session as the first; the second should + // use a different one. + const [, msg2Content] = await Promise.all([ + persistentStoreClient.sendTextMessage(ROOM_ID, "test2"), + expectEncryptedSendMessage(), + ]); + expect(msg2Content.session_id).toEqual(msg1Content.session_id); + const [, msg3Content] = await Promise.all([ + persistentStoreClient.sendTextMessage(ROOM_ID, "test3"), + expectEncryptedSendMessage(), + ]); + expect(msg3Content.session_id).not.toEqual(msg1Content.session_id); + }); + + test("Changes to the rotation period should be ignored after a client restart", async () => { + // Alice is in an encrypted room, where the rotation period is set to 2 messages + const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 }); + syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState])); + await syncPromise(persistentStoreClient); + + // Send a message, and expect to get an `m.room.encrypted` event. + await Promise.all([persistentStoreClient.sendTextMessage(ROOM_ID, "test1"), expectEncryptedSendMessage()]); + + // We now replace the client, and allow the new one to resync with a *different* encryption event. + client2 = await replaceClient(persistentStoreClient); + const encryptionState2 = mkEncryptionEvent({ + algorithm: "m.megolm.v1.aes-sha2", + rotation_period_msgs: 100, + }); + syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState2])); + await client2.startClient({}); + await syncPromise(client2); + logger.log(client2.getUserId() + ": restarted"); + + // Now send another message, which should (for now) be rejected. + await expectSendMessageToFail(client2); + }); + + /** Shut down `oldClient`, and build a new MatrixClient for the same user. */ + async function replaceClient(oldClient: MatrixClient) { + oldClient.stopClient(); + syncResponder.sendOrQueueSyncResponse({}); // flush pending request from old client + return makeNewClient(oldClient.getHomeserverUrl(), oldClient.getSafeUserId(), "client2"); + } + + async function makeNewClient( + homeserverUrl: string, + userId: string, + loggerPrefix: string, + ): Promise { + const client = createClient({ + baseUrl: homeserverUrl, + userId: userId, + accessToken: "akjgkrgjs", + deviceId: "xzcvb", + cryptoCallbacks: createCryptoCallbacks(), + logger: logger.getChild(loggerPrefix), + + // For legacy crypto, these tests only work with a proper persistent cryptoStore. + cryptoStore: new IndexedDBCryptoStore(indexedDB, "test"), + }); + await initCrypto(client); + mockInitialApiRequests(client.getHomeserverUrl()); + return client; + } + + function mkEncryptionEvent(content: Object) { + return mkEventCustom({ + sender: persistentStoreClient.getSafeUserId(), + type: "m.room.encryption", + state_key: "", + content: content, + }); + } + + /** Sync response which includes `TEST_ROOM_ID`, where alice is a member + * + * @param stateEvents - Additional state events for the test room + */ + function getSyncResponseWithState(stateEvents: Array) { + const roomResponse = { + state: { + events: [ + mkMembershipCustom({ membership: "join", sender: persistentStoreClient.getSafeUserId() }), + ...stateEvents, + ], + }, + timeline: { + events: [], + prev_batch: "", + }, + }; + + return { + next_batch: "1", + rooms: { join: { [TEST_ROOM_ID]: roomResponse } }, + }; + } + + /** Send a message with the given client, and check that it is not sent in plaintext */ + async function expectSendMessageToFail(aliceClient2: MatrixClient) { + // The precise failure mode here is somewhat up for debate (https://github.com/element-hq/element-meta/issues/69). + // For now, the attempt to send is rejected with an exception. The text is different between old and new stacks. + await expect(aliceClient2.sendTextMessage(ROOM_ID, "test")).rejects.toThrow( + /unconfigured room !room:id|Room !room:id was previously configured to use encryption/, + ); + } + }); }); diff --git a/spec/test-utils/mockEndpoints.ts b/spec/test-utils/mockEndpoints.ts index 22dda0b88b2..988d6f13b6c 100644 --- a/spec/test-utils/mockEndpoints.ts +++ b/spec/test-utils/mockEndpoints.ts @@ -24,11 +24,21 @@ import { KeyBackupInfo } from "../../src/crypto-api"; * @param homeserverUrl - the homeserver url for the client under test */ export function mockInitialApiRequests(homeserverUrl: string) { - fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] }); - fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {}); - fetchMock.postOnce(new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), { - filter_id: "fid", - }); + fetchMock.getOnce( + new URL("/_matrix/client/versions", homeserverUrl).toString(), + { versions: ["v1.1"] }, + { overwriteRoutes: true }, + ); + fetchMock.getOnce( + new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), + {}, + { overwriteRoutes: true }, + ); + fetchMock.postOnce( + new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), + { filter_id: "fid" }, + { overwriteRoutes: true }, + ); } /** diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 2a79293e2ad..f9fab0864ba 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -65,7 +65,7 @@ import { PolicyScope, } from "../../src/models/invites-ignorer"; import { IOlmDevice } from "../../src/crypto/algorithms/megolm"; -import { QueryDict } from "../../src/utils"; +import { defer, QueryDict } from "../../src/utils"; import { SyncState } from "../../src/sync"; import * as featureUtils from "../../src/feature"; import { StubStore } from "../../src/store/stub"; @@ -1453,6 +1453,8 @@ describe("MatrixClient", function () { hasEncryptionStateEvent: jest.fn().mockReturnValue(true), } as unknown as Room; + let mockCrypto: Mocked; + let event: MatrixEvent; beforeEach(async () => { event = new MatrixEvent({ @@ -1467,11 +1469,12 @@ describe("MatrixClient", function () { expect(getRoomId).toEqual(roomId); return mockRoom; }; - client.crypto = client["cryptoBackend"] = { - // mock crypto - encryptEvent: () => new Promise(() => {}), + mockCrypto = { + isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true), + encryptEvent: jest.fn(), stop: jest.fn(), - } as unknown as Crypto; + } as unknown as Mocked; + client.crypto = client["cryptoBackend"] = mockCrypto; }); function assertCancelled() { @@ -1488,12 +1491,21 @@ describe("MatrixClient", function () { }); it("should cancel an event which is encrypting", async () => { + const encryptEventDefer = defer(); + mockCrypto.encryptEvent.mockReturnValue(encryptEventDefer.promise); + + const statusPromise = testUtils.emitPromise(event, "Event.status"); // @ts-ignore protected method access - client.encryptAndSendEvent(mockRoom, event); - await testUtils.emitPromise(event, "Event.status"); + const encryptAndSendPromise = client.encryptAndSendEvent(mockRoom, event); + await statusPromise; expect(event.status).toBe(EventStatus.ENCRYPTING); client.cancelPendingEvent(event); assertCancelled(); + + // now let the encryption complete, and check that the message is not sent. + encryptEventDefer.resolve(); + await encryptAndSendPromise; + assertCancelled(); }); it("should cancel an event which is not sent", () => { diff --git a/spec/unit/queueToDevice.spec.ts b/spec/unit/queueToDevice.spec.ts index 1099bcb82e8..f9b90304e02 100644 --- a/spec/unit/queueToDevice.spec.ts +++ b/spec/unit/queueToDevice.spec.ts @@ -265,6 +265,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st }); const mockRoom = { updatePendingEvent: jest.fn(), + hasEncryptionStateEvent: jest.fn().mockReturnValue(false), } as unknown as Room; client.resendEvent(dummyEvent, mockRoom); diff --git a/src/client.ts b/src/client.ts index 44d9fbd33a9..23bf2af75fd 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1305,7 +1305,13 @@ export class MatrixClient extends TypedEventEmitter>(); + + /** IDs of events which are currently being encrypted. + * + * This is part of the cancellation mechanism: if the event is no longer listed here when encryption completes, + * that tells us that it has been cancelled, and we should not send it. + */ + private eventsBeingEncrypted = new Set(); private useE2eForGroupCall = true; private toDeviceMessageQueue: ToDeviceMessageQueue; @@ -3245,6 +3251,9 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -3257,6 +3266,9 @@ export class MatrixClient extends TypedEventEmitter { - let cancelled = false; - // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections, - // so that we can handle synchronous and asynchronous exceptions with the - // same code path. - return Promise.resolve() - .then(() => { - const encryptionPromise = this.encryptEventIfNeeded(event, room ?? undefined); - if (!encryptionPromise) return null; // doesn't need encryption - - this.pendingEventEncryption.set(event.getId()!, encryptionPromise); - this.updatePendingEventStatus(room, event, EventStatus.ENCRYPTING); - return encryptionPromise.then(() => { - if (!this.pendingEventEncryption.has(event.getId()!)) { - // cancelled via MatrixClient::cancelPendingEvent - cancelled = true; - return; - } - this.updatePendingEventStatus(room, event, EventStatus.SENDING); - }); - }) - .then(() => { - if (cancelled) return {} as ISendEventResponse; - let promise: Promise | null = null; - if (this.scheduler) { - // if this returns a promise then the scheduler has control now and will - // resolve/reject when it is done. Internally, the scheduler will invoke - // processFn which is set to this._sendEventHttpRequest so the same code - // path is executed regardless. - promise = this.scheduler.queueEvent(event); - if (promise && this.scheduler.getQueueForEvent(event)!.length > 1) { - // event is processed FIFO so if the length is 2 or more we know - // this event is stuck behind an earlier event. - this.updatePendingEventStatus(room, event, EventStatus.QUEUED); - } - } + protected async encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise { + try { + let cancelled: boolean; + this.eventsBeingEncrypted.add(event.getId()!); + try { + await this.encryptEventIfNeeded(event, room ?? undefined); + } finally { + cancelled = !this.eventsBeingEncrypted.delete(event.getId()!); + } - if (!promise) { - promise = this.sendEventHttpRequest(event); - if (room) { - promise = promise.then((res) => { - room.updatePendingEvent(event, EventStatus.SENT, res["event_id"]); - return res; - }); - } - } + if (cancelled) { + // cancelled via MatrixClient::cancelPendingEvent + return {} as ISendEventResponse; + } - return promise; - }) - .catch((err) => { - this.logger.error("Error sending event", err.stack || err); - try { - // set the error on the event before we update the status: - // updating the status emits the event, so the state should be - // consistent at that point. - event.error = err; - this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT); - } catch (e) { - this.logger.error("Exception in error handler!", (e).stack || err); + // encryptEventIfNeeded may have updated the status from SENDING to ENCRYPTING. If so, we need + // to put it back. + if (event.status === EventStatus.ENCRYPTING) { + this.updatePendingEventStatus(room, event, EventStatus.SENDING); + } + + let promise: Promise | null = null; + if (this.scheduler) { + // if this returns a promise then the scheduler has control now and will + // resolve/reject when it is done. Internally, the scheduler will invoke + // processFn which is set to this._sendEventHttpRequest so the same code + // path is executed regardless. + promise = this.scheduler.queueEvent(event); + if (promise && this.scheduler.getQueueForEvent(event)!.length > 1) { + // event is processed FIFO so if the length is 2 or more we know + // this event is stuck behind an earlier event. + this.updatePendingEventStatus(room, event, EventStatus.QUEUED); } - if (err instanceof MatrixError) { - err.event = event; + } + + if (!promise) { + promise = this.sendEventHttpRequest(event); + if (room) { + promise = promise.then((res) => { + room.updatePendingEvent(event, EventStatus.SENT, res["event_id"]); + return res; + }); } - throw err; - }); - } + } - private encryptEventIfNeeded(event: MatrixEvent, room?: Room): Promise | null { - if (event.isEncrypted()) { - // this event has already been encrypted; this happens if the - // encryption step succeeded, but the send step failed on the first - // attempt. - return null; + return await promise; + } catch (err) { + this.logger.error("Error sending event", err); + try { + // set the error on the event before we update the status: + // updating the status emits the event, so the state should be + // consistent at that point. + event.error = err; + this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT); + } catch (e) { + this.logger.error("Exception in error handler!", e); + } + if (err instanceof MatrixError) { + err.event = event; + } + throw err; } + } - if (event.isRedaction()) { - // Redactions do not support encryption in the spec at this time, - // whilst it mostly worked in some clients, it wasn't compliant. - return null; - } + private async encryptEventIfNeeded(event: MatrixEvent, room?: Room): Promise { + // If the room is unknown, we cannot encrypt for it + if (!room) return; - if (!room || !this.isRoomEncrypted(event.getRoomId()!)) { - return null; - } + if (!(await this.shouldEncryptEventForRoom(event, room))) return; if (!this.cryptoBackend && this.usingExternalCrypto) { // The client has opted to allow sending messages to encrypted - // rooms even if the room is encrypted, and we haven't setup + // rooms even if the room is encrypted, and we haven't set up // crypto. This is useful for users of matrix-org/pantalaimon - return null; + return; + } + + if (!this.cryptoBackend) { + throw new Error("This room is configured to use encryption, but your client does not support encryption."); + } + + this.updatePendingEventStatus(room, event, EventStatus.ENCRYPTING); + await this.cryptoBackend.encryptEvent(event, room); + } + + /** + * Determine whether a given event should be encrypted when we send it to the given room. + * + * This takes into account event type and room configuration. + */ + private async shouldEncryptEventForRoom(event: MatrixEvent, room: Room): Promise { + if (event.isEncrypted()) { + // this event has already been encrypted; this happens if the + // encryption step succeeded, but the send step failed on the first + // attempt. + return false; } if (event.getType() === EventType.Reaction) { @@ -4852,14 +4871,23 @@ export class MatrixClient extends TypedEventEmitter; + /** + * Check if we believe the given room to be encrypted. + * + * This method returns true if the room has been configured with encryption. The setting is persistent, so that + * even if the encryption event is removed from the room state, it still returns true. This helps to guard against + * a downgrade attack wherein a server admin attempts to remove encryption. + * + * @returns `true` if the room with the supplied ID is encrypted. `false` if the room is not encrypted, or is unknown to + * us. + */ + isEncryptionEnabledInRoom(roomId: string): Promise; + /** * Perform any background tasks that can be done before a message is ready to * send, in order to speed up sending of the message. @@ -189,7 +201,7 @@ export interface CryptoApi { * Cross-signing a device indicates, to our other devices and to other users, that we have verified that it really * belongs to us. * - * Requires that cross-signing has been set up on this device (normally by calling {@link bootstrapCrossSigning}. + * Requires that cross-signing has been set up on this device (normally by calling {@link bootstrapCrossSigning}). * * *Note*: Do not call this unless you have verified, somehow, that the device is genuine! * diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 16e37605b94..1a0e4f43dae 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -4273,6 +4273,13 @@ export class Crypto extends TypedEventEmitter { + return this.isRoomEncrypted(roomId); + } + /** * @returns information about the encryption on the room with the supplied * ID, or null if the room is not encrypted or unknown to us. diff --git a/src/models/event.ts b/src/models/event.ts index 370f94c2993..4f5f5ab3030 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -350,7 +350,7 @@ export class MatrixEvent extends TypedEventEmitter { + const roomSettings: RustSdkCryptoJs.RoomSettings | undefined = await this.olmMachine.getRoomSettings( + new RustSdkCryptoJs.RoomId(roomId), + ); + return Boolean(roomSettings?.algorithm); + } + /** * Implementation of {@link CryptoApi#getOwnDeviceKeys}. */ @@ -1285,7 +1295,27 @@ export class RustCrypto extends TypedEventEmitter { const config = event.getContent(); + const settings = new RustSdkCryptoJs.RoomSettings(); + + if (config.algorithm === "m.megolm.v1.aes-sha2") { + settings.algorithm = RustSdkCryptoJs.EncryptionAlgorithm.MegolmV1AesSha2; + } else { + // Among other situations, this happens if the crypto state event is redacted. + this.logger.warn(`Room ${room.roomId}: ignoring crypto event with invalid algorithm ${config.algorithm}`); + return; + } + + try { + settings.sessionRotationPeriodMs = config.rotation_period_ms; + settings.sessionRotationPeriodMessages = config.rotation_period_msgs; + await this.olmMachine.setRoomSettings(new RustSdkCryptoJs.RoomId(room.roomId), settings); + } catch (e) { + this.logger.warn(`Room ${room.roomId}: ignoring crypto event which caused error: ${e}`); + return; + } + // If we got this far, the SDK found the event acceptable. + // We need to either create or update the active RoomEncryptor. const existingEncryptor = this.roomEncryptors[room.roomId]; if (existingEncryptor) { existingEncryptor.onCryptoEvent(config); diff --git a/yarn.lock b/yarn.lock index 19b24992816..bf2db4f2bb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1674,10 +1674,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@matrix-org/matrix-sdk-crypto-wasm@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.0.1.tgz#b1d3848a6adc120622e5225045330d253273b117" - integrity sha512-0B4QQ9kop8AocmQDcOfROCQ6QyGZeogpsvTYfEB9ZIBtndCCwy/C3mkxzJD6+gEo1bJ4TdYnblhN7hEQlAG50g== +"@matrix-org/matrix-sdk-crypto-wasm@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.1.0.tgz#9b470ed57cf82b0891f6e1cced1dba90c87ef668" + integrity sha512-/jCyvpDmgAybQWiRMmzflm4cneaRNVt8USqEV1RxoHBzlIE68LtLc9/HCfaPjkY7aYLHTbCrThR9GFXRWGyRxQ== "@matrix-org/olm@3.2.15": version "3.2.15"