diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index 08ad1f5e281..f725cb63f66 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -23,7 +23,7 @@ import { MockResponse, MockResponseFunction } from "fetch-mock"; import Olm from "@matrix-org/olm"; import * as testUtils from "../../test-utils/test-utils"; -import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils"; +import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, mkEventCustom, syncPromise } from "../../test-utils/test-utils"; import * as testData from "../../test-utils/test-data"; import { BOB_SIGNED_CROSS_SIGNING_KEYS_DATA, @@ -37,6 +37,7 @@ import { import { TestClient } from "../../TestClient"; import { logger } from "../../../src/logger"; import { + Category, ClientEvent, createClient, CryptoEvent, @@ -53,6 +54,7 @@ import { Room, RoomMember, RoomStateEvent, + HistoryVisibility, } from "../../../src/matrix"; import { DeviceInfo } from "../../../src/crypto/deviceinfo"; import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; @@ -152,6 +154,27 @@ async function expectSendRoomKey( }); } +/** + * Return the event received on rooms/{roomId}/send/m.room.encrypted endpoint. + * See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid + * @returns the content of event (no decryption) + */ +function expectEncryptedSendMessage() { + return new Promise((resolve) => { + fetchMock.putOnce( + new RegExp("/send/m.room.encrypted/"), + (url, request) => { + const content = JSON.parse(request.body as string); + resolve(content); + return { event_id: "$event_id" }; + }, + // append to the list of intercepts on this path (since we have some tests that call + // this function multiple times) + { overwriteRoutes: false }, + ); + }); +} + /** * Expect that the client sends an encrypted event * @@ -165,22 +188,7 @@ async function expectSendRoomKey( async function expectSendMegolmMessage( inboundGroupSessionPromise: Promise, ): Promise> { - const encryptedMessageContent = await new Promise((resolve) => { - fetchMock.putOnce( - new RegExp("/send/m.room.encrypted/"), - (url: string, opts: RequestInit): MockResponse => { - resolve(JSON.parse(opts.body as string)); - return { - event_id: "$event_id", - }; - }, - { - // append to the list of intercepts on this path (since we have some tests that call - // this function multiple times) - overwriteRoutes: false, - }, - ); - }); + const encryptedMessageContent = await expectEncryptedSendMessage(); // In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now. const inboundGroupSession = await inboundGroupSessionPromise; @@ -930,6 +938,61 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, }); }); + newBackendOnly("should rotate the session when the history visibility changes", async () => { + expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); + await startClientAndAwaitFirstSync(); + const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount); + + // Tell alice we share a room with bob + syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"])); + await syncPromise(aliceClient); + + // Force alice to download bob keys + expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); + + // Send a message to bob and get the current session id + let [, , encryptedMessage] = await Promise.all([ + aliceClient.sendTextMessage(ROOM_ID, "test"), + expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession), + expectEncryptedSendMessage(), + ]); + + // Check that the session id exists + const sessionId = encryptedMessage.session_id; + expect(sessionId).toBeDefined(); + + // Change history visibility in sync response + const syncResponse = getSyncResponse([]); + syncResponse.rooms[Category.Join][ROOM_ID].timeline.events.push( + mkEventCustom({ + sender: TEST_USER_ID, + type: "m.room.history_visibility", + state_key: "", + content: { + history_visibility: HistoryVisibility.Invited, + }, + }), + ); + + // Update the new visibility + syncResponder.sendOrQueueSyncResponse(syncResponse); + await syncPromise(aliceClient); + + // Resend a message to bob and get the new session id + [, , encryptedMessage] = await Promise.all([ + aliceClient.sendTextMessage(ROOM_ID, "test"), + expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession), + expectEncryptedSendMessage(), + ]); + + // Check that the new session id exists + const newSessionId = encryptedMessage.session_id; + expect(newSessionId).toBeDefined(); + + // Check that the session id has changed + expect(sessionId).not.toEqual(newSessionId); + }); + oldBackendOnly("We should start a new megolm session when a device is blocked", async () => { expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync(); diff --git a/spec/unit/rust-crypto/RoomEncryptor.spec.ts b/spec/unit/rust-crypto/RoomEncryptor.spec.ts new file mode 100644 index 00000000000..66d21f9d5da --- /dev/null +++ b/spec/unit/rust-crypto/RoomEncryptor.spec.ts @@ -0,0 +1,31 @@ +/* + * + * 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 { HistoryVisibility as RustHistoryVisibility } from "@matrix-org/matrix-sdk-crypto-wasm"; + +import { HistoryVisibility } from "../../../src"; +import { toRustHistoryVisibility } from "../../../src/rust-crypto/RoomEncryptor"; + +it.each([ + [HistoryVisibility.Invited, RustHistoryVisibility.Invited], + [HistoryVisibility.Joined, RustHistoryVisibility.Joined], + [HistoryVisibility.Shared, RustHistoryVisibility.Shared], + [HistoryVisibility.WorldReadable, RustHistoryVisibility.WorldReadable], +])("JS HistoryVisibility to Rust HistoryVisibility: converts %s to %s", (historyVisibility, expected) => { + expect(toRustHistoryVisibility(historyVisibility)).toBe(expected); +}); diff --git a/src/rust-crypto/RoomEncryptor.ts b/src/rust-crypto/RoomEncryptor.ts index 5f1af2610ca..fc10c6a1338 100644 --- a/src/rust-crypto/RoomEncryptor.ts +++ b/src/rust-crypto/RoomEncryptor.ts @@ -14,7 +14,13 @@ 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-wasm"; +import { + EncryptionSettings, + OlmMachine, + RoomId, + UserId, + HistoryVisibility as RustHistoryVisibility, +} from "@matrix-org/matrix-sdk-crypto-wasm"; import { EventType } from "../@types/event"; import { IContent, MatrixEvent } from "../models/event"; @@ -23,6 +29,7 @@ import { Logger, logger } from "../logger"; import { KeyClaimManager } from "./KeyClaimManager"; import { RoomMember } from "../models/room-member"; import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; +import { HistoryVisibility } from "../@types/partials"; /** * RoomEncryptor: responsible for encrypting messages to a given room @@ -103,7 +110,9 @@ export class RoomEncryptor { this.prefixedLogger.debug("Sessions for users are ready; now sharing room key"); const rustEncryptionSettings = new EncryptionSettings(); - /* FIXME historyVisibility, rotation, etc */ + /* FIXME rotation, etc */ + + rustEncryptionSettings.historyVisibility = toRustHistoryVisibility(this.room.getHistoryVisibility()); const shareMessages = await this.olmMachine.shareRoomKey( new RoomId(this.room.roomId), @@ -152,3 +161,21 @@ export class RoomEncryptor { ); } } + +/** + * Convert a HistoryVisibility to a RustHistoryVisibility + * @param visibility - HistoryVisibility enum + * @returns a RustHistoryVisibility enum + */ +export function toRustHistoryVisibility(visibility: HistoryVisibility): RustHistoryVisibility { + switch (visibility) { + case HistoryVisibility.Invited: + return RustHistoryVisibility.Invited; + case HistoryVisibility.Joined: + return RustHistoryVisibility.Joined; + case HistoryVisibility.Shared: + return RustHistoryVisibility.Shared; + case HistoryVisibility.WorldReadable: + return RustHistoryVisibility.WorldReadable; + } +}