You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
Element-R: check persistent room list for encryption config (#4035)
* crypto.spec: make `keyResponder` a local var it is never used between functions, so making it external was confusing * Persist encryption state to the rust room list. * `MatrixClient.shouldEncryptEventForRoom`: fix for rust crypto Previously, we were not bothering to ask the Rust Crypto stack if it thought we should be encrypting for a given room. This adds a new method to `CryptoApi`, wires it up for legacy and Rust crypto, and calls it. * Tests for persistent room list
This commit is contained in:
committed by
GitHub
parent
869576747c
commit
11348f9532
@ -29,6 +29,7 @@ import {
|
|||||||
getSyncResponse,
|
getSyncResponse,
|
||||||
InitCrypto,
|
InitCrypto,
|
||||||
mkEventCustom,
|
mkEventCustom,
|
||||||
|
mkMembershipCustom,
|
||||||
syncPromise,
|
syncPromise,
|
||||||
} from "../../test-utils/test-utils";
|
} from "../../test-utils/test-utils";
|
||||||
import * as testData from "../../test-utils/test-data";
|
import * as testData from "../../test-utils/test-data";
|
||||||
@ -38,6 +39,7 @@ import {
|
|||||||
BOB_TEST_USER_ID,
|
BOB_TEST_USER_ID,
|
||||||
SIGNED_CROSS_SIGNING_KEYS_DATA,
|
SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||||
SIGNED_TEST_DEVICE_DATA,
|
SIGNED_TEST_DEVICE_DATA,
|
||||||
|
TEST_ROOM_ID,
|
||||||
TEST_ROOM_ID as ROOM_ID,
|
TEST_ROOM_ID as ROOM_ID,
|
||||||
TEST_USER_ID,
|
TEST_USER_ID,
|
||||||
} from "../../test-utils/test-data";
|
} from "../../test-utils/test-data";
|
||||||
@ -230,9 +232,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */
|
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */
|
||||||
let keyReceiver: E2EKeyReceiver;
|
let keyReceiver: E2EKeyReceiver;
|
||||||
|
|
||||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
|
||||||
let keyResponder: E2EKeyResponder;
|
|
||||||
|
|
||||||
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||||
let syncResponder: ISyncResponder;
|
let syncResponder: ISyncResponder;
|
||||||
|
|
||||||
@ -368,6 +367,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
accessToken: "akjgkrgjs",
|
accessToken: "akjgkrgjs",
|
||||||
deviceId: "xzcvb",
|
deviceId: "xzcvb",
|
||||||
cryptoCallbacks: createCryptoCallbacks(),
|
cryptoCallbacks: createCryptoCallbacks(),
|
||||||
|
logger: logger.getChild("aliceClient"),
|
||||||
});
|
});
|
||||||
|
|
||||||
/* set up listeners for /keys/upload and /sync */
|
/* set up listeners for /keys/upload and /sync */
|
||||||
@ -701,7 +701,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
|
|
||||||
it("prepareToEncrypt", async () => {
|
it("prepareToEncrypt", async () => {
|
||||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||||
|
|
||||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||||
@ -732,7 +732,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
it("Alice sends a megolm message with GlobalErrorOnUnknownDevices=false", async () => {
|
it("Alice sends a megolm message with GlobalErrorOnUnknownDevices=false", async () => {
|
||||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||||
|
|
||||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||||
@ -760,7 +760,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
it("We should start a new megolm session after forceDiscardSession", async () => {
|
it("We should start a new megolm session after forceDiscardSession", async () => {
|
||||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||||
|
|
||||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||||
@ -2070,7 +2070,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
|
|
||||||
it("Sending an event initiates a member list sync", async () => {
|
it("Sending an event initiates a member list sync", async () => {
|
||||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||||
|
|
||||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||||
@ -2093,7 +2093,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
|
|
||||||
it("loading the membership list inhibits a later load", async () => {
|
it("loading the membership list inhibits a later load", async () => {
|
||||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||||
|
|
||||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||||
@ -2903,7 +2903,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
// anything that we don't have a specific matcher for silently returns a 404
|
// anything that we don't have a specific matcher for silently returns a 404
|
||||||
fetchMock.catch(404);
|
fetchMock.catch(404);
|
||||||
|
|
||||||
keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
const keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
||||||
keyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
keyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||||
keyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
keyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||||
keyResponder.addKeyReceiver(BOB_TEST_USER_ID, keyReceiver);
|
keyResponder.addKeyReceiver(BOB_TEST_USER_ID, keyReceiver);
|
||||||
@ -2939,4 +2939,180 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
expect(hasCrossSigningKeysForUser).toBe(false);
|
expect(hasCrossSigningKeysForUser).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Guards against downgrade attacks from servers hiding or manipulating the crypto settings. */
|
||||||
|
describe("Persistent encryption settings", () => {
|
||||||
|
let persistentStoreClient: MatrixClient;
|
||||||
|
let client2: MatrixClient;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const homeserverurl = "https://alice-server.com";
|
||||||
|
const userId = "@alice:localhost";
|
||||||
|
|
||||||
|
const keyResponder = new E2EKeyResponder(homeserverurl);
|
||||||
|
keyResponder.addKeyReceiver(userId, keyReceiver);
|
||||||
|
|
||||||
|
// For legacy crypto, these tests only work properly with a proper (indexeddb-based) CryptoStore, so
|
||||||
|
// rather than using the existing `aliceClient`, create a new client. Once we drop legacy crypto, we can
|
||||||
|
// just use `aliceClient` here.
|
||||||
|
persistentStoreClient = await makeNewClient(homeserverurl, userId, "persistentStoreClient");
|
||||||
|
await persistentStoreClient.startClient({});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
persistentStoreClient.stopClient();
|
||||||
|
client2?.stopClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Sending a message in a room where the server is hiding the state event does not send a plaintext event", async () => {
|
||||||
|
// Alice is in an encrypted room
|
||||||
|
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2" });
|
||||||
|
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState]));
|
||||||
|
await syncPromise(persistentStoreClient);
|
||||||
|
|
||||||
|
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||||
|
await Promise.all([persistentStoreClient.sendTextMessage(ROOM_ID, "test"), expectEncryptedSendMessage()]);
|
||||||
|
|
||||||
|
// We now replace the client, and allow the new one to resync, *without* the encryption event.
|
||||||
|
client2 = await replaceClient(persistentStoreClient);
|
||||||
|
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([]));
|
||||||
|
await client2.startClient({});
|
||||||
|
await syncPromise(client2);
|
||||||
|
logger.log(client2.getUserId() + ": restarted");
|
||||||
|
|
||||||
|
await expectSendMessageToFail(client2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Changes to the rotation period should be ignored", async () => {
|
||||||
|
// Alice is in an encrypted room, where the rotation period is set to 2 messages
|
||||||
|
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 });
|
||||||
|
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState]));
|
||||||
|
await syncPromise(persistentStoreClient);
|
||||||
|
|
||||||
|
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||||
|
const [, msg1Content] = await Promise.all([
|
||||||
|
persistentStoreClient.sendTextMessage(ROOM_ID, "test1"),
|
||||||
|
expectEncryptedSendMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Replace the state with one which bumps the rotation period. This should be ignored, though it's not
|
||||||
|
// clear that is correct behaviour (see https://github.com/element-hq/element-meta/issues/69)
|
||||||
|
const encryptionState2 = mkEncryptionEvent({
|
||||||
|
algorithm: "m.megolm.v1.aes-sha2",
|
||||||
|
rotation_period_msgs: 100,
|
||||||
|
});
|
||||||
|
syncResponder.sendOrQueueSyncResponse({
|
||||||
|
next_batch: "1",
|
||||||
|
rooms: { join: { [TEST_ROOM_ID]: { timeline: { events: [encryptionState2], prev_batch: "" } } } },
|
||||||
|
});
|
||||||
|
await syncPromise(persistentStoreClient);
|
||||||
|
|
||||||
|
// Send two more messages. The first should use the same megolm session as the first; the second should
|
||||||
|
// use a different one.
|
||||||
|
const [, msg2Content] = await Promise.all([
|
||||||
|
persistentStoreClient.sendTextMessage(ROOM_ID, "test2"),
|
||||||
|
expectEncryptedSendMessage(),
|
||||||
|
]);
|
||||||
|
expect(msg2Content.session_id).toEqual(msg1Content.session_id);
|
||||||
|
const [, msg3Content] = await Promise.all([
|
||||||
|
persistentStoreClient.sendTextMessage(ROOM_ID, "test3"),
|
||||||
|
expectEncryptedSendMessage(),
|
||||||
|
]);
|
||||||
|
expect(msg3Content.session_id).not.toEqual(msg1Content.session_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Changes to the rotation period should be ignored after a client restart", async () => {
|
||||||
|
// Alice is in an encrypted room, where the rotation period is set to 2 messages
|
||||||
|
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 });
|
||||||
|
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState]));
|
||||||
|
await syncPromise(persistentStoreClient);
|
||||||
|
|
||||||
|
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||||
|
await Promise.all([persistentStoreClient.sendTextMessage(ROOM_ID, "test1"), expectEncryptedSendMessage()]);
|
||||||
|
|
||||||
|
// We now replace the client, and allow the new one to resync with a *different* encryption event.
|
||||||
|
client2 = await replaceClient(persistentStoreClient);
|
||||||
|
const encryptionState2 = mkEncryptionEvent({
|
||||||
|
algorithm: "m.megolm.v1.aes-sha2",
|
||||||
|
rotation_period_msgs: 100,
|
||||||
|
});
|
||||||
|
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState2]));
|
||||||
|
await client2.startClient({});
|
||||||
|
await syncPromise(client2);
|
||||||
|
logger.log(client2.getUserId() + ": restarted");
|
||||||
|
|
||||||
|
// Now send another message, which should (for now) be rejected.
|
||||||
|
await expectSendMessageToFail(client2);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Shut down `oldClient`, and build a new MatrixClient for the same user. */
|
||||||
|
async function replaceClient(oldClient: MatrixClient) {
|
||||||
|
oldClient.stopClient();
|
||||||
|
syncResponder.sendOrQueueSyncResponse({}); // flush pending request from old client
|
||||||
|
return makeNewClient(oldClient.getHomeserverUrl(), oldClient.getSafeUserId(), "client2");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeNewClient(
|
||||||
|
homeserverUrl: string,
|
||||||
|
userId: string,
|
||||||
|
loggerPrefix: string,
|
||||||
|
): Promise<MatrixClient> {
|
||||||
|
const client = createClient({
|
||||||
|
baseUrl: homeserverUrl,
|
||||||
|
userId: userId,
|
||||||
|
accessToken: "akjgkrgjs",
|
||||||
|
deviceId: "xzcvb",
|
||||||
|
cryptoCallbacks: createCryptoCallbacks(),
|
||||||
|
logger: logger.getChild(loggerPrefix),
|
||||||
|
|
||||||
|
// For legacy crypto, these tests only work with a proper persistent cryptoStore.
|
||||||
|
cryptoStore: new IndexedDBCryptoStore(indexedDB, "test"),
|
||||||
|
});
|
||||||
|
await initCrypto(client);
|
||||||
|
mockInitialApiRequests(client.getHomeserverUrl());
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkEncryptionEvent(content: Object) {
|
||||||
|
return mkEventCustom({
|
||||||
|
sender: persistentStoreClient.getSafeUserId(),
|
||||||
|
type: "m.room.encryption",
|
||||||
|
state_key: "",
|
||||||
|
content: content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sync response which includes `TEST_ROOM_ID`, where alice is a member
|
||||||
|
*
|
||||||
|
* @param stateEvents - Additional state events for the test room
|
||||||
|
*/
|
||||||
|
function getSyncResponseWithState(stateEvents: Array<Object>) {
|
||||||
|
const roomResponse = {
|
||||||
|
state: {
|
||||||
|
events: [
|
||||||
|
mkMembershipCustom({ membership: "join", sender: persistentStoreClient.getSafeUserId() }),
|
||||||
|
...stateEvents,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
events: [],
|
||||||
|
prev_batch: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
next_batch: "1",
|
||||||
|
rooms: { join: { [TEST_ROOM_ID]: roomResponse } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Send a message with the given client, and check that it is not sent in plaintext */
|
||||||
|
async function expectSendMessageToFail(aliceClient2: MatrixClient) {
|
||||||
|
// The precise failure mode here is somewhat up for debate (https://github.com/element-hq/element-meta/issues/69).
|
||||||
|
// For now, the attempt to send is rejected with an exception. The text is different between old and new stacks.
|
||||||
|
await expect(aliceClient2.sendTextMessage(ROOM_ID, "test")).rejects.toThrow(
|
||||||
|
/unconfigured room !room:id|Room !room:id was previously configured to use encryption/,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -24,11 +24,21 @@ import { KeyBackupInfo } from "../../src/crypto-api";
|
|||||||
* @param homeserverUrl - the homeserver url for the client under test
|
* @param homeserverUrl - the homeserver url for the client under test
|
||||||
*/
|
*/
|
||||||
export function mockInitialApiRequests(homeserverUrl: string) {
|
export function mockInitialApiRequests(homeserverUrl: string) {
|
||||||
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
|
fetchMock.getOnce(
|
||||||
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
new URL("/_matrix/client/versions", homeserverUrl).toString(),
|
||||||
fetchMock.postOnce(new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), {
|
{ versions: ["v1.1"] },
|
||||||
filter_id: "fid",
|
{ overwriteRoutes: true },
|
||||||
});
|
);
|
||||||
|
fetchMock.getOnce(
|
||||||
|
new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(),
|
||||||
|
{},
|
||||||
|
{ overwriteRoutes: true },
|
||||||
|
);
|
||||||
|
fetchMock.postOnce(
|
||||||
|
new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(),
|
||||||
|
{ filter_id: "fid" },
|
||||||
|
{ overwriteRoutes: true },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1469,8 +1469,8 @@ describe("MatrixClient", function () {
|
|||||||
expect(getRoomId).toEqual(roomId);
|
expect(getRoomId).toEqual(roomId);
|
||||||
return mockRoom;
|
return mockRoom;
|
||||||
};
|
};
|
||||||
|
|
||||||
mockCrypto = {
|
mockCrypto = {
|
||||||
|
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true),
|
||||||
encryptEvent: jest.fn(),
|
encryptEvent: jest.fn(),
|
||||||
stop: jest.fn(),
|
stop: jest.fn(),
|
||||||
} as unknown as Mocked<Crypto>;
|
} as unknown as Mocked<Crypto>;
|
||||||
|
@ -3251,6 +3251,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @param roomId - The room ID to enable encryption in.
|
* @param roomId - The room ID to enable encryption in.
|
||||||
* @param config - The encryption config for the room.
|
* @param config - The encryption config for the room.
|
||||||
* @returns A promise that will resolve when encryption is set up.
|
* @returns A promise that will resolve when encryption is set up.
|
||||||
|
*
|
||||||
|
* @deprecated Not supported for Rust Cryptography. To enable encryption in a room, send an `m.room.encryption`
|
||||||
|
* state event.
|
||||||
*/
|
*/
|
||||||
public setRoomEncryption(roomId: string, config: IRoomEncryption): Promise<void> {
|
public setRoomEncryption(roomId: string, config: IRoomEncryption): Promise<void> {
|
||||||
if (!this.crypto) {
|
if (!this.crypto) {
|
||||||
@ -3263,6 +3266,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* Whether encryption is enabled for a room.
|
* Whether encryption is enabled for a room.
|
||||||
* @param roomId - the room id to query.
|
* @param roomId - the room id to query.
|
||||||
* @returns whether encryption is enabled.
|
* @returns whether encryption is enabled.
|
||||||
|
*
|
||||||
|
* @deprecated Not correctly supported for Rust Cryptography. Use {@link CryptoApi.isEncryptionEnabledInRoom} and/or
|
||||||
|
* {@link Room.hasEncryptionStateEvent}.
|
||||||
*/
|
*/
|
||||||
public isRoomEncrypted(roomId: string): boolean {
|
public isRoomEncrypted(roomId: string): boolean {
|
||||||
const room = this.getRoom(roomId);
|
const room = this.getRoom(roomId);
|
||||||
@ -4824,7 +4830,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
// If the room is unknown, we cannot encrypt for it
|
// If the room is unknown, we cannot encrypt for it
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
||||||
if (!this.shouldEncryptEventForRoom(event, room)) return;
|
if (!(await this.shouldEncryptEventForRoom(event, room))) return;
|
||||||
|
|
||||||
if (!this.cryptoBackend && this.usingExternalCrypto) {
|
if (!this.cryptoBackend && this.usingExternalCrypto) {
|
||||||
// The client has opted to allow sending messages to encrypted
|
// The client has opted to allow sending messages to encrypted
|
||||||
@ -4846,7 +4852,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
*
|
*
|
||||||
* This takes into account event type and room configuration.
|
* This takes into account event type and room configuration.
|
||||||
*/
|
*/
|
||||||
private shouldEncryptEventForRoom(event: MatrixEvent, room: Room): boolean {
|
private async shouldEncryptEventForRoom(event: MatrixEvent, room: Room): Promise<boolean> {
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
// this event has already been encrypted; this happens if the
|
// this event has already been encrypted; this happens if the
|
||||||
// encryption step succeeded, but the send step failed on the first
|
// encryption step succeeded, but the send step failed on the first
|
||||||
@ -4878,7 +4884,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
if (room.hasEncryptionStateEvent()) return true;
|
if (room.hasEncryptionStateEvent()) return true;
|
||||||
|
|
||||||
// If we have a crypto impl, and *it* thinks we should encrypt, then we should.
|
// If we have a crypto impl, and *it* thinks we should encrypt, then we should.
|
||||||
if (this.crypto?.isRoomEncrypted(room.roomId)) return true;
|
if (await this.cryptoBackend?.isEncryptionEnabledInRoom(room.roomId)) return true;
|
||||||
|
|
||||||
// Otherwise, no need to encrypt.
|
// Otherwise, no need to encrypt.
|
||||||
return false;
|
return false;
|
||||||
|
@ -53,6 +53,18 @@ export interface CryptoApi {
|
|||||||
*/
|
*/
|
||||||
getOwnDeviceKeys(): Promise<OwnDeviceKeys>;
|
getOwnDeviceKeys(): Promise<OwnDeviceKeys>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we believe the given room to be encrypted.
|
||||||
|
*
|
||||||
|
* This method returns true if the room has been configured with encryption. The setting is persistent, so that
|
||||||
|
* even if the encryption event is removed from the room state, it still returns true. This helps to guard against
|
||||||
|
* a downgrade attack wherein a server admin attempts to remove encryption.
|
||||||
|
*
|
||||||
|
* @returns `true` if the room with the supplied ID is encrypted. `false` if the room is not encrypted, or is unknown to
|
||||||
|
* us.
|
||||||
|
*/
|
||||||
|
isEncryptionEnabledInRoom(roomId: string): Promise<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.
|
||||||
@ -189,7 +201,7 @@ export interface CryptoApi {
|
|||||||
* Cross-signing a device indicates, to our other devices and to other users, that we have verified that it really
|
* Cross-signing a device indicates, to our other devices and to other users, that we have verified that it really
|
||||||
* belongs to us.
|
* belongs to us.
|
||||||
*
|
*
|
||||||
* Requires that cross-signing has been set up on this device (normally by calling {@link bootstrapCrossSigning}.
|
* Requires that cross-signing has been set up on this device (normally by calling {@link bootstrapCrossSigning}).
|
||||||
*
|
*
|
||||||
* *Note*: Do not call this unless you have verified, somehow, that the device is genuine!
|
* *Note*: Do not call this unless you have verified, somehow, that the device is genuine!
|
||||||
*
|
*
|
||||||
|
@ -4273,6 +4273,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
return this.roomList.isRoomEncrypted(roomId);
|
return this.roomList.isRoomEncrypted(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link CryptoApi#isEncryptionEnabledInRoom}.
|
||||||
|
*/
|
||||||
|
public async isEncryptionEnabledInRoom(roomId: string): Promise<boolean> {
|
||||||
|
return this.isRoomEncrypted(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns information about the encryption on the room with the supplied
|
* @returns information about the encryption on the room with the supplied
|
||||||
* ID, or null if the room is not encrypted or unknown to us.
|
* ID, or null if the room is not encrypted or unknown to us.
|
||||||
|
@ -88,7 +88,8 @@ export class RoomEncryptor {
|
|||||||
*/
|
*/
|
||||||
public onCryptoEvent(config: IContent): void {
|
public onCryptoEvent(config: IContent): void {
|
||||||
if (JSON.stringify(this.encryptionSettings) != JSON.stringify(config)) {
|
if (JSON.stringify(this.encryptionSettings) != JSON.stringify(config)) {
|
||||||
this.prefixedLogger.error(`Ignoring m.room.encryption event which requests a change of config`);
|
// This should currently be unreachable, since the Rust SDK will reject any attempts to change config.
|
||||||
|
throw new Error("Cannot reconfigure an active RoomEncryptor");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,6 +311,16 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
|||||||
return `Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`;
|
return `Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link CryptoApi#isEncryptionEnabledInRoom}.
|
||||||
|
*/
|
||||||
|
public async isEncryptionEnabledInRoom(roomId: string): Promise<boolean> {
|
||||||
|
const roomSettings: RustSdkCryptoJs.RoomSettings | undefined = await this.olmMachine.getRoomSettings(
|
||||||
|
new RustSdkCryptoJs.RoomId(roomId),
|
||||||
|
);
|
||||||
|
return Boolean(roomSettings?.algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of {@link CryptoApi#getOwnDeviceKeys}.
|
* Implementation of {@link CryptoApi#getOwnDeviceKeys}.
|
||||||
*/
|
*/
|
||||||
@ -1285,7 +1295,27 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
|||||||
*/
|
*/
|
||||||
public async onCryptoEvent(room: Room, event: MatrixEvent): Promise<void> {
|
public async onCryptoEvent(room: Room, event: MatrixEvent): Promise<void> {
|
||||||
const config = event.getContent();
|
const config = event.getContent();
|
||||||
|
const settings = new RustSdkCryptoJs.RoomSettings();
|
||||||
|
|
||||||
|
if (config.algorithm === "m.megolm.v1.aes-sha2") {
|
||||||
|
settings.algorithm = RustSdkCryptoJs.EncryptionAlgorithm.MegolmV1AesSha2;
|
||||||
|
} else {
|
||||||
|
// Among other situations, this happens if the crypto state event is redacted.
|
||||||
|
this.logger.warn(`Room ${room.roomId}: ignoring crypto event with invalid algorithm ${config.algorithm}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
settings.sessionRotationPeriodMs = config.rotation_period_ms;
|
||||||
|
settings.sessionRotationPeriodMessages = config.rotation_period_msgs;
|
||||||
|
await this.olmMachine.setRoomSettings(new RustSdkCryptoJs.RoomId(room.roomId), settings);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`Room ${room.roomId}: ignoring crypto event which caused error: ${e}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got this far, the SDK found the event acceptable.
|
||||||
|
// We need to either create or update the active RoomEncryptor.
|
||||||
const existingEncryptor = this.roomEncryptors[room.roomId];
|
const existingEncryptor = this.roomEncryptors[room.roomId];
|
||||||
if (existingEncryptor) {
|
if (existingEncryptor) {
|
||||||
existingEncryptor.onCryptoEvent(config);
|
existingEncryptor.onCryptoEvent(config);
|
||||||
|
Reference in New Issue
Block a user