You've already forked matrix-js-sdk
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:
@@ -49,7 +49,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
|
||||
@@ -88,7 +88,10 @@ import {
|
||||
encryptMegolmEventRawPlainText,
|
||||
establishOlmSession,
|
||||
getTestOlmAccountKeys,
|
||||
} from "./olm-utils";
|
||||
expectSendRoomKey,
|
||||
expectSendMegolmMessageEvent,
|
||||
expectEncryptedSendMessageEvent,
|
||||
} from "./olm-utils.ts";
|
||||
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
|
||||
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
@@ -104,107 +107,6 @@ afterEach(() => {
|
||||
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", () => {
|
||||
let testOlmAccount = {} as unknown as Olm.Account;
|
||||
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.
|
||||
await Promise.all([
|
||||
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.
|
||||
await Promise.all([
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
expectSendMegolmMessage(inboundGroupSessionPromise),
|
||||
expectSendMegolmMessageEvent(inboundGroupSessionPromise),
|
||||
]);
|
||||
|
||||
// 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.
|
||||
const inboundGroupSessionPromise2 = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
const p2 = expectSendMegolmMessage(inboundGroupSessionPromise2);
|
||||
const p2 = expectSendMegolmMessageEvent(inboundGroupSessionPromise2);
|
||||
await Promise.all([aliceClient.sendTextMessage(ROOM_ID, "test2"), p2]);
|
||||
});
|
||||
|
||||
@@ -1037,7 +939,7 @@ describe("crypto", () => {
|
||||
*/
|
||||
async function sendEncryptedMessage(): Promise<IContent> {
|
||||
const [encryptedMessage] = await Promise.all([
|
||||
expectEncryptedSendMessage(),
|
||||
expectEncryptedSendMessageEvent(),
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
]);
|
||||
return encryptedMessage;
|
||||
@@ -1159,7 +1061,7 @@ describe("crypto", () => {
|
||||
let [, , encryptedMessage] = await Promise.all([
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession),
|
||||
expectEncryptedSendMessage(),
|
||||
expectEncryptedSendMessageEvent(),
|
||||
]);
|
||||
|
||||
// Check that the session id exists
|
||||
@@ -1187,7 +1089,7 @@ describe("crypto", () => {
|
||||
[, , encryptedMessage] = await Promise.all([
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession),
|
||||
expectEncryptedSendMessage(),
|
||||
expectEncryptedSendMessageEvent(),
|
||||
]);
|
||||
|
||||
// Check that the new session id exists
|
||||
@@ -1385,7 +1287,7 @@ describe("crypto", () => {
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession);
|
||||
|
||||
// and finally the megolm message
|
||||
const megolmMessagePromise = expectSendMegolmMessage(inboundGroupSessionPromise);
|
||||
const megolmMessagePromise = expectSendMegolmMessageEvent(inboundGroupSessionPromise);
|
||||
|
||||
// kick it off
|
||||
const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test");
|
||||
@@ -1408,7 +1310,7 @@ describe("crypto", () => {
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession);
|
||||
|
||||
// and finally the megolm message
|
||||
const megolmMessagePromise = expectSendMegolmMessage(inboundGroupSessionPromise);
|
||||
const megolmMessagePromise = expectSendMegolmMessageEvent(inboundGroupSessionPromise);
|
||||
|
||||
// kick it off
|
||||
const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test");
|
||||
@@ -2300,7 +2202,7 @@ describe("crypto", () => {
|
||||
await syncPromise(client1);
|
||||
|
||||
// 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.
|
||||
client2 = await replaceClient(client1);
|
||||
@@ -2321,7 +2223,7 @@ describe("crypto", () => {
|
||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||
const [, msg1Content] = await Promise.all([
|
||||
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
|
||||
@@ -2340,12 +2242,12 @@ describe("crypto", () => {
|
||||
// use a different one.
|
||||
const [, msg2Content] = await Promise.all([
|
||||
client1.sendTextMessage(ROOM_ID, "test2"),
|
||||
expectEncryptedSendMessage(),
|
||||
expectEncryptedSendMessageEvent(),
|
||||
]);
|
||||
expect(msg2Content.session_id).toEqual(msg1Content.session_id);
|
||||
const [, msg3Content] = await Promise.all([
|
||||
client1.sendTextMessage(ROOM_ID, "test3"),
|
||||
expectEncryptedSendMessage(),
|
||||
expectEncryptedSendMessageEvent(),
|
||||
]);
|
||||
expect(msg3Content.session_id).not.toEqual(msg1Content.session_id);
|
||||
});
|
||||
@@ -2357,7 +2259,7 @@ describe("crypto", () => {
|
||||
await syncPromise(client1);
|
||||
|
||||
// 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.
|
||||
client2 = await replaceClient(client1);
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import Olm from "@matrix-org/olm";
|
||||
import anotherjson from "another-json";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import {
|
||||
type IContent,
|
||||
@@ -30,6 +31,8 @@ import { type IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { type ISyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { syncPromise } from "../../test-utils/test-utils";
|
||||
import { type KeyBackupInfo } from "../../../src/crypto-api";
|
||||
import { logger } from "../../../src/logger";
|
||||
import type FetchMock from "fetch-mock";
|
||||
|
||||
/**
|
||||
* @module
|
||||
@@ -302,6 +305,7 @@ export function encryptMegolmEventRawPlainText(opts: {
|
||||
},
|
||||
type: "m.room.encrypted",
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
219
spec/integ/crypto/state-events.spec.ts
Normal file
219
spec/integ/crypto/state-events.spec.ts
Normal 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),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -63,7 +63,11 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
*
|
||||
* @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 = {
|
||||
summary: {
|
||||
"m.heroes": [],
|
||||
@@ -77,7 +81,8 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"io.element.msc3414.encrypt_state_events": encryptStateEvents,
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
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 {
|
||||
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", () => {
|
||||
it("should emit VisibilityChange if a change was made", async () => {
|
||||
const ev = new MatrixEvent({
|
||||
|
||||
@@ -72,6 +72,7 @@ describe("RoomEncryptor", () => {
|
||||
body: text,
|
||||
msgtype: "m.text",
|
||||
}),
|
||||
isState: () => false,
|
||||
makeEncrypted: jest.fn().mockReturnValue(undefined),
|
||||
} as unknown as Mocked<MatrixEvent>;
|
||||
}
|
||||
|
||||
@@ -830,6 +830,7 @@ describe("RustCrypto", () => {
|
||||
TEST_DEVICE_ID,
|
||||
secretStorage,
|
||||
{} as CryptoCallbacks,
|
||||
false,
|
||||
);
|
||||
|
||||
async function createSecretStorageKey() {
|
||||
|
||||
@@ -105,9 +105,10 @@ export interface RoomPinnedEventsEventContent {
|
||||
}
|
||||
|
||||
export interface RoomEncryptionEventContent {
|
||||
algorithm: "m.megolm.v1.aes-sha2";
|
||||
rotation_period_ms?: number;
|
||||
rotation_period_msgs?: number;
|
||||
"algorithm": "m.megolm.v1.aes-sha2";
|
||||
"io.element.msc3414.encrypt_state_events"?: boolean;
|
||||
"rotation_period_ms"?: number;
|
||||
"rotation_period_msgs"?: number;
|
||||
}
|
||||
|
||||
export interface RoomHistoryVisibilityEventContent {
|
||||
|
||||
@@ -425,6 +425,11 @@ export interface ICreateClientOpts {
|
||||
*/
|
||||
cryptoCallbacks?: CryptoCallbacks;
|
||||
|
||||
/**
|
||||
* Enable encrypted state events.
|
||||
*/
|
||||
enableEncryptedStateEvents?: boolean;
|
||||
|
||||
/**
|
||||
* Method to generate room names for empty rooms and rooms names based on membership.
|
||||
* 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.
|
||||
|
||||
private cryptoBackend?: CryptoBackend; // one of crypto or rustCrypto
|
||||
private readonly enableEncryptedStateEvents: boolean;
|
||||
public cryptoCallbacks: CryptoCallbacks; // XXX: Intended private, used in code.
|
||||
public callEventHandler?: CallEventHandler; // XXX: Intended private, used in code.
|
||||
public groupCallEventHandler?: GroupCallEventHandler;
|
||||
@@ -1363,6 +1369,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.legacyCryptoStore = opts.cryptoStore;
|
||||
this.verificationMethods = opts.verificationMethods;
|
||||
this.cryptoCallbacks = opts.cryptoCallbacks || {};
|
||||
this.enableEncryptedStateEvents = opts.enableEncryptedStateEvents ?? false;
|
||||
|
||||
this.forceTURN = opts.forceTURN || false;
|
||||
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 => {
|
||||
this.emit(CryptoEvent.LegacyCryptoStoreMigrationProgress, progress, total);
|
||||
},
|
||||
|
||||
enableEncryptedStateEvents: this.enableEncryptedStateEvents,
|
||||
});
|
||||
|
||||
rustCrypto.setSupportedVerificationMethods(this.verificationMethods);
|
||||
@@ -6034,6 +6043,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns A decryption promise
|
||||
*/
|
||||
public decryptEventIfNeeded(event: MatrixEvent, options?: IDecryptOptions): Promise<void> {
|
||||
if (event.isState() && !this.enableEncryptedStateEvents) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (event.shouldAttemptDecryption() && this.getCrypto()) {
|
||||
event.attemptDecryption(this.cryptoBackend!, options);
|
||||
}
|
||||
@@ -6618,23 +6631,83 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public sendStateEvent<K extends keyof StateEvents>(
|
||||
public async sendStateEvent<K extends keyof StateEvents>(
|
||||
roomId: string,
|
||||
eventType: K,
|
||||
content: StateEvents[K],
|
||||
stateKey = "",
|
||||
opts: IRequestOpts = {},
|
||||
): 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 = {
|
||||
$roomId: roomId,
|
||||
$eventType: eventType,
|
||||
$stateKey: stateKey,
|
||||
$eventType: event.getWireType(),
|
||||
$stateKey: event.getWireStateKey(),
|
||||
};
|
||||
let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
|
||||
if (stateKey !== undefined) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -118,6 +118,11 @@ export interface CryptoApi {
|
||||
*/
|
||||
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
|
||||
* send, in order to speed up sending of the message.
|
||||
|
||||
@@ -158,6 +158,7 @@ export interface IMarkedUnreadEvent {
|
||||
export interface IClearEvent {
|
||||
room_id?: string;
|
||||
type: string;
|
||||
state_key?: string;
|
||||
content: Omit<IContent, "membership" | "avatar_url" | "displayname" | "m.relates_to">;
|
||||
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
|
||||
* </code> for message events.
|
||||
* Get the event state_key if it has one. If necessary, this will perform
|
||||
* string-unpacking on the state key, as per MSC3414. This will return
|
||||
* <code>undefined</code> for message events.
|
||||
* @returns The event's `state_key`.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -785,11 +800,17 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
this.clearEvent = {
|
||||
type: this.event.type!,
|
||||
content: this.event.content!,
|
||||
state_key: this.event.state_key,
|
||||
};
|
||||
this.event.type = cryptoType;
|
||||
this.event.content = cryptoContent;
|
||||
this.senderCurve25519Key = senderCurve25519Key;
|
||||
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.
|
||||
*/
|
||||
public isEncrypted(): boolean {
|
||||
return !this.isState() && this.event.type === EventType.RoomMessageEncrypted;
|
||||
return this.event.type === EventType.RoomMessageEncrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
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 Logger, LogSpan } from "../logger.ts";
|
||||
import { type KeyClaimManager } from "./KeyClaimManager.ts";
|
||||
@@ -314,11 +314,23 @@ export class RoomEncryptor {
|
||||
|
||||
private async encryptEventInner(logger: LogSpan, event: MatrixEvent): Promise<void> {
|
||||
logger.debug("Encrypting actual message content");
|
||||
const encryptedContent = await this.olmMachine.encryptRoomEvent(
|
||||
new RoomId(this.room.roomId),
|
||||
event.getType(),
|
||||
JSON.stringify(event.getContent()),
|
||||
);
|
||||
|
||||
const room = new RoomId(this.room.roomId);
|
||||
const type = event.getType();
|
||||
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(
|
||||
EventType.RoomMessageEncrypted,
|
||||
|
||||
@@ -91,6 +91,11 @@ export async function initRustCrypto(args: {
|
||||
* Called with (-1, -1) to mark the end of migration.
|
||||
*/
|
||||
legacyMigrationProgressListener?: (progress: number, total: number) => void;
|
||||
|
||||
/**
|
||||
* Whether to enable support for encrypting state events.
|
||||
*/
|
||||
enableEncryptedStateEvents?: boolean;
|
||||
}): Promise<RustCrypto> {
|
||||
const { logger } = args;
|
||||
|
||||
@@ -128,6 +133,7 @@ export async function initRustCrypto(args: {
|
||||
args.cryptoCallbacks,
|
||||
storeHandle,
|
||||
args.legacyCryptoStore,
|
||||
args.enableEncryptedStateEvents,
|
||||
);
|
||||
|
||||
storeHandle.free();
|
||||
@@ -145,6 +151,7 @@ async function initOlmMachine(
|
||||
cryptoCallbacks: CryptoCallbacks,
|
||||
storeHandle: StoreHandle,
|
||||
legacyCryptoStore?: CryptoStore,
|
||||
enableEncryptedStateEvents?: boolean,
|
||||
): Promise<RustCrypto> {
|
||||
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.
|
||||
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[]) =>
|
||||
rustCrypto.onRoomKeysUpdated(sessions),
|
||||
|
||||
@@ -162,6 +162,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
|
||||
/** Crypto callbacks provided by the application */
|
||||
private readonly cryptoCallbacks: CryptoCallbacks,
|
||||
|
||||
/** Enable support for encrypted state events under MSC3414. */
|
||||
private readonly enableEncryptedStateEvents: boolean = false,
|
||||
) {
|
||||
super();
|
||||
this.outgoingRequestProcessor = new OutgoingRequestProcessor(logger, olmMachine, http);
|
||||
@@ -421,6 +424,16 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
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}.
|
||||
*/
|
||||
@@ -1717,7 +1730,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
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 event - encryption event to be processed
|
||||
@@ -1734,6 +1747,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
return;
|
||||
}
|
||||
|
||||
if (config["io.element.msc3414.encrypt_state_events"] && this.enableEncryptedStateEvents) {
|
||||
this.logger.info("crypto Enabling state event encryption...");
|
||||
settings.encryptStateEvents = true;
|
||||
}
|
||||
|
||||
try {
|
||||
settings.sessionRotationPeriodMs = config.rotation_period_ms;
|
||||
settings.sessionRotationPeriodMessages = config.rotation_period_msgs;
|
||||
|
||||
@@ -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 {
|
||||
if ("org.matrix.msc4222.state_after" in joinObj) {
|
||||
await this.injectRoomEvents(
|
||||
|
||||
@@ -1763,7 +1763,7 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@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"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-15.3.0.tgz#141fd041ae382b793369bcee4394b0b577bdea0c"
|
||||
integrity sha512-QyxHvncvkl7nf+tnn92PjQ54gMNV8hMSpiukiDgNrqF6IYwgySTlcSdkPYdw8QjZJ0NR6fnVrNzMec0OohM3wA==
|
||||
@@ -2314,12 +2314,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.21.0.tgz#58f30aec8db8212fd886835dc5969cdf47cb29f5"
|
||||
integrity sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==
|
||||
|
||||
"@typescript-eslint/types@8.44.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":
|
||||
"@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==
|
||||
|
||||
Reference in New Issue
Block a user