Skip to content

Commit a692fe2

Browse files
author
Kerry
authored
Add .well-known config option to force disable encryption on room creation (matrix-org#11120)
* force disable encryption on room creation * test allowChangingEncryption * move into utils/room directory * tests * unit test CreateRoomDialog * remove debug * wait for constructor promises to settle * test case for force_disable * comment * set forced value after resolving checkUserIsAllowedToChangeEncryption * tidy and comments * use label text in test
1 parent 9d9c55d commit a692fe2

9 files changed

+375
-9
lines changed

src/components/views/dialogs/CreateRoomDialog.tsx

+9-6
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import SdkConfig from "../../../SdkConfig";
2424
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
2525
import { _t } from "../../../languageHandler";
2626
import { MatrixClientPeg } from "../../../MatrixClientPeg";
27-
import { IOpts } from "../../../createRoom";
27+
import { checkUserIsAllowedToChangeEncryption, IOpts } from "../../../createRoom";
2828
import Field from "../elements/Field";
2929
import RoomAliasField from "../elements/RoomAliasField";
3030
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
@@ -86,11 +86,15 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
8686
detailsOpen: false,
8787
noFederate: SdkConfig.get().default_federate === false,
8888
nameIsValid: false,
89-
canChangeEncryption: true,
89+
canChangeEncryption: false,
9090
};
9191

92-
cli.doesServerForceEncryptionForPreset(Preset.PrivateChat).then((isForced) =>
93-
this.setState({ canChangeEncryption: !isForced }),
92+
checkUserIsAllowedToChangeEncryption(cli, Preset.PrivateChat).then(({ allowChange, forcedValue }) =>
93+
this.setState((state) => ({
94+
canChangeEncryption: allowChange,
95+
// override with forcedValue if it is set
96+
isEncrypted: forcedValue ?? state.isEncrypted,
97+
})),
9498
);
9599
}
96100

@@ -107,8 +111,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
107111
const { alias } = this.state;
108112
createOpts.room_alias_name = alias.substring(1, alias.indexOf(":"));
109113
} else {
110-
// If we cannot change encryption we pass `true` for safety, the server should automatically do this for us.
111-
opts.encryption = this.state.canChangeEncryption ? this.state.isEncrypted : true;
114+
opts.encryption = this.state.isEncrypted;
112115
}
113116

114117
if (this.state.topic) {

src/createRoom.ts

+47
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import Spinner from "./components/views/elements/Spinner";
4343
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
4444
import { findDMForUser } from "./utils/dm/findDMForUser";
4545
import { privateShouldBeEncrypted } from "./utils/rooms";
46+
import { shouldForceDisableEncryption } from "./utils/room/shouldForceDisableEncryption";
4647
import { waitForMember } from "./utils/membership";
4748
import { PreferredRoomVersions } from "./utils/PreferredRoomVersions";
4849
import SettingsStore from "./settings/SettingsStore";
@@ -471,3 +472,49 @@ export async function ensureDMExists(client: MatrixClient, userId: string): Prom
471472
}
472473
return roomId;
473474
}
475+
476+
interface AllowedEncryptionSetting {
477+
/**
478+
* True when the user is allowed to choose whether encryption is enabled
479+
*/
480+
allowChange: boolean;
481+
/**
482+
* Set when user is not allowed to choose encryption setting
483+
* True when encryption is forced to enabled
484+
*/
485+
forcedValue?: boolean;
486+
}
487+
/**
488+
* Check if server configuration supports the user changing encryption for a room
489+
* First check if server features force enable encryption for the given room type
490+
* If not, check if server .well-known forces encryption to disabled
491+
* If either are forced, then do not allow the user to change room's encryption
492+
* @param client
493+
* @param chatPreset chat type
494+
* @returns Promise<boolean>
495+
*/
496+
export async function checkUserIsAllowedToChangeEncryption(
497+
client: MatrixClient,
498+
chatPreset: Preset,
499+
): Promise<AllowedEncryptionSetting> {
500+
const doesServerForceEncryptionForPreset = await client.doesServerForceEncryptionForPreset(chatPreset);
501+
const doesWellKnownForceDisableEncryption = shouldForceDisableEncryption(client);
502+
503+
// server is forcing encryption to ENABLED
504+
// while .well-known config is forcing it to DISABLED
505+
// server version config overrides wk config
506+
if (doesServerForceEncryptionForPreset && doesWellKnownForceDisableEncryption) {
507+
console.warn(
508+
`Conflicting e2ee settings: server config and .well-known configuration disagree. Using server forced encryption setting for chat type ${chatPreset}`,
509+
);
510+
}
511+
512+
if (doesServerForceEncryptionForPreset) {
513+
return { allowChange: false, forcedValue: true };
514+
}
515+
if (doesWellKnownForceDisableEncryption) {
516+
return { allowChange: false, forcedValue: false };
517+
}
518+
519+
return { allowChange: true };
520+
}

src/utils/WellKnownUtils.ts

+7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ export interface ICallBehaviourWellKnown {
3131

3232
export interface IE2EEWellKnown {
3333
default?: boolean;
34+
/**
35+
* Forces the encryption to disabled for all new rooms
36+
* When true, overrides configured 'default' behaviour
37+
* Hides the option to enable encryption on room creation
38+
* Disables the option to enable encryption in room settings for all new and existing rooms
39+
*/
40+
force_disable?: boolean;
3441
secure_backup_required?: boolean;
3542
secure_backup_setup_methods?: SecureBackupSetupMethod[];
3643
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixClient } from "matrix-js-sdk/src/matrix";
18+
19+
import { getE2EEWellKnown } from "../WellKnownUtils";
20+
21+
/**
22+
* Check e2ee io.element.e2ee setting
23+
* Returns true when .well-known e2ee config force_disable is TRUE
24+
* When true all new rooms should be created with encryption disabled
25+
* Can be overriden by synapse option encryption_enabled_by_default_for_room_type ( :/ )
26+
* https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html#encryption_enabled_by_default_for_room_type
27+
*
28+
* @param client
29+
* @returns whether well-known config forces encryption to DISABLED
30+
*/
31+
export function shouldForceDisableEncryption(client: MatrixClient): boolean {
32+
const e2eeWellKnown = getE2EEWellKnown(client);
33+
34+
if (e2eeWellKnown) {
35+
const shouldForceDisable = e2eeWellKnown["force_disable"] === true;
36+
return shouldForceDisable;
37+
}
38+
return false;
39+
}

src/utils/rooms.ts

+4
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ limitations under the License.
1616

1717
import { MatrixClient } from "matrix-js-sdk/src/matrix";
1818

19+
import { shouldForceDisableEncryption } from "./room/shouldForceDisableEncryption";
1920
import { getE2EEWellKnown } from "./WellKnownUtils";
2021

2122
export function privateShouldBeEncrypted(client: MatrixClient): boolean {
23+
if (shouldForceDisableEncryption(client)) {
24+
return false;
25+
}
2226
const e2eeWellKnown = getE2EEWellKnown(client);
2327
if (e2eeWellKnown) {
2428
const defaultDisabled = e2eeWellKnown["default"] === false;

test/components/views/dialogs/CreateRoomDialog-test.tsx

+67
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,26 @@ describe("<CreateRoomDialog />", () => {
8181
);
8282
});
8383

84+
it("should use server .well-known force_disable for encryption setting", async () => {
85+
// force to off
86+
mockClient.getClientWellKnown.mockReturnValue({
87+
"io.element.e2ee": {
88+
default: true,
89+
force_disable: true,
90+
},
91+
});
92+
getComponent();
93+
await flushPromises();
94+
95+
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
96+
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
97+
expect(
98+
screen.getByText(
99+
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
100+
),
101+
);
102+
});
103+
84104
it("should use defaultEncrypted prop", async () => {
85105
// default to off in server wk
86106
mockClient.getClientWellKnown.mockReturnValue({
@@ -96,6 +116,53 @@ describe("<CreateRoomDialog />", () => {
96116
expect(getE2eeEnableToggleIsDisabled()).toBeFalsy();
97117
});
98118

119+
it("should use defaultEncrypted prop when it is false", async () => {
120+
// default to off in server wk
121+
mockClient.getClientWellKnown.mockReturnValue({
122+
"io.element.e2ee": {
123+
default: true,
124+
},
125+
});
126+
// but pass defaultEncrypted prop
127+
getComponent({ defaultEncrypted: false });
128+
await flushPromises();
129+
// encryption disabled
130+
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
131+
// not forced to off
132+
expect(getE2eeEnableToggleIsDisabled()).toBeFalsy();
133+
});
134+
135+
it("should override defaultEncrypted when server .well-known forces disabled encryption", async () => {
136+
// force to off
137+
mockClient.getClientWellKnown.mockReturnValue({
138+
"io.element.e2ee": {
139+
force_disable: true,
140+
},
141+
});
142+
getComponent({ defaultEncrypted: true });
143+
await flushPromises();
144+
145+
// server forces encryption to disabled, even though defaultEncrypted is false
146+
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
147+
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
148+
expect(
149+
screen.getByText(
150+
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
151+
),
152+
);
153+
});
154+
155+
it("should override defaultEncrypted when server forces enabled encryption", async () => {
156+
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(true);
157+
getComponent({ defaultEncrypted: false });
158+
await flushPromises();
159+
160+
// server forces encryption to enabled, even though defaultEncrypted is true
161+
expect(getE2eeEnableToggleInputElement()).toBeChecked();
162+
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
163+
expect(screen.getByText("Your server requires encryption to be enabled in private rooms."));
164+
});
165+
99166
it("should enable encryption toggle and disable field when server forces encryption", async () => {
100167
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(true);
101168
getComponent();

test/createRoom-test.ts

+55-3
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ limitations under the License.
1515
*/
1616

1717
import { mocked, Mocked } from "jest-mock";
18-
import { CryptoApi, MatrixClient, Device } from "matrix-js-sdk/src/matrix";
18+
import { CryptoApi, MatrixClient, Device, Preset } from "matrix-js-sdk/src/matrix";
1919
import { RoomType } from "matrix-js-sdk/src/@types/event";
2020

21-
import { stubClient, setupAsyncStoreWithClient, mockPlatformPeg } from "./test-utils";
21+
import { stubClient, setupAsyncStoreWithClient, mockPlatformPeg, getMockClientWithEventEmitter } from "./test-utils";
2222
import { MatrixClientPeg } from "../src/MatrixClientPeg";
2323
import WidgetStore from "../src/stores/WidgetStore";
2424
import WidgetUtils from "../src/utils/WidgetUtils";
2525
import { JitsiCall, ElementCall } from "../src/models/Call";
26-
import createRoom, { canEncryptToAllUsers } from "../src/createRoom";
26+
import createRoom, { checkUserIsAllowedToChangeEncryption, canEncryptToAllUsers } from "../src/createRoom";
2727
import SettingsStore from "../src/settings/SettingsStore";
2828

2929
describe("createRoom", () => {
@@ -207,3 +207,55 @@ describe("canEncryptToAllUsers", () => {
207207
expect(result).toBe(true);
208208
});
209209
});
210+
211+
describe("checkUserIsAllowedToChangeEncryption()", () => {
212+
const mockClient = getMockClientWithEventEmitter({
213+
doesServerForceEncryptionForPreset: jest.fn(),
214+
getClientWellKnown: jest.fn().mockReturnValue({}),
215+
});
216+
beforeEach(() => {
217+
mockClient.doesServerForceEncryptionForPreset.mockClear().mockResolvedValue(false);
218+
mockClient.getClientWellKnown.mockClear().mockReturnValue({});
219+
});
220+
221+
it("should allow changing when neither server nor well known force encryption", async () => {
222+
expect(await checkUserIsAllowedToChangeEncryption(mockClient, Preset.PrivateChat)).toEqual({
223+
allowChange: true,
224+
});
225+
226+
expect(mockClient.doesServerForceEncryptionForPreset).toHaveBeenCalledWith(Preset.PrivateChat);
227+
});
228+
229+
it("should not allow changing when server forces encryption", async () => {
230+
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(true);
231+
expect(await checkUserIsAllowedToChangeEncryption(mockClient, Preset.PrivateChat)).toEqual({
232+
allowChange: false,
233+
forcedValue: true,
234+
});
235+
});
236+
237+
it("should not allow changing when well-known force_disable is true", async () => {
238+
mockClient.getClientWellKnown.mockReturnValue({
239+
"io.element.e2ee": {
240+
force_disable: true,
241+
},
242+
});
243+
expect(await checkUserIsAllowedToChangeEncryption(mockClient, Preset.PrivateChat)).toEqual({
244+
allowChange: false,
245+
forcedValue: false,
246+
});
247+
});
248+
249+
it("should not allow changing when server forces enabled and wk forces disabled encryption", async () => {
250+
mockClient.getClientWellKnown.mockReturnValue({
251+
"io.element.e2ee": {
252+
force_disable: true,
253+
},
254+
});
255+
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(true);
256+
expect(await checkUserIsAllowedToChangeEncryption(mockClient, Preset.PrivateChat)).toEqual(
257+
// server's forced enable takes precedence
258+
{ allowChange: false, forcedValue: true },
259+
);
260+
});
261+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { shouldForceDisableEncryption } from "../../../src/utils/room/shouldForceDisableEncryption";
18+
import { getMockClientWithEventEmitter } from "../../test-utils";
19+
20+
describe("shouldForceDisableEncryption()", () => {
21+
const mockClient = getMockClientWithEventEmitter({
22+
getClientWellKnown: jest.fn(),
23+
});
24+
25+
beforeEach(() => {
26+
mockClient.getClientWellKnown.mockReturnValue(undefined);
27+
});
28+
29+
it("should return false when there is no e2ee well known", () => {
30+
expect(shouldForceDisableEncryption(mockClient)).toEqual(false);
31+
});
32+
33+
it("should return false when there is no force_disable property", () => {
34+
mockClient.getClientWellKnown.mockReturnValue({
35+
"io.element.e2ee": {
36+
// empty
37+
},
38+
});
39+
expect(shouldForceDisableEncryption(mockClient)).toEqual(false);
40+
});
41+
42+
it("should return false when force_disable property is falsy", () => {
43+
mockClient.getClientWellKnown.mockReturnValue({
44+
"io.element.e2ee": {
45+
force_disable: false,
46+
},
47+
});
48+
expect(shouldForceDisableEncryption(mockClient)).toEqual(false);
49+
});
50+
51+
it("should return false when force_disable property is not equal to true", () => {
52+
mockClient.getClientWellKnown.mockReturnValue({
53+
"io.element.e2ee": {
54+
force_disable: 1,
55+
},
56+
});
57+
expect(shouldForceDisableEncryption(mockClient)).toEqual(false);
58+
});
59+
60+
it("should return true when force_disable property is true", () => {
61+
mockClient.getClientWellKnown.mockReturnValue({
62+
"io.element.e2ee": {
63+
force_disable: true,
64+
},
65+
});
66+
expect(shouldForceDisableEncryption(mockClient)).toEqual(true);
67+
});
68+
});

0 commit comments

Comments
 (0)