Skip to content

Commit a575683

Browse files
committed
feat: Implemenet lab for encrypted state events (MSC3414)
Signed-off-by: Skye Elliot <[email protected]> feat: use correct key for enabling state encryption feat: add lab to element web feat: add EncryptedStateSettingsController chore: update lockfile
1 parent bb2ced6 commit a575683

File tree

8 files changed

+92
-3
lines changed

8 files changed

+92
-3
lines changed

src/MatrixClientPeg.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
437437
// These are always installed regardless of the labs flag so that cross-signing features
438438
// can toggle on without reloading and also be accessed immediately after login.
439439
cryptoCallbacks: { ...crossSigningCallbacks },
440+
enableEncryptedStateEvents: SettingsStore.getValue("feature_msc3414_encrypted_state_events"),
440441
roomNameGenerator: (_: string, state: RoomNameState) => {
441442
switch (state.type) {
442443
case RoomNameType.Generated:

src/components/views/dialogs/CreateRoomDialog.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ interface IProps {
3434
defaultName?: string;
3535
parentSpace?: Room;
3636
defaultEncrypted?: boolean;
37+
defaultStateEncrypted?: boolean;
3738
onFinished(proceed?: false): void;
3839
onFinished(proceed: true, opts: IOpts): void;
3940
}
@@ -52,6 +53,10 @@ interface IState {
5253
* Indicates whether end-to-end encryption is enabled for the room.
5354
*/
5455
isEncrypted: boolean;
56+
/**
57+
* Indicates whether end-to-end state encryption is enabled for this room.
58+
*/
59+
isStateEncrypted: boolean;
5560
/**
5661
* The room name.
5762
*/
@@ -111,6 +116,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
111116
this.state = {
112117
isPublicKnockRoom: defaultPublic || false,
113118
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli),
119+
isStateEncrypted: this.props.defaultStateEncrypted ?? false,
114120
joinRule,
115121
name: this.props.defaultName || "",
116122
topic: "",
@@ -136,6 +142,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
136142
createOpts.room_alias_name = alias.substring(1, alias.indexOf(":"));
137143
} else {
138144
opts.encryption = this.state.isEncrypted;
145+
opts.stateEncryption = this.state.isStateEncrypted;
139146
}
140147

141148
if (this.state.topic) {
@@ -230,6 +237,10 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
230237
this.setState({ isEncrypted });
231238
};
232239

240+
private onStateEncryptedChange = (isStateEncrypted: boolean): void => {
241+
this.setState({ isStateEncrypted });
242+
};
243+
233244
private onAliasChange = (alias: string): void => {
234245
this.setState({ alias });
235246
};
@@ -373,6 +384,37 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
373384
);
374385
}
375386

387+
let e2eeStateSection: JSX.Element | undefined;
388+
if (
389+
SettingsStore.getValue("feature_msc3414_encrypted_state_events", null, false) &&
390+
this.state.joinRule !== JoinRule.Public
391+
) {
392+
let microcopy: string;
393+
if (privateShouldBeEncrypted(MatrixClientPeg.safeGet())) {
394+
if (this.state.canChangeEncryption) {
395+
microcopy = isVideoRoom
396+
? _t("create_room|encrypted_video_room_warning")
397+
: _t("create_room|state_encrypted_warning");
398+
} else {
399+
microcopy = _t("create_room|encryption_forced");
400+
}
401+
} else {
402+
microcopy = _t("settings|security|e2ee_default_disabled_warning");
403+
}
404+
e2eeStateSection = (
405+
<React.Fragment>
406+
<LabelledToggleSwitch
407+
label={_t("create_room|state_encryption_label")}
408+
onChange={this.onStateEncryptedChange}
409+
value={this.state.isStateEncrypted}
410+
className="mx_CreateRoomDialog_e2eSwitch" // for end-to-end tests
411+
disabled={!this.state.canChangeEncryption}
412+
/>
413+
<p>{microcopy}</p>
414+
</React.Fragment>
415+
);
416+
}
417+
376418
let federateLabel = _t("create_room|unfederated_label_default_off");
377419
if (SdkConfig.get().default_federate === false) {
378420
// We only change the label if the default setting is different to avoid jarring text changes to the
@@ -433,6 +475,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
433475
{publicPrivateLabel}
434476
{visibilitySection}
435477
{e2eeSection}
478+
{e2eeStateSection}
436479
{aliasField}
437480
{this.advancedSettingsEnabled && (
438481
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">

src/components/views/messages/EncryptionEvent.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,19 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
4949
subtitle = _t("timeline|m.room.encryption|enabled_local");
5050
} else {
5151
subtitle = _t("timeline|m.room.encryption|enabled");
52+
if (content["io.element.msc3414.encrypt_state_events"]) {
53+
subtitle += " " + _t("timeline|m.room.encryption|state_enabled");
54+
}
5255
}
5356

5457
return (
5558
<EventTileBubble
5659
className="mx_cryptoEvent mx_cryptoEvent_icon"
57-
title={_t("common|encryption_enabled")}
60+
title={
61+
content["io.element.msc3414.encrypt_state_events"]
62+
? _t("common|state_encryption_enabled")
63+
: _t("common|encryption_enabled")
64+
}
5865
subtitle={subtitle}
5966
timestamp={timestamp}
6067
/>

src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
175175
this.setState({ encrypted: true });
176176
this.context
177177
.sendStateEvent(this.props.room.roomId, EventType.RoomEncryption, {
178-
algorithm: MEGOLM_ENCRYPTION_ALGORITHM,
178+
"algorithm": MEGOLM_ENCRYPTION_ALGORITHM,
179+
"io.element.msc3414.encrypt_state_events": false,
179180
})
180181
.catch((e) => {
181182
logger.error(e);

src/createRoom.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export interface IOpts {
5353
spinner?: boolean;
5454
guestAccess?: boolean;
5555
encryption?: boolean;
56+
stateEncryption?: boolean;
5657
inlineErrors?: boolean;
5758
andView?: boolean;
5859
avatar?: File | string; // will upload if given file, else mxcUrl is needed
@@ -100,6 +101,7 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
100101
if (opts.spinner === undefined) opts.spinner = true;
101102
if (opts.guestAccess === undefined) opts.guestAccess = true;
102103
if (opts.encryption === undefined) opts.encryption = false;
104+
if (opts.stateEncryption === undefined) opts.stateEncryption = false;
103105

104106
if (client.isGuest()) {
105107
dis.dispatch({ action: "require_registration" });
@@ -213,7 +215,8 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
213215
type: "m.room.encryption",
214216
state_key: "",
215217
content: {
216-
algorithm: MEGOLM_ENCRYPTION_ALGORITHM,
218+
"algorithm": MEGOLM_ENCRYPTION_ALGORITHM,
219+
"io.element.msc3414.encrypt_state_events": opts.stateEncryption,
217220
},
218221
});
219222
}

src/i18n/strings/en_EN.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,7 @@
578578
"someone": "Someone",
579579
"space": "Space",
580580
"spaces": "Spaces",
581+
"state_encryption_enabled": "Experimental state encryption enabled",
581582
"sticker": "Sticker",
582583
"stickerpack": "Stickerpack",
583584
"success": "Success",
@@ -673,6 +674,8 @@
673674
"encrypted_warning": "You can't disable this later. Bridges & most bots won't work yet.",
674675
"encryption_forced": "Your server requires encryption to be enabled in private rooms.",
675676
"encryption_label": "Enable end-to-end encryption",
677+
"state_encrypted_warning": "WARNING: This is an experimental feature.",
678+
"state_encryption_label": "Encrypt state events",
676679
"error_title": "Failure to create room",
677680
"generic_error": "Server may be unavailable, overloaded, or you hit a bug.",
678681
"join_rule_change_notice": "You can change this at any time from room settings.",
@@ -1494,6 +1497,8 @@
14941497
"dynamic_room_predecessors": "Dynamic room predecessors",
14951498
"dynamic_room_predecessors_description": "Enable MSC3946 (to support late-arriving room archives)",
14961499
"element_call_video_rooms": "Element Call video rooms",
1500+
"encrypted_state_events": "Encrypted state events",
1501+
"encrypted_state_events_decsription": "Enables experimental support for encrypting state events, which hides metadata such as room names and topics from being visible to the server.",
14971502
"exclude_insecure_devices": "Exclude insecure devices when sending/receiving messages",
14981503
"exclude_insecure_devices_description": "When this mode is enabled, encrypted messages will not be shared with unverified devices, and messages from unverified devices will be shown as an error. Note that if you enable this mode, you may be unable to communicate with users who have not verified their devices.",
14991504
"experimental_description": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. <a>Learn more</a>.",
@@ -3525,6 +3530,7 @@
35253530
"enabled": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their profile picture.",
35263531
"enabled_dm": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their profile picture.",
35273532
"enabled_local": "Messages in this chat will be end-to-end encrypted.",
3533+
"state_enabled": "State events in this room are end-to-end encrypted.",
35283534
"parameters_changed": "Some encryption parameters have been changed.",
35293535
"unsupported": "The encryption used by this room isn't supported."
35303536
},

src/settings/Settings.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { SortingAlgorithm } from "../stores/room-list-v3/skip-list/sorters/index
5050
import MediaPreviewConfigController from "./controllers/MediaPreviewConfigController.ts";
5151
import InviteRulesConfigController from "./controllers/InviteRulesConfigController.ts";
5252
import { type ComputedInviteConfig } from "../@types/invite-rules.ts";
53+
import EncryptedStateEventsController from "./controllers/EncryptedStateEventsController.ts";
5354

5455
export const defaultWatchManager = new WatchManager();
5556

@@ -221,6 +222,7 @@ export interface Settings {
221222
"feature_new_room_list": IFeature;
222223
"feature_ask_to_join": IFeature;
223224
"feature_notifications": IFeature;
225+
"feature_msc3414_encrypted_state_events": IFeature;
224226
// These are in the feature namespace but aren't actually features
225227
"feature_hidebold": IBaseSetting<boolean>;
226228

@@ -783,6 +785,17 @@ export const SETTINGS: Settings = {
783785
supportedLevelsAreOrdered: true,
784786
default: false,
785787
},
788+
"feature_msc3414_encrypted_state_events": {
789+
isFeature: true,
790+
labsGroup: LabGroup.Encryption,
791+
controller: new EncryptedStateEventsController(),
792+
displayName: _td("labs|encrypted_state_events"),
793+
description: _td("labs|encrypted_state_events_decsription"),
794+
supportedLevels: LEVELS_ROOM_SETTINGS,
795+
supportedLevelsAreOrdered: true,
796+
shouldWarn: true,
797+
default: false,
798+
},
786799
"useCompactLayout": {
787800
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
788801
displayName: _td("settings|preferences|compact_modern"),
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
Copyright 2024 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import PlatformPeg from "../../PlatformPeg";
9+
import SettingController from "./SettingController";
10+
11+
export default class EncryptedStateEventsController extends SettingController {
12+
public async onChange(): Promise<void> {
13+
PlatformPeg.get()?.reload();
14+
}
15+
}

0 commit comments

Comments
 (0)