Fix local room encryption status always not enabled (#30461)
* Fix local room encryption status always not enabled * refactor: put back the e2e test after merge * fix: look at e2eStatus in composer of local room * doc: add docs to `LocalRoom.isEncryptionEnabled` * test(e2e): check composer doesn't display unencrypted state * test: update existing tests * test(e2e): update existing tests * refactor: move room encryption check in a dedicated function * refactor: make `isEncryptionEnabled` cleaner * test: add tests for `LocalRoom.isEncrypted` * doc: fix `useIsEncrypted` comment --------- Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
This commit is contained in:
co-authored by
Florian Duros
parent
8a1fc65beb
commit
cd7f1a0638
@@ -154,8 +154,8 @@ test.describe("Cryptography", function () {
|
|||||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||||
await startDMWithBob(page, bob);
|
await startDMWithBob(page, bob);
|
||||||
// send first message
|
// send first message
|
||||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hey!");
|
await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!");
|
||||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter");
|
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
|
||||||
await checkDMRoom(page);
|
await checkDMRoom(page);
|
||||||
const bobRoomId = await bobJoin(page, bob);
|
const bobRoomId = await bobJoin(page, bob);
|
||||||
// We no longer show the grey badge in the composer, check that it is not there.
|
// We no longer show the grey badge in the composer, check that it is not there.
|
||||||
|
|||||||
@@ -44,6 +44,21 @@ test.describe("Create Room", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test("should allow us to start a chat and show encryption state", async ({ page, user, app }) => {
|
||||||
|
await page.getByRole("button", { name: "Add", exact: true }).click();
|
||||||
|
await page.getByText("Start new chat").click();
|
||||||
|
|
||||||
|
await page.getByTestId("invite-dialog-input").fill(user.userId);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Go" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText("Encryption enabled")).toBeVisible();
|
||||||
|
await expect(page.getByText("Send your first message to")).toBeVisible();
|
||||||
|
|
||||||
|
const composer = page.getByRole("region", { name: "Message composer" });
|
||||||
|
await expect(composer.getByRole("textbox", { name: "Send a message…" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("should create a video room", { tag: "@screenshot" }, async ({ page, user, app }) => {
|
test("should create a video room", { tag: "@screenshot" }, async ({ page, user, app }) => {
|
||||||
await app.settings.setValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
|
await app.settings.setValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise<v
|
|||||||
await result.first().click();
|
await result.first().click();
|
||||||
|
|
||||||
// send first message to start DM
|
// send first message to start DM
|
||||||
const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
const locator = page.getByRole("textbox", { name: "Send a message…" });
|
||||||
await expect(locator).toBeFocused();
|
await expect(locator).toBeFocused();
|
||||||
await locator.fill("Hey!");
|
await locator.fill("Hey!");
|
||||||
await locator.press("Enter");
|
await locator.press("Enter");
|
||||||
@@ -260,7 +260,7 @@ test.describe("Spotlight", () => {
|
|||||||
|
|
||||||
// Send first message to actually start DM
|
// Send first message to actually start DM
|
||||||
await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName);
|
await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName);
|
||||||
const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
const locator = page.getByRole("textbox", { name: "Send a message…" });
|
||||||
await locator.fill("Hey!");
|
await locator.fill("Hey!");
|
||||||
await locator.press("Enter");
|
await locator.press("Enter");
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner";
|
|||||||
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext";
|
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext";
|
||||||
import { DeclineAndBlockInviteDialog } from "../views/dialogs/DeclineAndBlockInviteDialog";
|
import { DeclineAndBlockInviteDialog } from "../views/dialogs/DeclineAndBlockInviteDialog";
|
||||||
import { type FocusMessageSearchPayload } from "../../dispatcher/payloads/FocusMessageSearchPayload.ts";
|
import { type FocusMessageSearchPayload } from "../../dispatcher/payloads/FocusMessageSearchPayload.ts";
|
||||||
|
import { isRoomEncrypted } from "../../hooks/useIsEncrypted";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||||
@@ -257,6 +258,7 @@ interface LocalRoomViewProps {
|
|||||||
roomView: RefObject<HTMLElement | null>;
|
roomView: RefObject<HTMLElement | null>;
|
||||||
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
|
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
|
||||||
mainSplitContentType: MainSplitContentType;
|
mainSplitContentType: MainSplitContentType;
|
||||||
|
e2eStatus?: E2EStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -304,6 +306,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
|||||||
} else {
|
} else {
|
||||||
composer = (
|
composer = (
|
||||||
<MessageComposer
|
<MessageComposer
|
||||||
|
e2eStatus={props.e2eStatus}
|
||||||
room={props.localRoom}
|
room={props.localRoom}
|
||||||
resizeNotifier={props.resizeNotifier}
|
resizeNotifier={props.resizeNotifier}
|
||||||
permalinkCreator={props.permalinkCreator}
|
permalinkCreator={props.permalinkCreator}
|
||||||
@@ -1397,10 +1400,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getIsRoomEncrypted(roomId = this.state.roomId): Promise<boolean> {
|
private async getIsRoomEncrypted(roomId = this.state.roomId): Promise<boolean> {
|
||||||
const crypto = this.context.client?.getCrypto();
|
if (!roomId) return false;
|
||||||
if (!crypto || !roomId) return false;
|
|
||||||
|
|
||||||
return await crypto.isEncryptionEnabledInRoom(roomId);
|
const room = this.context.client?.getRoom(roomId);
|
||||||
|
const crypto = this.context.client?.getCrypto();
|
||||||
|
if (!room || !crypto) return false;
|
||||||
|
|
||||||
|
return isRoomEncrypted(room, crypto);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async calculateRecommendedVersion(room: Room): Promise<void> {
|
private async calculateRecommendedVersion(room: Room): Promise<void> {
|
||||||
@@ -2061,6 +2067,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
return (
|
return (
|
||||||
<ScopedRoomContextProvider {...this.state}>
|
<ScopedRoomContextProvider {...this.state}>
|
||||||
<LocalRoomView
|
<LocalRoomView
|
||||||
|
e2eStatus={this.state.e2eStatus}
|
||||||
localRoom={localRoom}
|
localRoom={localRoom}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
permalinkCreator={this.permalinkCreator}
|
permalinkCreator={this.permalinkCreator}
|
||||||
|
|||||||
@@ -7,9 +7,29 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MatrixClient, type MatrixEvent, type Room, EventType } from "matrix-js-sdk/src/matrix";
|
import { type MatrixClient, type MatrixEvent, type Room, EventType } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
import { useRoomState } from "./useRoomState.ts";
|
import { useRoomState } from "./useRoomState.ts";
|
||||||
import { useAsyncMemo } from "./useAsyncMemo.ts";
|
import { useAsyncMemo } from "./useAsyncMemo.ts";
|
||||||
|
import { LocalRoom } from "../models/LocalRoom.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a room is encrypted.
|
||||||
|
* If the room is a LocalRoom, check the state directly.
|
||||||
|
* Otherwise, use the crypto API to check if encryption is enabled in the room.
|
||||||
|
*
|
||||||
|
* @param room - The room to check.
|
||||||
|
* @param cryptoApi - The crypto API from the Matrix client.
|
||||||
|
*/
|
||||||
|
export async function isRoomEncrypted(room: Room, cryptoApi: CryptoApi): Promise<boolean> {
|
||||||
|
if (room instanceof LocalRoom) {
|
||||||
|
// For local room check the state.
|
||||||
|
// The crypto check fails because the room ID is not valid (it is a local id)
|
||||||
|
return (room as LocalRoom).isEncryptionEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await cryptoApi.isEncryptionEnabledInRoom(room.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
// Hook to simplify watching whether a Matrix room is encrypted, returns null if room is undefined or the state is loading
|
// Hook to simplify watching whether a Matrix room is encrypted, returns null if room is undefined or the state is loading
|
||||||
export function useIsEncrypted(cli: MatrixClient, room?: Room): boolean | null {
|
export function useIsEncrypted(cli: MatrixClient, room?: Room): boolean | null {
|
||||||
@@ -22,7 +42,7 @@ export function useIsEncrypted(cli: MatrixClient, room?: Room): boolean | null {
|
|||||||
const crypto = cli.getCrypto();
|
const crypto = cli.getCrypto();
|
||||||
if (!room || !crypto) return null;
|
if (!room || !crypto) return null;
|
||||||
|
|
||||||
return crypto.isEncryptionEnabledInRoom(room.roomId);
|
return isRoomEncrypted(room, crypto);
|
||||||
},
|
},
|
||||||
[room, encryptionStateEvent],
|
[room, encryptionStateEvent],
|
||||||
null,
|
null,
|
||||||
|
|||||||
+24
-1
@@ -6,7 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MatrixClient, Room, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
|
import {
|
||||||
|
type MatrixClient,
|
||||||
|
Room,
|
||||||
|
PendingEventOrdering,
|
||||||
|
MatrixEvent,
|
||||||
|
Direction,
|
||||||
|
EventType,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { type Member } from "../utils/direct-messages";
|
import { type Member } from "../utils/direct-messages";
|
||||||
|
|
||||||
@@ -50,4 +57,20 @@ export class LocalRoom extends Room {
|
|||||||
public get isError(): boolean {
|
public get isError(): boolean {
|
||||||
return this.state === LocalRoomState.ERROR;
|
return this.state === LocalRoomState.ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if encryption is enabled in this room.
|
||||||
|
* True if the room has any encryption state event
|
||||||
|
*/
|
||||||
|
public isEncryptionEnabled(): boolean {
|
||||||
|
const roomState = this.getLiveTimeline().getState(Direction.Forward);
|
||||||
|
if (!roomState) return false;
|
||||||
|
|
||||||
|
const stateEvents = roomState.getStateEvents(EventType.RoomEncryption);
|
||||||
|
if (stateEvents.length === 0) return false;
|
||||||
|
|
||||||
|
// if there is an encryption state event, it is encrypted.
|
||||||
|
// Regardless of the content/algorithm, we assume it is encrypted.
|
||||||
|
return stateEvents[0] instanceof MatrixEvent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1183,7 +1183,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-label="Message composer"
|
aria-label="Message composer"
|
||||||
class="mx_MessageComposer mx_MessageComposer_e2eStatus"
|
class="mx_MessageComposer"
|
||||||
role="region"
|
role="region"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1192,25 +1192,6 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
|||||||
<div
|
<div
|
||||||
class="mx_MessageComposer_row"
|
class="mx_MessageComposer_row"
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
class="mx_MessageComposer_e2eIconWrapper"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-label="Messages in this room are not end-to-end encrypted"
|
|
||||||
aria-labelledby="«rfm»"
|
|
||||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
|
||||||
color="var(--cpd-color-icon-info-primary)"
|
|
||||||
fill="currentColor"
|
|
||||||
height="12"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="12"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6 22q-.825 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412a2 2 0 0 1 .702-.463L1.333 4.167a1 1 0 0 1 1.414-1.414L7 7.006v-.012l13 13v.012l1.247 1.247a1 1 0 1 1-1.414 1.414l-.896-.896A1.94 1.94 0 0 1 18 22zm14-4.834V10q0-.825-.587-1.412A1.93 1.93 0 0 0 18 8h-1V6q0-2.075-1.463-3.537Q14.075 1 12 1T8.463 2.463a4.9 4.9 0 0 0-1.22 1.946L9 6.166V6q0-1.25.875-2.125A2.9 2.9 0 0 1 12 3q1.25 0 2.125.875T15 6v2h-4.166z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="mx_SendMessageComposer"
|
class="mx_SendMessageComposer"
|
||||||
>
|
>
|
||||||
@@ -1269,14 +1250,14 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
|||||||
aria-autocomplete="list"
|
aria-autocomplete="list"
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-label="Send an unencrypted message…"
|
aria-label="Send a message…"
|
||||||
aria-multiline="true"
|
aria-multiline="true"
|
||||||
class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty"
|
class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty"
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
data-testid="basicmessagecomposer"
|
data-testid="basicmessagecomposer"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
role="textbox"
|
role="textbox"
|
||||||
style="--placeholder: 'Send\\ an\\ unencrypted\\ message…';"
|
style="--placeholder: 'Send\\ a\\ message…';"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
translate="no"
|
translate="no"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -112,16 +112,19 @@ describe("EncryptionEvent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("for an encrypted local room", () => {
|
describe("for an encrypted local room", () => {
|
||||||
|
let localRoom: LocalRoom;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
event.event.content!.algorithm = algorithm;
|
event.event.content!.algorithm = algorithm;
|
||||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
// jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||||
const localRoom = new LocalRoom(roomId, client, client.getUserId()!);
|
localRoom = new LocalRoom(roomId, client, client.getUserId()!);
|
||||||
|
jest.spyOn(localRoom, "isEncryptionEnabled").mockReturnValue(true);
|
||||||
mocked(client.getRoom).mockReturnValue(localRoom);
|
mocked(client.getRoom).mockReturnValue(localRoom);
|
||||||
renderEncryptionEvent(client, event);
|
renderEncryptionEvent(client, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the expected texts", async () => {
|
it("should show the expected texts", async () => {
|
||||||
expect(client.getCrypto()!.isEncryptionEnabledInRoom).toHaveBeenCalledWith(roomId);
|
expect(localRoom.isEncryptionEnabled).toHaveBeenCalled();
|
||||||
await checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted.");
|
await checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { Direction, EventType, type MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { LocalRoom, LocalRoomState, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom";
|
import { LocalRoom, LocalRoomState, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom";
|
||||||
import { createTestClient } from "../../test-utils";
|
import { createTestClient } from "../../test-utils";
|
||||||
@@ -79,4 +79,26 @@ describe("LocalRoom", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return false for isEncryptionEnabled with no state", () => {
|
||||||
|
expect(room.isEncryptionEnabled()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true for isEncryptionEnabled with an encryption state event", () => {
|
||||||
|
const encryptionEvent = new MatrixEvent({
|
||||||
|
type: EventType.RoomEncryption,
|
||||||
|
state_key: "",
|
||||||
|
content: {
|
||||||
|
algorithm: "m.megolm.v1.aes-sha2",
|
||||||
|
},
|
||||||
|
sender: "@test:localhost",
|
||||||
|
room_id: room.roomId,
|
||||||
|
event_id: "$test:localhost",
|
||||||
|
});
|
||||||
|
|
||||||
|
const roomState = room.getLiveTimeline().getState(Direction.Forward);
|
||||||
|
roomState?.setStateEvents([encryptionEvent]);
|
||||||
|
|
||||||
|
expect(room.isEncryptionEnabled()).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user