Skip to content

Commit

Permalink
Merge branch 'develop' into florianduros/rip-out-legacy-crypto/aes
Browse files Browse the repository at this point in the history
  • Loading branch information
florianduros authored Oct 1, 2024
2 parents 05e5725 + 39a0f6e commit 6eec203
Show file tree
Hide file tree
Showing 33 changed files with 299 additions and 281 deletions.
18 changes: 9 additions & 9 deletions .github/labels.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
- name: "A-Timesheet-1"
description: "Log any time spent on this into the A-Timesheet-1 project"
color: "#5319E7"
color: "5319E7"
- name: "backport staging"
description: "Label to automatically backport PR to staging branch"
color: "#B60205"
color: "B60205"
- name: "Dependencies"
description: "Pull requests that update a dependency file"
color: "#0366d6"
color: "0366d6"
- name: "Sponsored"
color: "#b506d8"
color: "b506d8"
- name: "T-Deprecation"
description: "A pull request that makes something deprecated"
color: "#98e6ae"
color: "98e6ae"
- name: "X-Blocked"
description: "The PR cannot move forward in any capacity until an action is made"
color: "ff7979"
- name: "X-Breaking-Change"
color: "#ff7979"
color: "ff7979"
- name: "X-Upcoming-Release-Blocker"
description: "This does not affect the current release cycle but will affect the next one"
color: "#e99695"
color: "e99695"
- name: "Z-Community-PR"
description: "Issue is solved by a community member's PR"
color: "#ededed"
color: "ededed"
- name: "Z-Experiment"
description: "Experimental PR, primarily up for its Netlify build, high likelihood of never making it beyond here."
color: "#b60205"
color: "b60205"
14 changes: 9 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,17 @@ jobs:
coverage
!coverage/lcov-report
skip_sonar:
name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
runs-on: ubuntu-latest
complete:
name: jest-tests
needs: jest
if: always()
runs-on: ubuntu-latest
steps:
- name: Skip SonarCloud
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
run: exit 1

- name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
Expand Down
24 changes: 9 additions & 15 deletions playwright/e2e/crypto/event-shields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { Page } from "@playwright/test";

import { expect, test } from "../../element-web-test";
import { autoJoin, createSharedRoomWithUser, enableKeyBackup, logIntoElement, logOutOfElement, verify } from "./utils";
import { Bot } from "../../pages/bot";
import { HomeserverInstance } from "../../plugins/homeserver";
import {
autoJoin,
createSecondBotDevice,
createSharedRoomWithUser,
enableKeyBackup,
logIntoElement,
logOutOfElement,
verify,
} from "./utils";

test.describe("Cryptography", function () {
test.use({
Expand Down Expand Up @@ -296,13 +300,3 @@ test.describe("Cryptography", function () {
});
});
});

async function createSecondBotDevice(page: Page, homeserver: HomeserverInstance, bob: Bot) {
const bobSecondDevice = new Bot(page, homeserver, {
bootstrapSecretStorage: false,
bootstrapCrossSigning: false,
});
bobSecondDevice.setCredentials(await homeserver.loginUser(bob.credentials.userId, bob.credentials.password));
await bobSecondDevice.prepareClient();
return bobSecondDevice;
}
56 changes: 56 additions & 0 deletions playwright/e2e/crypto/invisible-crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { expect, test } from "../../element-web-test";
import { autoJoin, createSecondBotDevice, createSharedRoomWithUser, verify } from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";

/** Tests for the "invisible crypto" behaviour -- i.e., when the "exclude insecure devices" setting is enabled */
test.describe("Invisible cryptography", () => {
test.use({
displayName: "Alice",
botCreateOpts: { displayName: "Bob" },
labsFlags: ["feature_exclude_insecure_devices"],
});

test("Messages fail to decrypt when sender is previously verified", async ({
page,
bot: bob,
user: aliceCredentials,
app,
homeserver,
}) => {
await app.client.bootstrapCrossSigning(aliceCredentials);
await autoJoin(bob);

// create an encrypted room
const testRoomId = await createSharedRoomWithUser(app, bob.credentials.userId, {
name: "TestRoom",
initial_state: [
{
type: "m.room.encryption",
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
},
],
});

// Verify Bob
await verify(app, bob);

// Bob logs in a new device and resets cross-signing
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);

/* should show an error for a message from a previously verified device */
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
const lastTile = page.locator(".mx_EventTile_last");
await expect(lastTile).toContainText("Verified identity has changed");
});
});
11 changes: 11 additions & 0 deletions playwright/e2e/crypto/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,14 @@ export async function awaitVerifier(
return verificationRequest.verifier;
});
}

/** Log in a second device for the given bot user */
export async function createSecondBotDevice(page: Page, homeserver: HomeserverInstance, bob: Bot) {
const bobSecondDevice = new Bot(page, homeserver, {
bootstrapSecretStorage: false,
bootstrapCrossSigning: false,
});
bobSecondDevice.setCredentials(await homeserver.loginUser(bob.credentials.userId, bob.credentials.password));
await bobSecondDevice.prepareClient();
return bobSecondDevice;
}
3 changes: 3 additions & 0 deletions playwright/e2e/read-receipts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,9 @@ class Helpers {
*/
async closeThreadsPanel() {
await this.page.locator(".mx_RoomHeader").getByLabel("Threads").click();
if (await this.page.locator("#thread-panel").isVisible()) {
await this.page.locator(".mx_RoomHeader").getByLabel("Threads").click();
}
await expect(this.page.locator(".mx_RightPanel")).not.toBeVisible();
}

Expand Down
21 changes: 21 additions & 0 deletions res/css/views/messages/_DecryptionFailureBody.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,24 @@ Please see LICENSE files in the repository root for full details.
color: $secondary-content;
font-style: italic;
}

/* Formatting for the "Verified identity has changed" error */
.mx_DecryptionFailureVerifiedIdentityChanged > span {
/* Show it in red */
color: var(--cpd-color-text-critical-primary);
background-color: var(--cpd-color-bg-critical-subtle);

/* With a red border */
border: 1px solid var(--cpd-color-border-critical-subtle);
border-radius: $font-16px;

/* Some space inside the border */
padding: var(--cpd-space-1x) var(--cpd-space-3x) var(--cpd-space-1x) var(--cpd-space-2x);

/* some space between the (!) icon and text */
display: inline-flex;
gap: var(--cpd-space-2x);

/* Center vertically */
align-items: center;
}
4 changes: 0 additions & 4 deletions src/BasePlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,12 +432,8 @@ export default abstract class BasePlatform {
redirectUris: [this.getOidcCallbackUrl().href],
logoUri: config.oidc_metadata?.logo_uri ?? new URL("vector-icons/1024.png", this.baseUrl).href,
applicationType: "web",
// XXX: We break the spec by not consistently supplying these required fields
// @ts-ignore
contacts: config.oidc_metadata?.contacts,
// @ts-ignore
tosUri: config.oidc_metadata?.tos_uri ?? config.terms_and_conditions_links?.[0]?.url,
// @ts-ignore
policyUri: config.oidc_metadata?.policy_uri ?? config.privacy_policy_url,
};
}
Expand Down
7 changes: 1 addition & 6 deletions src/Notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,12 +513,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
const thisUserHasConnectedDevice =
room && MatrixRTCSession.callMembershipsForRoom(room).some((m) => m.sender === cli.getUserId());

if (
EventType.CallNotify === ev.getType() &&
SettingsStore.getValue("feature_group_calls") &&
(ev.getAge() ?? 0) < 10000 &&
!thisUserHasConnectedDevice
) {
if (EventType.CallNotify === ev.getType() && (ev.getAge() ?? 0) < 10000 && !thisUserHasConnectedDevice) {
const content = ev.getContent();
const roomId = ev.getRoomId();
if (typeof content.call_id !== "string") {
Expand Down
22 changes: 11 additions & 11 deletions src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -614,10 +614,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};

private getMainSplitContentType = (room: Room): MainSplitContentType => {
if (
(SettingsStore.getValue("feature_group_calls") && this.context.roomViewStore.isViewingCall()) ||
isVideoRoom(room)
) {
if (this.context.roomViewStore.isViewingCall() || isVideoRoom(room)) {
return MainSplitContentType.Call;
}
if (this.context.widgetLayoutStore.hasMaximisedWidget(room)) {
Expand Down Expand Up @@ -2183,10 +2180,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}

const myMembership = this.state.room.getMyMembership();
if (
isVideoRoom(this.state.room) &&
!(SettingsStore.getValue("feature_video_rooms") && myMembership === KnownMembership.Join)
) {
if (isVideoRoom(this.state.room) && myMembership !== KnownMembership.Join) {
return (
<ErrorBoundary>
<div className="mx_MainSplit">
Expand Down Expand Up @@ -2521,17 +2515,23 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
mx_RoomView_timeline_rr_enabled: this.state.showReadReceipts,
});

let { mainSplitContentType } = this.state;
if (this.state.search) {
// When in the middle of a search force the main split content type to timeline
mainSplitContentType = MainSplitContentType.Timeline;
}

const mainClasses = classNames("mx_RoomView", {
mx_RoomView_inCall: Boolean(activeCall),
mx_RoomView_immersive: this.state.mainSplitContentType !== MainSplitContentType.Timeline,
mx_RoomView_immersive: mainSplitContentType !== MainSplitContentType.Timeline,
});

const showChatEffects = SettingsStore.getValue("showChatEffects");

let mainSplitBody: JSX.Element | undefined;
let mainSplitContentClassName: string | undefined;
// Decide what to show in the main split
switch (this.state.mainSplitContentType) {
switch (mainSplitContentType) {
case MainSplitContentType.Timeline:
mainSplitContentClassName = "mx_MainSplit_timeline";
mainSplitBody = (
Expand Down Expand Up @@ -2595,7 +2595,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
let viewingCall = false;

// Simplify the header for other main split types
switch (this.state.mainSplitContentType) {
switch (mainSplitContentType) {
case MainSplitContentType.MaximisedWidget:
excludedRightPanelPhaseButtons = [];
onAppsClick = null;
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/context_menus/RoomContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { shouldShowComponent } from "../../../customisations/helpers/UIComponent
import { UIComponent } from "../../../settings/UIFeature";
import { DeveloperToolsOption } from "./DeveloperToolsOption";
import { tagRoom } from "../../../utils/room/tagRoom";
import { useIsVideoRoom } from "../../../utils/video-rooms";
import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";
import { usePinnedEvents } from "../../../hooks/usePinnedEvents";

interface IProps extends IContextMenuProps {
Expand Down Expand Up @@ -105,7 +105,7 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
}

const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
const isVideoRoom = useIsVideoRoom(room);
const isVideoRoom = calcIsVideoRoom(room);
const canInvite = useEventEmitterState(cli, RoomMemberEvent.PowerLevel, () => room.canInvite(cli.getUserId()!));
let inviteOption: JSX.Element | undefined;
if (canInvite && !isDm && shouldShowComponent(UIComponent.InviteUsers)) {
Expand Down
34 changes: 32 additions & 2 deletions src/components/views/messages/DecryptionFailureBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import classNames from "classnames";
import React, { forwardRef, ForwardRefExoticComponent, useContext } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";

import { _t } from "../../../languageHandler";
import { IBodyProps } from "./IBodyProps";
import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext";
import { Icon as WarningBadgeIcon } from "../../../../res/img/compound/error-16px.svg";

function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string {
function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string | React.JSX.Element {
switch (mxEvent.decryptionFailureReason) {
case DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE:
return _t("timeline|decryption_failure|blocked");
Expand All @@ -32,16 +34,44 @@ function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined):
break;

case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED:
// TODO: event should be hidden instead of showing this error.
// To be revisited as part of https://github.com/element-hq/element-meta/issues/2449
return _t("timeline|decryption_failure|historical_event_user_not_joined");

case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
return (
<span>
<WarningBadgeIcon className="mx_Icon mx_Icon_16" />
{_t("timeline|decryption_failure|sender_identity_previously_verified")}
</span>
);

case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE:
// TODO: event should be hidden instead of showing this error.
// To be revisited as part of https://github.com/element-hq/element-meta/issues/2449
return _t("timeline|decryption_failure|sender_unsigned_device");
}
return _t("timeline|decryption_failure|unable_to_decrypt");
}

/** Get an extra CSS class, specific to the decryption failure reason */
function errorClassName(mxEvent: MatrixEvent): string | null {
switch (mxEvent.decryptionFailureReason) {
case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
return "mx_DecryptionFailureVerifiedIdentityChanged";

default:
return null;
}
}

// A placeholder element for messages that could not be decrypted
export const DecryptionFailureBody = forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent }, ref): React.JSX.Element => {
const verificationState = useContext(LocalDeviceVerificationStateContext);
const classes = classNames("mx_DecryptionFailureBody", "mx_EventTile_content", errorClassName(mxEvent));

return (
<div className="mx_DecryptionFailureBody mx_EventTile_content" ref={ref}>
<div className={classes} ref={ref}>
{getErrorMessage(mxEvent, verificationState)}
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/right_panel/RightPanelTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { Action } from "../../../dispatcher/actions";
import SettingsStore from "../../../settings/SettingsStore";
import { UIComponent, UIFeature } from "../../../settings/UIFeature";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { useIsVideoRoom } from "../../../utils/video-rooms";
import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";

function shouldShowTabsForPhase(phase?: RightPanelPhases): boolean {
const tabs = [
Expand Down Expand Up @@ -48,7 +48,7 @@ export const RightPanelTabs: React.FC<Props> = ({ phase, room }): JSX.Element |
}
});

const isVideoRoom = useIsVideoRoom(room);
const isVideoRoom = room !== undefined && calcIsVideoRoom(room);

if (!shouldShowTabsForPhase(phase)) return null;

Expand Down
Loading

0 comments on commit 6eec203

Please sign in to comment.