Provide a labs flag for encrypted state events (MSC3414) (#31513)
Signed-off-by: Skye Elliot <actuallyori@gmail.com> Co-authored-by: Skye Elliot <actuallyori@gmail.com>
This commit is contained in:
co-authored by
Skye Elliot
parent
6f0369e623
commit
ff3f069122
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen, within } from "jest-matrix-react";
|
||||
import { act, fireEvent, render, screen, within } from "jest-matrix-react";
|
||||
import { type Room, JoinRule, MatrixError, Preset, Visibility } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import CreateRoomDialog from "../../../../../src/components/views/dialogs/CreateRoomDialog";
|
||||
@@ -247,6 +247,7 @@ describe("<CreateRoomDialog />", () => {
|
||||
createOpts: {},
|
||||
name: roomName,
|
||||
encryption: true,
|
||||
stateEncryption: false,
|
||||
parentSpace: undefined,
|
||||
roomType: undefined,
|
||||
});
|
||||
@@ -260,6 +261,29 @@ describe("<CreateRoomDialog />", () => {
|
||||
await flushPromises();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when the state encryption labs flag is on", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName) => settingName === "feature_msc4362_encrypted_state_events",
|
||||
);
|
||||
});
|
||||
|
||||
it("should turn on state encryption when toggled", async () => {
|
||||
// Given we have the create room dialog open
|
||||
const { asFragment } = getComponent();
|
||||
await flushPromises();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
// When I click the Encrypt state events toggle
|
||||
const toggle = screen.getByRole("switch", { name: "Encrypt state events" });
|
||||
expect(toggle).not.toBeChecked();
|
||||
act(() => toggle.click());
|
||||
|
||||
// Then it changes state
|
||||
expect(toggle).toBeChecked();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a knock room", () => {
|
||||
@@ -308,6 +332,7 @@ describe("<CreateRoomDialog />", () => {
|
||||
},
|
||||
name: roomName,
|
||||
encryption: true,
|
||||
stateEncryption: false,
|
||||
joinRule: JoinRule.Knock,
|
||||
parentSpace: undefined,
|
||||
roomType: undefined,
|
||||
@@ -326,6 +351,7 @@ describe("<CreateRoomDialog />", () => {
|
||||
},
|
||||
name: roomName,
|
||||
encryption: true,
|
||||
stateEncryption: false,
|
||||
joinRule: JoinRule.Knock,
|
||||
parentSpace: undefined,
|
||||
roomType: undefined,
|
||||
|
||||
+267
@@ -390,6 +390,273 @@ exports[`<CreateRoomDialog /> for a private room should render not the advanced
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
class="_input_udcm8_24"
|
||||
id="_r_7n_"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="_r_7n_"
|
||||
>
|
||||
Encrypt state events
|
||||
</label>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-_r_7p_"
|
||||
>
|
||||
Enables experimental support for encrypting state events, which hides metadata such as room names and topics from the server. This metadata will also be hidden from people joining rooms later, and people whose clients do not support MSC4362.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
data-testid="dialog-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Create room
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<CreateRoomDialog /> for a private room when the state encryption labs flag is on should turn on state encryption when toggled 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateRoomDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Create a private room
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_CreateRoomDialog_name"
|
||||
>
|
||||
<input
|
||||
id="mx_Field_29"
|
||||
label="Name"
|
||||
placeholder="Name"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_29"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_CreateRoomDialog_topic"
|
||||
>
|
||||
<input
|
||||
id="mx_Field_30"
|
||||
label="Topic (optional)"
|
||||
placeholder="Topic (optional)"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_30"
|
||||
>
|
||||
Topic (optional)
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Dropdown mx_JoinRuleDropdown mx_Dropdown_disabled"
|
||||
>
|
||||
<div
|
||||
aria-describedby="mx_JoinRuleDropdown_value"
|
||||
aria-disabled="true"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="Room visibility"
|
||||
aria-owns="mx_JoinRuleDropdown_input"
|
||||
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput mx_AccessibleButton_disabled"
|
||||
disabled=""
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Dropdown_option"
|
||||
id="mx_JoinRuleDropdown_value"
|
||||
>
|
||||
<div
|
||||
class="mx_JoinRuleDropdown_invite"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
|
||||
/>
|
||||
</svg>
|
||||
Private room (invite only)
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
class="mx_Dropdown_arrow"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Only people invited will be able to find and join this room. You can change this at any time from room settings.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_udcm8_24"
|
||||
id="_r_86_"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="_r_86_"
|
||||
>
|
||||
Enable end-to-end encryption
|
||||
</label>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-_r_88_"
|
||||
>
|
||||
You can't disable this later. Bridges & most bots won't work yet.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
class="_input_udcm8_24"
|
||||
id="_r_89_"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="_r_89_"
|
||||
>
|
||||
Encrypt state events
|
||||
</label>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-_r_8b_"
|
||||
>
|
||||
Enables experimental support for encrypting state events, which hides metadata such as room names and topics from the server. This metadata will also be hidden from people joining rooms later, and people whose clients do not support MSC4362.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -72,6 +72,20 @@ describe("EncryptionEvent", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should show the expected texts for experimental state event encryption", async () => {
|
||||
client.enableEncryptedStateEvents = true;
|
||||
event.event.content!["io.element.msc4362.encrypt_state_events"] = true;
|
||||
renderEncryptionEvent(client, event);
|
||||
await waitFor(() =>
|
||||
checkTexts(
|
||||
"Experimental state encryption enabled",
|
||||
"Messages and state events in this room are end-to-end encrypted. " +
|
||||
"When people join, you can verify them in their profile, " +
|
||||
"just tap on their profile picture.",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe("with same previous algorithm", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(event, "getPrevContent").mockReturnValue({
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||
import { act } from "jest-matrix-react";
|
||||
|
||||
import {
|
||||
stubClient,
|
||||
@@ -30,7 +31,11 @@ import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import WidgetStore from "../../src/stores/WidgetStore";
|
||||
import WidgetUtils from "../../src/utils/WidgetUtils";
|
||||
import { JitsiCall, ElementCall } from "../../src/models/Call";
|
||||
import createRoom, { checkUserIsAllowedToChangeEncryption, canEncryptToAllUsers } from "../../src/createRoom";
|
||||
import createRoom, {
|
||||
checkUserIsAllowedToChangeEncryption,
|
||||
canEncryptToAllUsers,
|
||||
waitForRoomEncryption,
|
||||
} from "../../src/createRoom";
|
||||
import SettingsStore from "../../src/settings/SettingsStore";
|
||||
import { ElementCallMemberEventType } from "../../src/call-types";
|
||||
import DMRoomMap from "../../src/utils/DMRoomMap";
|
||||
@@ -58,6 +63,149 @@ describe("createRoom", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a private room with encryption", async () => {
|
||||
await createRoom(client, { createOpts: { preset: Preset.PrivateChat }, encryption: true });
|
||||
|
||||
expect(client.createRoom).toHaveBeenCalledWith({
|
||||
preset: "private_chat",
|
||||
visibility: "private",
|
||||
initial_state: [
|
||||
{ state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } },
|
||||
{
|
||||
state_key: "",
|
||||
type: "m.room.encryption",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a private room with state event encryption", async () => {
|
||||
// When we create a room with state encryption and details like name,
|
||||
// topic, avatar
|
||||
const oldCreateRoom = await createRoomWithStateEncryption(client, {
|
||||
name: "Super-Secret Super-colliding Super Room",
|
||||
topic: "super **Topic**",
|
||||
avatar: "http://example.com/myavatar.png",
|
||||
});
|
||||
|
||||
// Then it is created with the right m.room.encryption event
|
||||
expect(oldCreateRoom).toHaveBeenCalledWith({
|
||||
preset: "private_chat",
|
||||
visibility: "private",
|
||||
initial_state: [
|
||||
{ state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } },
|
||||
{
|
||||
state_key: "",
|
||||
type: "m.room.encryption",
|
||||
content: {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"io.element.msc4362.encrypt_state_events": true,
|
||||
},
|
||||
},
|
||||
// Room name is NOT included, since it needs to be encrypted.
|
||||
],
|
||||
});
|
||||
|
||||
// And the room name, topic and avatar are set later
|
||||
expect(client.setRoomName).toHaveBeenCalledWith("!1:example.org", "Super-Secret Super-colliding Super Room");
|
||||
|
||||
expect(client.setRoomTopic).toHaveBeenCalledWith(
|
||||
"!1:example.org",
|
||||
"super **Topic**",
|
||||
"super <strong>Topic</strong>",
|
||||
);
|
||||
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
"!1:example.org",
|
||||
"m.room.avatar",
|
||||
{ url: "http://example.com/myavatar.png" },
|
||||
"",
|
||||
);
|
||||
});
|
||||
|
||||
it("creates a private room with state event encryption - file avatar", async () => {
|
||||
// When we create a room with state encryption and a file avatar
|
||||
client.uploadContent.mockResolvedValue({ content_uri: "mxc://foo.png" });
|
||||
const oldCreateRoom = await createRoomWithStateEncryption(client, {
|
||||
avatar: new File([], "myfile.png"),
|
||||
});
|
||||
|
||||
// Then it is created with the right m.room.encryption event
|
||||
expect(oldCreateRoom).toHaveBeenCalledWith({
|
||||
preset: "private_chat",
|
||||
visibility: "private",
|
||||
initial_state: [
|
||||
{ state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } },
|
||||
{
|
||||
state_key: "",
|
||||
type: "m.room.encryption",
|
||||
content: {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"io.element.msc4362.encrypt_state_events": true,
|
||||
},
|
||||
},
|
||||
// Room name is NOT included, since it needs to be encrypted.
|
||||
],
|
||||
});
|
||||
|
||||
// And the avatar is set later
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
"!1:example.org",
|
||||
"m.room.avatar",
|
||||
{ url: "mxc://foo.png" },
|
||||
"",
|
||||
);
|
||||
});
|
||||
|
||||
it("creates a private room with state event encryption - no details", async () => {
|
||||
// When we create a room with state encryption and no further room
|
||||
// details
|
||||
const oldCreateRoom = await createRoomWithStateEncryption(client, {});
|
||||
|
||||
// Then it is created with the right m.room.encryption event
|
||||
expect(oldCreateRoom).toHaveBeenCalledWith({
|
||||
preset: "private_chat",
|
||||
visibility: "private",
|
||||
initial_state: [
|
||||
{ state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } },
|
||||
{
|
||||
state_key: "",
|
||||
type: "m.room.encryption",
|
||||
content: {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"io.element.msc4362.encrypt_state_events": true,
|
||||
},
|
||||
},
|
||||
// Room name is NOT included, since it needs to be encrypted.
|
||||
],
|
||||
});
|
||||
|
||||
// And the room name, topic and avatar were not set since we didn't
|
||||
// supply them
|
||||
expect(client.setRoomName).not.toHaveBeenCalled();
|
||||
expect(client.setRoomTopic).not.toHaveBeenCalled();
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancels room creation if we time out before getting state events", async () => {
|
||||
// We are not testing createRoom here, just waitForRoomEncryption, which is used
|
||||
// inside. This allows us to pass in a shorter waitTime.
|
||||
|
||||
// Create a mock room that provides the needed methods
|
||||
const { room_id: roomId } = await client.createRoom({});
|
||||
const room = client.getRoom(roomId)!;
|
||||
room.getLiveTimeline = jest
|
||||
.fn()
|
||||
.mockReturnValue({ getState: jest.fn().mockReturnValue({ on: jest.fn(), off: jest.fn() }) });
|
||||
|
||||
// Call waitForRoomEncryption with a small timeout ans expect an error
|
||||
const error = new Error("Timed out while waiting for room to enable encryption");
|
||||
await expect(waitForRoomEncryption(room, 1)).rejects.toThrow(error);
|
||||
});
|
||||
|
||||
it("creates a private room in a space", async () => {
|
||||
const roomId = await createRoom(client, { roomType: RoomType.Space });
|
||||
const parentSpace = client.getRoom(roomId!)!;
|
||||
@@ -398,3 +546,42 @@ describe("checkUserIsAllowedToChangeEncryption()", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
interface RoomDetails {
|
||||
name?: string;
|
||||
topic?: string;
|
||||
avatar?: string | File;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call createRoom passing in stateEncryption: true, and set up the returned
|
||||
* room to return true when hasEncryptionStateEvent is called, to avoid
|
||||
* createRoom stalling forever waiting for an m.room.encryption event to arrive.
|
||||
*/
|
||||
async function createRoomWithStateEncryption(client: MatrixClient, roomDetails: RoomDetails) {
|
||||
const oldCreateRoom = client.createRoom;
|
||||
|
||||
// @ts-ignore Replacing createRoom
|
||||
client.createRoom = async (options) => {
|
||||
const { room_id: roomId } = await oldCreateRoom(options);
|
||||
const room = client.getRoom(roomId);
|
||||
room!.hasEncryptionStateEvent = () => true;
|
||||
return {
|
||||
room_id: roomId,
|
||||
};
|
||||
};
|
||||
|
||||
// When we create a room asking for state encryption
|
||||
await act(
|
||||
async () =>
|
||||
await createRoom(client, {
|
||||
createOpts: {
|
||||
preset: Preset.PrivateChat,
|
||||
},
|
||||
encryption: true,
|
||||
stateEncryption: true,
|
||||
...roomDetails,
|
||||
}),
|
||||
);
|
||||
return oldCreateRoom;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user