From 760b57e3141a8130b6ef55a51f6121250d8c9b15 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 17 Oct 2023 18:38:26 +0200 Subject: [PATCH 1/4] Wire up rotation --- src/rust-crypto/RoomEncryptor.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/rust-crypto/RoomEncryptor.ts b/src/rust-crypto/RoomEncryptor.ts index 5f1af2610ca..eb8c1a0f4f0 100644 --- a/src/rust-crypto/RoomEncryptor.ts +++ b/src/rust-crypto/RoomEncryptor.ts @@ -103,7 +103,18 @@ export class RoomEncryptor { this.prefixedLogger.debug("Sessions for users are ready; now sharing room key"); const rustEncryptionSettings = new EncryptionSettings(); - /* FIXME historyVisibility, rotation, etc */ + /* FIXME historyVisibility, etc */ + + // We need to convert the rotation period from milliseconds to microseconds + // See https://spec.matrix.org/v1.8/client-server-api/#mroomencryption and + // https://matrix-org.github.io/matrix-rust-sdk-crypto-wasm/classes/EncryptionSettings.html#rotationPeriod + if (typeof this.encryptionSettings.rotation_period_ms === "number") { + rustEncryptionSettings.rotationPeriod = BigInt(this.encryptionSettings.rotation_period_ms * 1000); + } + + if (typeof this.encryptionSettings.rotation_period_msgs === "number") { + rustEncryptionSettings.rotationPeriodMessages = BigInt(this.encryptionSettings.rotation_period_msgs); + } const shareMessages = await this.olmMachine.shareRoomKey( new RoomId(this.room.roomId), From baf77f64025c5d9f34dd96ac635ff508741ba316 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 17 Oct 2023 18:47:10 +0200 Subject: [PATCH 2/4] Wire up algorithm --- src/rust-crypto/RoomEncryptor.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/rust-crypto/RoomEncryptor.ts b/src/rust-crypto/RoomEncryptor.ts index eb8c1a0f4f0..b802afb9d66 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 { + EncryptionAlgorithm, + EncryptionSettings, + OlmMachine, + RoomId, + UserId, +} from "@matrix-org/matrix-sdk-crypto-wasm"; import { EventType } from "../@types/event"; import { IContent, MatrixEvent } from "../models/event"; @@ -105,6 +111,9 @@ export class RoomEncryptor { const rustEncryptionSettings = new EncryptionSettings(); /* FIXME historyVisibility, etc */ + // We only support megolm + rustEncryptionSettings.algorithm = EncryptionAlgorithm.MegolmV1AesSha2; + // We need to convert the rotation period from milliseconds to microseconds // See https://spec.matrix.org/v1.8/client-server-api/#mroomencryption and // https://matrix-org.github.io/matrix-rust-sdk-crypto-wasm/classes/EncryptionSettings.html#rotationPeriod From 7bbe247b993af51551adecb5ee5f06937490fc4d Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 18 Oct 2023 17:53:42 +0200 Subject: [PATCH 3/4] Add encryption settings test --- spec/integ/crypto/crypto.spec.ts | 155 +++++++++++++++++++++++++++---- 1 file changed, 139 insertions(+), 16 deletions(-) diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index 55058bf0789..e31897e098b 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -37,6 +37,7 @@ import { import { TestClient } from "../../TestClient"; import { logger } from "../../../src/logger"; import { + Category, ClientEvent, createClient, CryptoEvent, @@ -146,6 +147,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 * @@ -159,22 +181,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; @@ -924,6 +931,122 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, }); }); + describe("Session should rotate according to encryption settings", () => { + /** + * Send a message to bob and get the encrypted message + * @returns {Promise} The encrypted message + */ + async function sendEncryptedMessage(): Promise { + const [encryptedMessage] = await Promise.all([ + expectEncryptedSendMessage(), + aliceClient.sendTextMessage(ROOM_ID, "test"), + ]); + return encryptedMessage; + } + + afterEach(() => { + jest.useRealTimers(); + }); + + newBackendOnly("should rotate the session after 2 messages", async () => { + expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); + await startClientAndAwaitFirstSync(); + const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount); + + const syncResponse = getSyncResponse(["@bob:xyz"]); + // Every 2 messages in the room, the session should be rotated + syncResponse.rooms[Category.Join][ROOM_ID].state.events[0].content = { + algorithm: "m.megolm.v1.aes-sha2", + rotation_period_msgs: 2, + }; + + // tell alice we share a room with bob + syncResponder.sendOrQueueSyncResponse(syncResponse); + await syncPromise(aliceClient); + + // Force alice to download bob keys + expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); + + // Send a message to bob and get the encrypted message + const [encryptedMessage] = await Promise.all([ + sendEncryptedMessage(), + expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession), + ]); + + // Check that the session id exists + const sessionId = encryptedMessage.session_id; + expect(sessionId).toBeDefined(); + + // Send a message to bob and get the current message + const secondEncryptedMessage = await sendEncryptedMessage(); + + // Check that the same session id is shared between the two messages + const secondSessionId = secondEncryptedMessage.session_id; + expect(secondSessionId).toBe(sessionId); + + // The session should be rotated, we are expecting the room key to be sent + const [thirdEncryptedMessage] = await Promise.all([ + sendEncryptedMessage(), + expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession), + ]); + + // The session is rotated every 2 messages, we should have a new session id + const thirdSessionId = thirdEncryptedMessage.session_id; + expect(thirdSessionId).not.toBe(sessionId); + }); + + newBackendOnly("should rotate the session after 1h", async () => { + expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); + await startClientAndAwaitFirstSync(); + const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount); + + // We need to fake the timers to make the session rotate + jest.useFakeTimers(); + + const syncResponse = getSyncResponse(["@bob:xyz"]); + + // The minimum rotation period is 1h + // https://github.com/matrix-org/matrix-rust-sdk/blob/f75b2cd1d0981db42751dadb08c826740af1018e/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs#L410-L415 + const oneHourInMs = 60 * 60 * 1000; + + // Every 1h the session should be rotated + syncResponse.rooms[Category.Join][ROOM_ID].state.events[0].content = { + algorithm: "m.megolm.v1.aes-sha2", + rotation_period_ms: oneHourInMs, + }; + + // tell alice we share a room with bob + syncResponder.sendOrQueueSyncResponse(syncResponse); + await syncPromise(aliceClient); + + // Force alice to download bob keys + expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz")); + + // Send a message to bob and get the encrypted message + const [encryptedMessage] = await Promise.all([ + sendEncryptedMessage(), + expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession), + ]); + + // Check that the session id exists + const sessionId = encryptedMessage.session_id; + expect(sessionId).toBeDefined(); + + // Advance the time by 1h + jest.advanceTimersByTime(oneHourInMs); + + // Send a second message to bob and get the encrypted message + const [secondEncryptedMessage] = await Promise.all([ + sendEncryptedMessage(), + expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession), + ]); + + // The session should be rotated + const secondSessionId = secondEncryptedMessage.session_id; + expect(secondSessionId).not.toBe(sessionId); + }); + }); + oldBackendOnly("We should start a new megolm session when a device is blocked", async () => { expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync(); From ee6a52b815bcaba19e5343e9d64bd5613bfd1eae Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 23 Oct 2023 12:04:11 +0200 Subject: [PATCH 4/4] Update comments --- spec/integ/crypto/crypto.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index e31897e098b..a87fdd28d18 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -150,7 +150,7 @@ 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) + * @returns the content of the encrypted event */ function expectEncryptedSendMessage() { return new Promise((resolve) => { @@ -960,7 +960,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, rotation_period_msgs: 2, }; - // tell alice we share a room with bob + // Tell alice we share a room with bob syncResponder.sendOrQueueSyncResponse(syncResponse); await syncPromise(aliceClient); @@ -977,7 +977,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, const sessionId = encryptedMessage.session_id; expect(sessionId).toBeDefined(); - // Send a message to bob and get the current message + // Send a second message to bob and get the current message const secondEncryptedMessage = await sendEncryptedMessage(); // Check that the same session id is shared between the two messages @@ -1000,7 +1000,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, await startClientAndAwaitFirstSync(); const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount); - // We need to fake the timers to make the session rotate + // We need to fake the timers to advance the time jest.useFakeTimers(); const syncResponse = getSyncResponse(["@bob:xyz"]); @@ -1015,7 +1015,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, rotation_period_ms: oneHourInMs, }; - // tell alice we share a room with bob + // Tell alice we share a room with bob syncResponder.sendOrQueueSyncResponse(syncResponse); await syncPromise(aliceClient);