1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Implement experimental encrypted state events. (#4994)

* feat: Implement experimental encrypted state events.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* fix: Add cast from StateEvents[K] to IContent.

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
This commit is contained in:
Skye Elliot
2025-09-24 12:44:17 +01:00
committed by GitHub
parent dbe441de33
commit a08a2737e1
17 changed files with 594 additions and 144 deletions

View File

@@ -49,7 +49,7 @@
], ],
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^15.2.0", "@matrix-org/matrix-sdk-crypto-wasm": "^15.3.0",
"another-json": "^0.2.0", "another-json": "^0.2.0",
"bs58": "^6.0.0", "bs58": "^6.0.0",
"content-type": "^1.0.4", "content-type": "^1.0.4",

View File

@@ -88,7 +88,10 @@ import {
encryptMegolmEventRawPlainText, encryptMegolmEventRawPlainText,
establishOlmSession, establishOlmSession,
getTestOlmAccountKeys, getTestOlmAccountKeys,
} from "./olm-utils"; expectSendRoomKey,
expectSendMegolmMessageEvent,
expectEncryptedSendMessageEvent,
} from "./olm-utils.ts";
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator"; import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event"; import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
import { KnownMembership } from "../../../src/@types/membership"; import { KnownMembership } from "../../../src/@types/membership";
@@ -104,107 +107,6 @@ afterEach(() => {
jest.useRealTimers(); jest.useRealTimers();
}); });
/**
* Expect that the client shares keys with the given recipient
*
* Waits for an HTTP request to send the encrypted m.room_key to-device message; decrypts it and uses it
* to establish an Olm InboundGroupSession.
*
* @param recipientUserID - the user id of the expected recipient
*
* @param recipientOlmAccount - Olm.Account for the recipient
*
* @param recipientOlmSession - an Olm.Session for the recipient, which must already have exchanged pre-key
* messages with the sender. Alternatively, null, in which case we will expect a pre-key message.
*
* @returns the established inbound group session
*/
async function expectSendRoomKey(
recipientUserID: string,
recipientOlmAccount: Olm.Account,
recipientOlmSession: Olm.Session | null = null,
): Promise<Olm.InboundGroupSession> {
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
function onSendRoomKey(content: any): Olm.InboundGroupSession {
const m = content.messages[recipientUserID].DEVICE_ID;
const ct = m.ciphertext[testRecipientKey];
if (!recipientOlmSession) {
expect(ct.type).toEqual(0); // pre-key message
recipientOlmSession = new Olm.Session();
recipientOlmSession.create_inbound(recipientOlmAccount, ct.body);
} else {
expect(ct.type).toEqual(1); // regular message
}
const decrypted = JSON.parse(recipientOlmSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual("m.room_key");
const inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
return inboundGroupSession;
}
return await new Promise<Olm.InboundGroupSession>((resolve) => {
fetchMock.putOnce(
new RegExp("/sendToDevice/m.room.encrypted/"),
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const content = JSON.parse(opts.body as string);
resolve(onSendRoomKey(content));
return {};
},
{
// append to the list of intercepts on this path (since we have some tests that call
// this function multiple times)
overwriteRoutes: false,
},
);
});
}
/**
* 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 the encrypted event
*/
function expectEncryptedSendMessage() {
return new Promise<IContent>((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
*
* Waits for an HTTP request to send an encrypted message in the test room.
*
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
*
* @returns The content of the successfully-decrypted event
*/
async function expectSendMegolmMessage(
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
): Promise<Partial<IEvent>> {
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;
const r: any = inboundGroupSession.decrypt(encryptedMessageContent!.ciphertext);
logger.log("Decrypted received megolm message", r);
return JSON.parse(r.plaintext);
}
describe("crypto", () => { describe("crypto", () => {
let testOlmAccount = {} as unknown as Olm.Account; let testOlmAccount = {} as unknown as Olm.Account;
let testSenderKey = ""; let testSenderKey = "";
@@ -991,7 +893,7 @@ describe("crypto", () => {
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt. // Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
await Promise.all([ await Promise.all([
aliceClient.sendTextMessage(ROOM_ID, "test"), aliceClient.sendTextMessage(ROOM_ID, "test"),
expectSendMegolmMessage(inboundGroupSessionPromise), expectSendMegolmMessageEvent(inboundGroupSessionPromise),
]); ]);
}); });
@@ -1018,7 +920,7 @@ describe("crypto", () => {
// Send the first message, and check we can decrypt it. // Send the first message, and check we can decrypt it.
await Promise.all([ await Promise.all([
aliceClient.sendTextMessage(ROOM_ID, "test"), aliceClient.sendTextMessage(ROOM_ID, "test"),
expectSendMegolmMessage(inboundGroupSessionPromise), expectSendMegolmMessageEvent(inboundGroupSessionPromise),
]); ]);
// Finally the interesting part: discard the session. // Finally the interesting part: discard the session.
@@ -1026,7 +928,7 @@ describe("crypto", () => {
// Now when we send the next message, we should get a *new* megolm session. // Now when we send the next message, we should get a *new* megolm session.
const inboundGroupSessionPromise2 = expectSendRoomKey("@bob:xyz", testOlmAccount); const inboundGroupSessionPromise2 = expectSendRoomKey("@bob:xyz", testOlmAccount);
const p2 = expectSendMegolmMessage(inboundGroupSessionPromise2); const p2 = expectSendMegolmMessageEvent(inboundGroupSessionPromise2);
await Promise.all([aliceClient.sendTextMessage(ROOM_ID, "test2"), p2]); await Promise.all([aliceClient.sendTextMessage(ROOM_ID, "test2"), p2]);
}); });
@@ -1037,7 +939,7 @@ describe("crypto", () => {
*/ */
async function sendEncryptedMessage(): Promise<IContent> { async function sendEncryptedMessage(): Promise<IContent> {
const [encryptedMessage] = await Promise.all([ const [encryptedMessage] = await Promise.all([
expectEncryptedSendMessage(), expectEncryptedSendMessageEvent(),
aliceClient.sendTextMessage(ROOM_ID, "test"), aliceClient.sendTextMessage(ROOM_ID, "test"),
]); ]);
return encryptedMessage; return encryptedMessage;
@@ -1159,7 +1061,7 @@ describe("crypto", () => {
let [, , encryptedMessage] = await Promise.all([ let [, , encryptedMessage] = await Promise.all([
aliceClient.sendTextMessage(ROOM_ID, "test"), aliceClient.sendTextMessage(ROOM_ID, "test"),
expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession), expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession),
expectEncryptedSendMessage(), expectEncryptedSendMessageEvent(),
]); ]);
// Check that the session id exists // Check that the session id exists
@@ -1187,7 +1089,7 @@ describe("crypto", () => {
[, , encryptedMessage] = await Promise.all([ [, , encryptedMessage] = await Promise.all([
aliceClient.sendTextMessage(ROOM_ID, "test"), aliceClient.sendTextMessage(ROOM_ID, "test"),
expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession), expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession),
expectEncryptedSendMessage(), expectEncryptedSendMessageEvent(),
]); ]);
// Check that the new session id exists // Check that the new session id exists
@@ -1385,7 +1287,7 @@ describe("crypto", () => {
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession); const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession);
// and finally the megolm message // and finally the megolm message
const megolmMessagePromise = expectSendMegolmMessage(inboundGroupSessionPromise); const megolmMessagePromise = expectSendMegolmMessageEvent(inboundGroupSessionPromise);
// kick it off // kick it off
const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test"); const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test");
@@ -1408,7 +1310,7 @@ describe("crypto", () => {
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession); const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession);
// and finally the megolm message // and finally the megolm message
const megolmMessagePromise = expectSendMegolmMessage(inboundGroupSessionPromise); const megolmMessagePromise = expectSendMegolmMessageEvent(inboundGroupSessionPromise);
// kick it off // kick it off
const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test"); const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test");
@@ -2300,7 +2202,7 @@ describe("crypto", () => {
await syncPromise(client1); await syncPromise(client1);
// Send a message, and expect to get an `m.room.encrypted` event. // Send a message, and expect to get an `m.room.encrypted` event.
await Promise.all([client1.sendTextMessage(ROOM_ID, "test"), expectEncryptedSendMessage()]); await Promise.all([client1.sendTextMessage(ROOM_ID, "test"), expectEncryptedSendMessageEvent()]);
// We now replace the client, and allow the new one to resync, *without* the encryption event. // We now replace the client, and allow the new one to resync, *without* the encryption event.
client2 = await replaceClient(client1); client2 = await replaceClient(client1);
@@ -2321,7 +2223,7 @@ describe("crypto", () => {
// Send a message, and expect to get an `m.room.encrypted` event. // Send a message, and expect to get an `m.room.encrypted` event.
const [, msg1Content] = await Promise.all([ const [, msg1Content] = await Promise.all([
client1.sendTextMessage(ROOM_ID, "test1"), client1.sendTextMessage(ROOM_ID, "test1"),
expectEncryptedSendMessage(), expectEncryptedSendMessageEvent(),
]); ]);
// Replace the state with one which bumps the rotation period. This should be ignored, though it's not // Replace the state with one which bumps the rotation period. This should be ignored, though it's not
@@ -2340,12 +2242,12 @@ describe("crypto", () => {
// use a different one. // use a different one.
const [, msg2Content] = await Promise.all([ const [, msg2Content] = await Promise.all([
client1.sendTextMessage(ROOM_ID, "test2"), client1.sendTextMessage(ROOM_ID, "test2"),
expectEncryptedSendMessage(), expectEncryptedSendMessageEvent(),
]); ]);
expect(msg2Content.session_id).toEqual(msg1Content.session_id); expect(msg2Content.session_id).toEqual(msg1Content.session_id);
const [, msg3Content] = await Promise.all([ const [, msg3Content] = await Promise.all([
client1.sendTextMessage(ROOM_ID, "test3"), client1.sendTextMessage(ROOM_ID, "test3"),
expectEncryptedSendMessage(), expectEncryptedSendMessageEvent(),
]); ]);
expect(msg3Content.session_id).not.toEqual(msg1Content.session_id); expect(msg3Content.session_id).not.toEqual(msg1Content.session_id);
}); });
@@ -2357,7 +2259,7 @@ describe("crypto", () => {
await syncPromise(client1); await syncPromise(client1);
// Send a message, and expect to get an `m.room.encrypted` event. // Send a message, and expect to get an `m.room.encrypted` event.
await Promise.all([client1.sendTextMessage(ROOM_ID, "test1"), expectEncryptedSendMessage()]); await Promise.all([client1.sendTextMessage(ROOM_ID, "test1"), expectEncryptedSendMessageEvent()]);
// We now replace the client, and allow the new one to resync with a *different* encryption event. // We now replace the client, and allow the new one to resync with a *different* encryption event.
client2 = await replaceClient(client1); client2 = await replaceClient(client1);

View File

@@ -16,6 +16,7 @@ limitations under the License.
import Olm from "@matrix-org/olm"; import Olm from "@matrix-org/olm";
import anotherjson from "another-json"; import anotherjson from "another-json";
import fetchMock from "fetch-mock-jest";
import { import {
type IContent, type IContent,
@@ -30,6 +31,8 @@ import { type IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { type ISyncResponder } from "../../test-utils/SyncResponder"; import { type ISyncResponder } from "../../test-utils/SyncResponder";
import { syncPromise } from "../../test-utils/test-utils"; import { syncPromise } from "../../test-utils/test-utils";
import { type KeyBackupInfo } from "../../../src/crypto-api"; import { type KeyBackupInfo } from "../../../src/crypto-api";
import { logger } from "../../../src/logger";
import type FetchMock from "fetch-mock";
/** /**
* @module * @module
@@ -302,6 +305,7 @@ export function encryptMegolmEventRawPlainText(opts: {
}, },
type: "m.room.encrypted", type: "m.room.encrypted",
unsigned: {}, unsigned: {},
state_key: opts.plaintext.state_key ? `${opts.plaintext.type}:${opts.plaintext.state_key}` : undefined,
}; };
} }
@@ -414,3 +418,148 @@ export async function establishOlmSession(
await syncPromise(testClient); await syncPromise(testClient);
return p2pSession; return p2pSession;
} }
/**
* Expect that the client shares keys with the given recipient
*
* Waits for an HTTP request to send the encrypted m.room_key to-device message; decrypts it and uses it
* to establish an Olm InboundGroupSession.
*
* @param recipientUserID - the user id of the expected recipient
*
* @param recipientOlmAccount - Olm.Account for the recipient
*
* @param recipientOlmSession - an Olm.Session for the recipient, which must already have exchanged pre-key
* messages with the sender. Alternatively, null, in which case we will expect a pre-key message.
*
* @returns the established inbound group session
*/
export async function expectSendRoomKey(
recipientUserID: string,
recipientOlmAccount: Olm.Account,
recipientOlmSession: Olm.Session | null = null,
): Promise<Olm.InboundGroupSession> {
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
function onSendRoomKey(content: any): Olm.InboundGroupSession {
const m = content.messages[recipientUserID].DEVICE_ID;
const ct = m.ciphertext[testRecipientKey];
if (!recipientOlmSession) {
expect(ct.type).toEqual(0); // pre-key message
recipientOlmSession = new Olm.Session();
recipientOlmSession.create_inbound(recipientOlmAccount, ct.body);
} else {
expect(ct.type).toEqual(1); // regular message
}
const decrypted = JSON.parse(recipientOlmSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual("m.room_key");
const inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
return inboundGroupSession;
}
return await new Promise<Olm.InboundGroupSession>((resolve) => {
fetchMock.putOnce(
new RegExp("/sendToDevice/m.room.encrypted/"),
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const content = JSON.parse(opts.body as string);
resolve(onSendRoomKey(content));
return {};
},
{
// append to the list of intercepts on this path (since we have some tests that call
// this function multiple times)
overwriteRoutes: false,
},
);
});
}
/**
* 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 the encrypted event
*/
export function expectEncryptedSendMessageEvent() {
return new Promise<IContent>((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 },
);
});
}
/**
* Return the event received on rooms/{roomId}/state/m.room.encrypted/{stateKey} endpoint.
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
* @returns the content of the encrypted event
*/
function expectEncryptedSendStateEvent() {
return new Promise<IContent>((resolve) => {
fetchMock.putOnce(
new RegExp("/state/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 message event
*
* Waits for an HTTP request to send an encrypted message in the test room.
*
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
*
* @returns The content of the successfully-decrypted event
*/
export async function expectSendMegolmMessageEvent(
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
): Promise<Partial<IEvent>> {
const encryptedMessageContent = await expectEncryptedSendMessageEvent();
// 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;
const r: any = inboundGroupSession.decrypt(encryptedMessageContent!.ciphertext);
logger.log("Decrypted received megolm message", r);
return JSON.parse(r.plaintext);
}
/**
* Expect that the client sends an encrypted state event
*
* Waits for an HTTP request to send an encrypted state event in the test room.
*
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
*
* @returns The content of the successfully-decrypted state event
*/
export async function expectSendMegolmStateEvent(
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
): Promise<Partial<IEvent>> {
const encryptedStateContent = await expectEncryptedSendStateEvent();
// 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;
const r: any = inboundGroupSession.decrypt(encryptedStateContent!.ciphertext);
logger.log("Decrypted received megolm state event", r);
return JSON.parse(r.plaintext);
}

View File

@@ -0,0 +1,219 @@
/*
Copyright 2025 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 anotherjson from "another-json";
import fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import Olm from "@matrix-org/olm";
import * as testUtils from "../../test-utils/test-utils";
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
import { TEST_ROOM_ID as ROOM_ID } from "../../test-utils/test-data";
import { logger } from "../../../src/logger";
import { createClient, PendingEventOrdering, type IStartClientOpts, type MatrixClient } from "../../../src/matrix";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { type ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
import {
createOlmAccount,
createOlmSession,
encryptGroupSessionKey,
encryptMegolmEvent,
getTestOlmAccountKeys,
expectSendRoomKey,
expectSendMegolmStateEvent,
} from "./olm-utils";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
describe("Encrypted State Events", () => {
let testOlmAccount = {} as unknown as Olm.Account;
let testSenderKey = "";
/** the MatrixClient under test */
let aliceClient: MatrixClient;
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */
let keyReceiver: E2EKeyReceiver;
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
let syncResponder: ISyncResponder;
async function startClientAndAwaitFirstSync(opts: IStartClientOpts = {}): Promise<void> {
logger.log(aliceClient.getUserId() + ": starting");
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
syncResponder.sendOrQueueSyncResponse({ next_batch: 1 });
aliceClient.startClient({
// set this so that we can get hold of failed events
pendingEventOrdering: PendingEventOrdering.Detached,
...opts,
});
await syncPromise(aliceClient);
logger.log(aliceClient.getUserId() + ": started");
}
beforeEach(async () => {
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: "@alice:localhost",
accessToken: "akjgkrgjs",
deviceId: "xzcvb",
logger: logger.getChild("aliceClient"),
enableEncryptedStateEvents: true,
});
keyReceiver = new E2EKeyReceiver(homeserverUrl);
syncResponder = new SyncResponder(homeserverUrl);
await aliceClient.initRustCrypto();
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
testOlmAccount = await createOlmAccount();
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
testSenderKey = testE2eKeys.curve25519;
}, 10000);
afterEach(async () => {
await aliceClient.stopClient();
await jest.runAllTimersAsync();
fetchMock.mockReset();
});
function expectAliceKeyQuery(response: any) {
fetchMock.postOnce(new RegExp("/keys/query"), (url: string, opts: RequestInit) => response, {
overwriteRoutes: false,
});
}
function expectAliceKeyClaim(response: any) {
fetchMock.postOnce(new RegExp("/keys/claim"), response);
}
function getTestKeysClaimResponse(userId: string) {
testOlmAccount.generate_one_time_keys(1);
const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
testOlmAccount.mark_keys_as_published();
const keyId = Object.keys(testOneTimeKeys.curve25519)[0];
const oneTimeKey: string = testOneTimeKeys.curve25519[keyId];
const unsignedKeyResult = { key: oneTimeKey };
const j = anotherjson.stringify(unsignedKeyResult);
const sig = testOlmAccount.sign(j);
const keyResult = {
...unsignedKeyResult,
signatures: { [userId]: { "ed25519:DEVICE_ID": sig } },
};
return {
one_time_keys: { [userId]: { DEVICE_ID: { ["signed_curve25519:" + keyId]: keyResult } } },
failures: {},
};
}
it("Should receive an encrypted state event", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: keyReceiver.getDeviceKey(),
recipientEd25519Key: keyReceiver.getSigningKey(),
olmAccount: testOlmAccount,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a state event with the group session
const eventEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
plaintext: {
type: "m.room.topic",
state_key: "",
content: {
topic: "Secret!",
},
},
});
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {
[ROOM_ID]: { timeline: { events: [eventEncrypted] } },
},
},
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
// it probably won't be decrypted yet, because it takes a while to process the olm keys
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(decryptedEvent.getContent().topic).toEqual("Secret!");
});
it("Should send an encrypted state event", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl);
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
keyResponder.addDeviceKeys(testDeviceKeys);
await startClientAndAwaitFirstSync();
// Alice shares a room with Bob
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"], ROOM_ID, true));
await syncPromise(aliceClient);
// ... and claim one of Bob's OTKs ...
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));
// ... and send an m.room.topic message
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
await Promise.all([
aliceClient.setRoomTopic(ROOM_ID, "Secret!"),
expectSendMegolmStateEvent(inboundGroupSessionPromise),
]);
});
});

View File

@@ -63,7 +63,11 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
* *
* @returns the sync response * @returns the sync response
*/ */
export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): ISyncResponse { export function getSyncResponse(
roomMembers: string[],
roomId = TEST_ROOM_ID,
encryptStateEvents = false,
): ISyncResponse {
const roomResponse: IJoinedRoom = { const roomResponse: IJoinedRoom = {
summary: { summary: {
"m.heroes": [], "m.heroes": [],
@@ -77,7 +81,8 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
type: "m.room.encryption", type: "m.room.encryption",
state_key: "", state_key: "",
content: { content: {
algorithm: "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
"io.element.msc3414.encrypt_state_events": encryptStateEvents,
}, },
}), }),
], ],

View File

@@ -16,7 +16,7 @@ limitations under the License.
import { type MockedObject } from "jest-mock"; import { type MockedObject } from "jest-mock";
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event"; import { type IContent, MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
import { emitPromise } from "../../test-utils/test-utils"; import { emitPromise } from "../../test-utils/test-utils";
import { import {
type IAnnotatedPushRule, type IAnnotatedPushRule,
@@ -335,6 +335,31 @@ describe("MatrixEvent", () => {
} }
}); });
describe("state key packing", () => {
it("should pack the state key during encryption", () => {
const ev = createStateEvent("$event1:server", "m.room.topic", "", { topic: "" });
expect(ev.getStateKey()).toStrictEqual("");
ev.makeEncrypted("m.room.encrypted", { ciphertext: "xyz" }, "", "");
expect(ev.getStateKey()).toStrictEqual("");
expect(ev.getWireStateKey()).toStrictEqual("m.room.topic:");
const keyedEv = createStateEvent("$event2:server", "m.beacon_info", "@alice:server", {});
expect(keyedEv.getStateKey()).toStrictEqual("@alice:server");
keyedEv.makeEncrypted("m.room.encrypted", { ciphertext: "xyz" }, "", "");
expect(keyedEv.getStateKey()).toStrictEqual("@alice:server");
expect(keyedEv.getWireStateKey()).toStrictEqual("m.beacon_info:@alice:server");
});
function createStateEvent(eventId: string, type: string, stateKey: string, content?: IContent): MatrixEvent {
return new MatrixEvent({
type,
state_key: stateKey,
content,
event_id: eventId,
});
}
});
describe("applyVisibilityEvent", () => { describe("applyVisibilityEvent", () => {
it("should emit VisibilityChange if a change was made", async () => { it("should emit VisibilityChange if a change was made", async () => {
const ev = new MatrixEvent({ const ev = new MatrixEvent({

View File

@@ -72,6 +72,7 @@ describe("RoomEncryptor", () => {
body: text, body: text,
msgtype: "m.text", msgtype: "m.text",
}), }),
isState: () => false,
makeEncrypted: jest.fn().mockReturnValue(undefined), makeEncrypted: jest.fn().mockReturnValue(undefined),
} as unknown as Mocked<MatrixEvent>; } as unknown as Mocked<MatrixEvent>;
} }

View File

@@ -830,6 +830,7 @@ describe("RustCrypto", () => {
TEST_DEVICE_ID, TEST_DEVICE_ID,
secretStorage, secretStorage,
{} as CryptoCallbacks, {} as CryptoCallbacks,
false,
); );
async function createSecretStorageKey() { async function createSecretStorageKey() {

View File

@@ -105,9 +105,10 @@ export interface RoomPinnedEventsEventContent {
} }
export interface RoomEncryptionEventContent { export interface RoomEncryptionEventContent {
algorithm: "m.megolm.v1.aes-sha2"; "algorithm": "m.megolm.v1.aes-sha2";
rotation_period_ms?: number; "io.element.msc3414.encrypt_state_events"?: boolean;
rotation_period_msgs?: number; "rotation_period_ms"?: number;
"rotation_period_msgs"?: number;
} }
export interface RoomHistoryVisibilityEventContent { export interface RoomHistoryVisibilityEventContent {

View File

@@ -425,6 +425,11 @@ export interface ICreateClientOpts {
*/ */
cryptoCallbacks?: CryptoCallbacks; cryptoCallbacks?: CryptoCallbacks;
/**
* Enable encrypted state events.
*/
enableEncryptedStateEvents?: boolean;
/** /**
* Method to generate room names for empty rooms and rooms names based on membership. * Method to generate room names for empty rooms and rooms names based on membership.
* Defaults to a built-in English handler with basic pluralisation. * Defaults to a built-in English handler with basic pluralisation.
@@ -1205,6 +1210,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public http: MatrixHttpApi<IHttpOpts & { onlyData: true }>; // XXX: Intended private, used in code. public http: MatrixHttpApi<IHttpOpts & { onlyData: true }>; // XXX: Intended private, used in code.
private cryptoBackend?: CryptoBackend; // one of crypto or rustCrypto private cryptoBackend?: CryptoBackend; // one of crypto or rustCrypto
private readonly enableEncryptedStateEvents: boolean;
public cryptoCallbacks: CryptoCallbacks; // XXX: Intended private, used in code. public cryptoCallbacks: CryptoCallbacks; // XXX: Intended private, used in code.
public callEventHandler?: CallEventHandler; // XXX: Intended private, used in code. public callEventHandler?: CallEventHandler; // XXX: Intended private, used in code.
public groupCallEventHandler?: GroupCallEventHandler; public groupCallEventHandler?: GroupCallEventHandler;
@@ -1363,6 +1369,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.legacyCryptoStore = opts.cryptoStore; this.legacyCryptoStore = opts.cryptoStore;
this.verificationMethods = opts.verificationMethods; this.verificationMethods = opts.verificationMethods;
this.cryptoCallbacks = opts.cryptoCallbacks || {}; this.cryptoCallbacks = opts.cryptoCallbacks || {};
this.enableEncryptedStateEvents = opts.enableEncryptedStateEvents ?? false;
this.forceTURN = opts.forceTURN || false; this.forceTURN = opts.forceTURN || false;
this.iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize; this.iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize;
@@ -1979,6 +1986,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
legacyMigrationProgressListener: (progress: number, total: number): void => { legacyMigrationProgressListener: (progress: number, total: number): void => {
this.emit(CryptoEvent.LegacyCryptoStoreMigrationProgress, progress, total); this.emit(CryptoEvent.LegacyCryptoStoreMigrationProgress, progress, total);
}, },
enableEncryptedStateEvents: this.enableEncryptedStateEvents,
}); });
rustCrypto.setSupportedVerificationMethods(this.verificationMethods); rustCrypto.setSupportedVerificationMethods(this.verificationMethods);
@@ -6034,6 +6043,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns A decryption promise * @returns A decryption promise
*/ */
public decryptEventIfNeeded(event: MatrixEvent, options?: IDecryptOptions): Promise<void> { public decryptEventIfNeeded(event: MatrixEvent, options?: IDecryptOptions): Promise<void> {
if (event.isState() && !this.enableEncryptedStateEvents) {
return Promise.resolve();
}
if (event.shouldAttemptDecryption() && this.getCrypto()) { if (event.shouldAttemptDecryption() && this.getCrypto()) {
event.attemptDecryption(this.cryptoBackend!, options); event.attemptDecryption(this.cryptoBackend!, options);
} }
@@ -6618,23 +6631,83 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: TODO * @returns Promise which resolves: TODO
* @returns Rejects: with an error response. * @returns Rejects: with an error response.
*/ */
public sendStateEvent<K extends keyof StateEvents>( public async sendStateEvent<K extends keyof StateEvents>(
roomId: string, roomId: string,
eventType: K, eventType: K,
content: StateEvents[K], content: StateEvents[K],
stateKey = "", stateKey = "",
opts: IRequestOpts = {}, opts: IRequestOpts = {},
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
const room = this.getRoom(roomId);
const event = new MatrixEvent({
room_id: roomId,
type: eventType,
state_key: stateKey,
// Cast safety: StateEvents[K] is a stronger bound than IContent, which has [key: string]: any
content: content as IContent,
});
await this.encryptStateEventIfNeeded(event, room ?? undefined);
const pathParams = { const pathParams = {
$roomId: roomId, $roomId: roomId,
$eventType: eventType, $eventType: event.getWireType(),
$stateKey: stateKey, $stateKey: event.getWireStateKey(),
}; };
let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams); let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
if (stateKey !== undefined) { if (stateKey !== undefined) {
path = utils.encodeUri(path + "/$stateKey", pathParams); path = utils.encodeUri(path + "/$stateKey", pathParams);
} }
return this.http.authedRequest(Method.Put, path, undefined, content as Body, opts); return this.http.authedRequest(Method.Put, path, undefined, event.getWireContent(), opts);
}
private async encryptStateEventIfNeeded(event: MatrixEvent, room?: Room): Promise<void> {
if (!this.enableEncryptedStateEvents) {
return;
}
// If the room is unknown, we cannot encrypt for it
if (!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 set up
// crypto. This is useful for users of matrix-org/pantalaimon
return;
}
if (!this.cryptoBackend) {
throw new Error("This room is configured to use encryption, but your client does not support encryption.");
}
// Check regular encryption conditions.
if (!(await this.shouldEncryptEventForRoom(event, room))) {
return;
}
// If the crypto impl thinks we shouldn't encrypt, then we shouldn't.
// Safety: we checked the crypto impl exists above.
if (!(await this.cryptoBackend!.isStateEncryptionEnabledInRoom(room.roomId))) {
return;
}
// Check if the event is excluded under MSC3414
if (
[
"m.room.create",
"m.room.member",
"m.room.join_rules",
"m.room.power_levels",
"m.room.third_party_invite",
"m.room.history_visibility",
"m.room.guest_access",
"m.room.encryption",
].includes(event.getType())
) {
return;
}
await this.cryptoBackend.encryptEvent(event, room);
} }
/** /**

View File

@@ -118,6 +118,11 @@ export interface CryptoApi {
*/ */
isEncryptionEnabledInRoom(roomId: string): Promise<boolean>; isEncryptionEnabledInRoom(roomId: string): Promise<boolean>;
/**
* Check if we believe the given room supports encrypted state events.
*/
isStateEncryptionEnabledInRoom(roomId: string): Promise<boolean>;
/** /**
* Perform any background tasks that can be done before a message is ready to * Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message. * send, in order to speed up sending of the message.

View File

@@ -158,6 +158,7 @@ export interface IMarkedUnreadEvent {
export interface IClearEvent { export interface IClearEvent {
room_id?: string; room_id?: string;
type: string; type: string;
state_key?: string;
content: Omit<IContent, "membership" | "avatar_url" | "displayname" | "m.relates_to">; content: Omit<IContent, "membership" | "avatar_url" | "displayname" | "m.relates_to">;
unsigned?: IUnsigned; unsigned?: IUnsigned;
} }
@@ -728,11 +729,25 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
} }
/** /**
* Get the event state_key if it has one. This will return <code>undefined * Get the event state_key if it has one. If necessary, this will perform
* </code> for message events. * string-unpacking on the state key, as per MSC3414. This will return
* <code>undefined</code> for message events.
* @returns The event's `state_key`. * @returns The event's `state_key`.
*/ */
public getStateKey(): string | undefined { public getStateKey(): string | undefined {
if (this.clearEvent) {
return this.clearEvent.state_key;
}
return this.event.state_key;
}
/**
* Get the raw event state_key if it has one. This may be string-packed as per
* MSC3414 if the state event is encrypted. This will return <code>undefined
* </code> for message events.
* @returns The event's `state_key`.
*/
public getWireStateKey(): string | undefined {
return this.event.state_key; return this.event.state_key;
} }
@@ -785,11 +800,17 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
this.clearEvent = { this.clearEvent = {
type: this.event.type!, type: this.event.type!,
content: this.event.content!, content: this.event.content!,
state_key: this.event.state_key,
}; };
this.event.type = cryptoType; this.event.type = cryptoType;
this.event.content = cryptoContent; this.event.content = cryptoContent;
this.senderCurve25519Key = senderCurve25519Key; this.senderCurve25519Key = senderCurve25519Key;
this.claimedEd25519Key = claimedEd25519Key; this.claimedEd25519Key = claimedEd25519Key;
// if this is a state event, pack cleartext type and statekey
if (this.isState()) {
this.event.state_key = `${this.clearEvent!.type}:${this.clearEvent!.state_key}`;
}
} }
/** /**
@@ -1025,7 +1046,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* @returns True if this event is encrypted. * @returns True if this event is encrypted.
*/ */
public isEncrypted(): boolean { public isEncrypted(): boolean {
return !this.isState() && this.event.type === EventType.RoomMessageEncrypted; return this.event.type === EventType.RoomMessageEncrypted;
} }
/** /**

View File

@@ -27,7 +27,7 @@ import {
} from "@matrix-org/matrix-sdk-crypto-wasm"; } from "@matrix-org/matrix-sdk-crypto-wasm";
import { EventType } from "../@types/event.ts"; import { EventType } from "../@types/event.ts";
import { type IContent, type MatrixEvent } from "../models/event.ts"; import { type MatrixEvent, type IContent } from "../models/event.ts";
import { type Room } from "../models/room.ts"; import { type Room } from "../models/room.ts";
import { type Logger, LogSpan } from "../logger.ts"; import { type Logger, LogSpan } from "../logger.ts";
import { type KeyClaimManager } from "./KeyClaimManager.ts"; import { type KeyClaimManager } from "./KeyClaimManager.ts";
@@ -314,11 +314,23 @@ export class RoomEncryptor {
private async encryptEventInner(logger: LogSpan, event: MatrixEvent): Promise<void> { private async encryptEventInner(logger: LogSpan, event: MatrixEvent): Promise<void> {
logger.debug("Encrypting actual message content"); logger.debug("Encrypting actual message content");
const encryptedContent = await this.olmMachine.encryptRoomEvent(
new RoomId(this.room.roomId), const room = new RoomId(this.room.roomId);
event.getType(), const type = event.getType();
JSON.stringify(event.getContent()), const content = JSON.stringify(event.getContent());
);
let encryptedContent;
if (event.isState()) {
encryptedContent = await this.olmMachine.encryptStateEvent(
room,
type,
// Safety: we've already checked above that this is a state event, so the state key must exist.
event.getStateKey()!,
content,
);
} else {
encryptedContent = await this.olmMachine.encryptRoomEvent(room, type, content);
}
event.makeEncrypted( event.makeEncrypted(
EventType.RoomMessageEncrypted, EventType.RoomMessageEncrypted,

View File

@@ -91,6 +91,11 @@ export async function initRustCrypto(args: {
* Called with (-1, -1) to mark the end of migration. * Called with (-1, -1) to mark the end of migration.
*/ */
legacyMigrationProgressListener?: (progress: number, total: number) => void; legacyMigrationProgressListener?: (progress: number, total: number) => void;
/**
* Whether to enable support for encrypting state events.
*/
enableEncryptedStateEvents?: boolean;
}): Promise<RustCrypto> { }): Promise<RustCrypto> {
const { logger } = args; const { logger } = args;
@@ -128,6 +133,7 @@ export async function initRustCrypto(args: {
args.cryptoCallbacks, args.cryptoCallbacks,
storeHandle, storeHandle,
args.legacyCryptoStore, args.legacyCryptoStore,
args.enableEncryptedStateEvents,
); );
storeHandle.free(); storeHandle.free();
@@ -145,6 +151,7 @@ async function initOlmMachine(
cryptoCallbacks: CryptoCallbacks, cryptoCallbacks: CryptoCallbacks,
storeHandle: StoreHandle, storeHandle: StoreHandle,
legacyCryptoStore?: CryptoStore, legacyCryptoStore?: CryptoStore,
enableEncryptedStateEvents?: boolean,
): Promise<RustCrypto> { ): Promise<RustCrypto> {
logger.debug("Init OlmMachine"); logger.debug("Init OlmMachine");
@@ -167,7 +174,16 @@ async function initOlmMachine(
// Disable room key requests, per https://github.com/vector-im/element-web/issues/26524. // Disable room key requests, per https://github.com/vector-im/element-web/issues/26524.
olmMachine.roomKeyRequestsEnabled = false; olmMachine.roomKeyRequestsEnabled = false;
const rustCrypto = new RustCrypto(logger, olmMachine, http, userId, deviceId, secretStorage, cryptoCallbacks); const rustCrypto = new RustCrypto(
logger,
olmMachine,
http,
userId,
deviceId,
secretStorage,
cryptoCallbacks,
enableEncryptedStateEvents,
);
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) => await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
rustCrypto.onRoomKeysUpdated(sessions), rustCrypto.onRoomKeysUpdated(sessions),

View File

@@ -162,6 +162,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
/** Crypto callbacks provided by the application */ /** Crypto callbacks provided by the application */
private readonly cryptoCallbacks: CryptoCallbacks, private readonly cryptoCallbacks: CryptoCallbacks,
/** Enable support for encrypted state events under MSC3414. */
private readonly enableEncryptedStateEvents: boolean = false,
) { ) {
super(); super();
this.outgoingRequestProcessor = new OutgoingRequestProcessor(logger, olmMachine, http); this.outgoingRequestProcessor = new OutgoingRequestProcessor(logger, olmMachine, http);
@@ -421,6 +424,16 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
return Boolean(roomSettings?.algorithm); return Boolean(roomSettings?.algorithm);
} }
/**
* Implementation of {@link CryptoApi#isStateEncryptionEnabledInRoom}.
*/
public async isStateEncryptionEnabledInRoom(roomId: string): Promise<boolean> {
const roomSettings: RustSdkCryptoJs.RoomSettings | undefined = await this.olmMachine.getRoomSettings(
new RustSdkCryptoJs.RoomId(roomId),
);
return Boolean(roomSettings?.encryptStateEvents);
}
/** /**
* Implementation of {@link CryptoApi#getOwnDeviceKeys}. * Implementation of {@link CryptoApi#getOwnDeviceKeys}.
*/ */
@@ -1717,7 +1730,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
await this.receiveSyncChanges({ devices }); await this.receiveSyncChanges({ devices });
} }
/** called by the sync loop on m.room.encrypted events /** called by the sync loop on m.room.encryption events
* *
* @param room - in which the event was received * @param room - in which the event was received
* @param event - encryption event to be processed * @param event - encryption event to be processed
@@ -1734,6 +1747,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
return; return;
} }
if (config["io.element.msc3414.encrypt_state_events"] && this.enableEncryptedStateEvents) {
this.logger.info("crypto Enabling state event encryption...");
settings.encryptStateEvents = true;
}
try { try {
settings.sessionRotationPeriodMs = config.rotation_period_ms; settings.sessionRotationPeriodMs = config.rotation_period_ms;
settings.sessionRotationPeriodMessages = config.rotation_period_msgs; settings.sessionRotationPeriodMessages = config.rotation_period_msgs;

View File

@@ -1366,6 +1366,13 @@ export class SyncApi {
} }
} }
// Proactively decrypt state events: normally we decrypt on demand, but for state
// events we need them immediately, so we handle them here. Specifically, consumers
// (e.g. Element Web) expect state events to be unencrypted upon receipt.
for (const ev of timelineEvents.filter((ev) => ev.isState())) {
await this.client.decryptEventIfNeeded(ev);
}
try { try {
if ("org.matrix.msc4222.state_after" in joinObj) { if ("org.matrix.msc4222.state_after" in joinObj) {
await this.injectRoomEvents( await this.injectRoomEvents(

View File

@@ -1763,7 +1763,7 @@
"@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@jridgewell/sourcemap-codec" "^1.4.14"
"@matrix-org/matrix-sdk-crypto-wasm@^15.2.0": "@matrix-org/matrix-sdk-crypto-wasm@^15.3.0":
version "15.3.0" version "15.3.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-15.3.0.tgz#141fd041ae382b793369bcee4394b0b577bdea0c" resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-15.3.0.tgz#141fd041ae382b793369bcee4394b0b577bdea0c"
integrity sha512-QyxHvncvkl7nf+tnn92PjQ54gMNV8hMSpiukiDgNrqF6IYwgySTlcSdkPYdw8QjZJ0NR6fnVrNzMec0OohM3wA== integrity sha512-QyxHvncvkl7nf+tnn92PjQ54gMNV8hMSpiukiDgNrqF6IYwgySTlcSdkPYdw8QjZJ0NR6fnVrNzMec0OohM3wA==
@@ -2314,12 +2314,7 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.21.0.tgz#58f30aec8db8212fd886835dc5969cdf47cb29f5" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.21.0.tgz#58f30aec8db8212fd886835dc5969cdf47cb29f5"
integrity sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A== integrity sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==
"@typescript-eslint/types@8.44.0", "@typescript-eslint/types@^8.44.0": "@typescript-eslint/types@8.44.0", "@typescript-eslint/types@^8.41.0", "@typescript-eslint/types@^8.44.0":
version "8.44.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.44.0.tgz#4b9154ab164a0beff22d3217ff0fdc8d10bce924"
integrity sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==
"@typescript-eslint/types@^8.41.0":
version "8.44.0" version "8.44.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.44.0.tgz#4b9154ab164a0beff22d3217ff0fdc8d10bce924" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.44.0.tgz#4b9154ab164a0beff22d3217ff0fdc8d10bce924"
integrity sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA== integrity sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==